From 3d45cf27cf586c9afe078ceb06fee115ea246a92 Mon Sep 17 00:00:00 2001 From: Rusty Russell Date: Mon, 19 Dec 2011 11:51:45 +1030 Subject: [PATCH] opt: much prettier usage (using terminal size) --- ccan/opt/test/run-add_desc.c | 164 ++++++++++++++++++++ ccan/opt/test/run-consume_words.c | 37 +++++ ccan/opt/test/run-helpers.c | 2 +- ccan/opt/test/run-usage.c | 9 ++ ccan/opt/usage.c | 239 +++++++++++++++++++++--------- 5 files changed, 381 insertions(+), 70 deletions(-) create mode 100644 ccan/opt/test/run-add_desc.c create mode 100644 ccan/opt/test/run-consume_words.c diff --git a/ccan/opt/test/run-add_desc.c b/ccan/opt/test/run-add_desc.c new file mode 100644 index 00000000..ded3f880 --- /dev/null +++ b/ccan/opt/test/run-add_desc.c @@ -0,0 +1,164 @@ +#include +#include +#include +#include +#include + +static void show_10(char buf[OPT_SHOW_LEN], const void *arg) +{ + memset(buf, 'X', 10); + buf[10] = '\0'; +} + +static void show_max(char buf[OPT_SHOW_LEN], const void *arg) +{ + memset(buf, 'X', OPT_SHOW_LEN); +} + +/* Test add_desc helper. */ +int main(int argc, char *argv[]) +{ + struct opt_table opt; + char *ret; + size_t len, max; + + plan_tests(30); + + opt.show = NULL; + opt.names = "01234"; + opt.desc = "0123456789 0"; + opt.type = OPT_NOARG; + len = max = 0; + + /* Fits easily. */ + ret = add_desc(NULL, &len, &max, 10, 30, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, "01234 0123456789 0\n") == 0); + free(ret); len = max = 0; + + /* Name just fits. */ + ret = add_desc(NULL, &len, &max, 7, 30, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, "01234 0123456789 0\n") == 0); + free(ret); len = max = 0; + + /* Name doesn't fit. */ + ret = add_desc(NULL, &len, &max, 6, 30, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, + "01234\n" + " 0123456789 0\n") == 0); + free(ret); len = max = 0; + + /* Description just fits. */ + ret = add_desc(NULL, &len, &max, 7, 19, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, "01234 0123456789 0\n") == 0); + free(ret); len = max = 0; + + /* Description doesn't quite fit. */ + ret = add_desc(NULL, &len, &max, 7, 18, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, + "01234 0123456789\n" + " 0\n") == 0); + free(ret); len = max = 0; + + /* Neither quite fits. */ + ret = add_desc(NULL, &len, &max, 6, 17, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, + "01234\n" + " 0123456789\n" + " 0\n") == 0); + free(ret); len = max = 0; + + /* With show function, fits just. */ + opt.show = show_10; + ret = add_desc(NULL, &len, &max, 7, 41, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, "01234 0123456789 0 (default: XXXXXXXXXX)\n") == 0); + free(ret); len = max = 0; + + /* With show function, just too long. */ + ret = add_desc(NULL, &len, &max, 7, 40, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, + "01234 0123456789 0\n" + " (default: XXXXXXXXXX)\n") == 0); + free(ret); len = max = 0; + + /* With maximal show function, fits just (we assume OPT_SHOW_LEN = 80. */ + opt.show = show_max; + ret = add_desc(NULL, &len, &max, 7, 114, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, "01234 0123456789 0 (default: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX...)\n") == 0); + free(ret); len = max = 0; + + /* With maximal show function, just too long. */ + ret = add_desc(NULL, &len, &max, 7, 113, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, + "01234 0123456789 0\n" + " (default: XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX...)\n") == 0); + free(ret); len = max = 0; + + /* With added " ". Fits, just. */ + opt.show = NULL; + opt.type = OPT_HASARG; + ret = add_desc(NULL, &len, &max, 13, 25, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, "01234 0123456789 0\n") == 0); + free(ret); len = max = 0; + + /* With added " ". Name doesn't quite fit. */ + ret = add_desc(NULL, &len, &max, 12, 25, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, + "01234 \n" + " 0123456789 0\n") == 0); + free(ret); len = max = 0; + + /* With added " ". Desc doesn't quite fit. */ + ret = add_desc(NULL, &len, &max, 13, 24, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, + "01234 0123456789\n" + " 0\n") == 0); + free(ret); len = max = 0; + + /* Empty description, with and default. Just fits. */ + opt.show = show_10; + opt.desc = ""; + ret = add_desc(NULL, &len, &max, 13, 35, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, "01234 (default: XXXXXXXXXX)\n") == 0); + free(ret); len = max = 0; + + /* Empty description, with and default. Doesn't quite fit. */ + opt.show = show_10; + opt.desc = ""; + ret = add_desc(NULL, &len, &max, 13, 34, &opt); + ok1(len < max); + ret[len] = '\0'; + ok1(strcmp(ret, + "01234 \n" + " (default: XXXXXXXXXX)\n") == 0); + free(ret); len = max = 0; + + return exit_status(); +} diff --git a/ccan/opt/test/run-consume_words.c b/ccan/opt/test/run-consume_words.c new file mode 100644 index 00000000..ae4d5d3e --- /dev/null +++ b/ccan/opt/test/run-consume_words.c @@ -0,0 +1,37 @@ +#include +#include +#include +#include +#include + +/* Test consume_words helper. */ +int main(int argc, char *argv[]) +{ + unsigned int start, len; + + plan_tests(13); + + /* Every line over width. */ + len = consume_words("hello world", 1, &start); + ok1(start == 0); + ok1(len == strlen("hello")); + len = consume_words(" world", 1, &start); + ok1(start == 1); + ok1(len == strlen("world")); + ok1(consume_words("", 1, &start) == 0); + + /* Same with width where won't both fit. */ + len = consume_words("hello world", 5, &start); + ok1(start == 0); + ok1(len == strlen("hello")); + len = consume_words(" world", 5, &start); + ok1(start == 1); + ok1(len == strlen("world")); + ok1(consume_words("", 5, &start) == 0); + + len = consume_words("hello world", 11, &start); + ok1(start == 0); + ok1(len == strlen("hello world")); + ok1(consume_words("", 11, &start) == 0); + return exit_status(); +} diff --git a/ccan/opt/test/run-helpers.c b/ccan/opt/test/run-helpers.c index de60ac9f..10b24190 100644 --- a/ccan/opt/test/run-helpers.c +++ b/ccan/opt/test/run-helpers.c @@ -1024,7 +1024,7 @@ int main(int argc, char *argv[]) } ok1(strstr(output, "[args]")); ok1(strstr(output, argv[0])); - ok1(strstr(output, "[-a]")); + ok1(strstr(output, "\n-a")); free(output); free(argv); output = NULL; diff --git a/ccan/opt/test/run-usage.c b/ccan/opt/test/run-usage.c index fa1dc64c..7d94ced2 100644 --- a/ccan/opt/test/run-usage.c +++ b/ccan/opt/test/run-usage.c @@ -4,6 +4,15 @@ #include #include #include "utils.h" + +/* Ensure width is sane. */ +static const char *getenv_override(const char *name) +{ + return "100"; +} + +#define getenv getenv_override + #include #include #include diff --git a/ccan/opt/usage.c b/ccan/opt/usage.c index 873ee5db..d9b2ee59 100644 --- a/ccan/opt/usage.c +++ b/ccan/opt/usage.c @@ -1,5 +1,6 @@ /* Licensed under GPLv3+ - see LICENSE file for details */ #include +#include #include #include #include @@ -9,26 +10,177 @@ /* We only use this for pointer comparisons. */ const char opt_hidden[1]; -static unsigned write_short_options(char *str) +#define MIN_DESC_WIDTH 40 +#define MIN_TOTAL_WIDTH 50 + +static unsigned int get_columns(void) { - unsigned int i, num = 0; - const char *p; + struct winsize w; + const char *env = getenv("COLUMNS"); + + w.ws_col = 0; + if (env) + w.ws_col = atoi(env); + if (!w.ws_col) + if (ioctl(0, TIOCGWINSZ, &w) == -1) + w.ws_col = 0; + if (!w.ws_col) + w.ws_col = 80; + + return w.ws_col; +} + +/* Return number of chars of words to put on this line. + * Prefix is set to number to skip at start, maxlen is max width, returns + * length (after prefix) to put on this line. */ +static size_t consume_words(const char *words, size_t maxlen, size_t *prefix) +{ + size_t oldlen, len; + + /* Swallow leading whitespace. */ + *prefix = strspn(words, " "); + words += *prefix; - for (p = first_sopt(&i); p; p = next_sopt(p, &i)) { - if (opt_table[i].desc != opt_hidden) - str[num++] = *p; + /* Use at least one word, even if it takes us over maxlen. */ + oldlen = len = strcspn(words, " "); + while (len <= maxlen) { + oldlen = len; + len += strspn(words+len, " "); + len += strcspn(words+len, " "); + if (len == oldlen) + break; } - return num; + + return oldlen; +} + +static char *add_str_len(char *base, size_t *len, size_t *max, + const char *str, size_t slen) +{ + if (slen >= *max - *len) + base = realloc(base, *max = (*max * 2 + slen + 1)); + memcpy(base + *len, str, slen); + *len += slen; + return base; } -#define OPT_SPACE_PAD " " +static char *add_str(char *base, size_t *len, size_t *max, const char *str) +{ + return add_str_len(base, len, max, str, strlen(str)); +} + +static char *add_indent(char *base, size_t *len, size_t *max, size_t indent) +{ + if (indent >= *max - *len) + base = realloc(base, *max = (*max * 2 + indent + 1)); + memset(base + *len, ' ', indent); + *len += indent; + return base; +} + +static char *add_desc(char *base, size_t *len, size_t *max, + unsigned int indent, unsigned int width, + const struct opt_table *opt) +{ + size_t off, prefix, l; + const char *p; + bool same_line = false; + + base = add_str(base, len, max, opt->names); + off = strlen(opt->names); + if (opt->type == OPT_HASARG + && !strchr(opt->names, ' ') + && !strchr(opt->names, '=')) { + base = add_str(base, len, max, " "); + off += strlen(" "); + } + + /* Do we start description on next line? */ + if (off + 2 > indent) { + base = add_str(base, len, max, "\n"); + off = 0; + } else { + base = add_indent(base, len, max, indent - off); + off = indent; + same_line = true; + } + + /* Indent description. */ + p = opt->desc; + while ((l = consume_words(p, width - indent, &prefix)) != 0) { + if (!same_line) + base = add_indent(base, len, max, indent); + p += prefix; + base = add_str_len(base, len, max, p, l); + base = add_str(base, len, max, "\n"); + off = indent + l; + p += l; + same_line = false; + } + + /* Empty description? Make it match normal case. */ + if (same_line) + base = add_str(base, len, max, "\n"); + + if (opt->show) { + char buf[OPT_SHOW_LEN + sizeof("...")]; + strcpy(buf + OPT_SHOW_LEN, "..."); + opt->show(buf, opt->u.arg); + + /* If it doesn't fit on this line, indent. */ + if (off + strlen(" (default: ") + strlen(buf) + strlen(")") + > width) { + base = add_indent(base, len, max, indent); + } else { + /* Remove \n. */ + (*len)--; + } + + base = add_str(base, len, max, " (default: "); + base = add_str(base, len, max, buf); + base = add_str(base, len, max, ")\n"); + } + return base; +} -/* FIXME: Get all purdy. */ char *opt_usage(const char *argv0, const char *extra) { - unsigned int i, num, len; - char *ret, *p; + unsigned int i; + size_t max, len, width, indent; + char *ret; + + width = get_columns(); + if (width < MIN_TOTAL_WIDTH) + width = MIN_TOTAL_WIDTH; + + /* Figure out longest option. */ + indent = 0; + for (i = 0; i < opt_count; i++) { + size_t l; + if (opt_table[i].desc == opt_hidden) + continue; + if (opt_table[i].type == OPT_SUBTABLE) + continue; + l = strlen(opt_table[i].names); + if (opt_table[i].type == OPT_HASARG + && !strchr(opt_table[i].names, ' ') + && !strchr(opt_table[i].names, '=')) + l += strlen(" "); + if (l + 2 > indent) + indent = l + 2; + } + + /* Now we know how much to indent */ + if (indent + MIN_DESC_WIDTH > width) + indent = width - MIN_DESC_WIDTH; + + len = max = 0; + ret = NULL; + ret = add_str(ret, &len, &max, "Usage: "); + ret = add_str(ret, &len, &max, argv0); + + /* Find usage message from among registered options if necessary. */ if (!extra) { extra = ""; for (i = 0; i < opt_count; i++) { @@ -39,71 +191,20 @@ char *opt_usage(const char *argv0, const char *extra) } } } - - /* An overestimate of our length. */ - len = strlen("Usage: %s ") + strlen(argv0) - + strlen("[-%.*s]") + opt_num_short + 1 - + strlen(" ") + strlen(extra) - + strlen("\n"); - - for (i = 0; i < opt_count; i++) { - if (opt_table[i].type == OPT_SUBTABLE) { - len += strlen("\n") + strlen(opt_table[i].desc) - + strlen(":\n"); - } else if (opt_table[i].desc != opt_hidden) { - len += strlen(opt_table[i].names) + strlen(" "); - len += strlen(OPT_SPACE_PAD) - + strlen(opt_table[i].desc) + 1; - if (opt_table[i].show) { - len += strlen("(default: %s)") - + OPT_SHOW_LEN + sizeof("..."); - } - len += strlen("\n"); - } - } - - p = ret = malloc(len); - p += sprintf(p, "Usage: %s", argv0); - p += sprintf(p, " [-"); - num = write_short_options(p); - if (num) { - p += num; - p += sprintf(p, "]"); - } else { - /* Remove start of single-entry options */ - p -= 3; - } - if (extra) - p += sprintf(p, " %s", extra); - p += sprintf(p, "\n"); + ret = add_str(ret, &len, &max, " "); + ret = add_str(ret, &len, &max, extra); + ret = add_str(ret, &len, &max, "\n"); for (i = 0; i < opt_count; i++) { if (opt_table[i].desc == opt_hidden) continue; if (opt_table[i].type == OPT_SUBTABLE) { - p += sprintf(p, "%s:\n", opt_table[i].desc); + ret = add_str(ret, &len, &max, opt_table[i].desc); + ret = add_str(ret, &len, &max, ":\n"); continue; } - len = sprintf(p, "%s", opt_table[i].names); - if (opt_table[i].type == OPT_HASARG - && !strchr(opt_table[i].names, ' ') - && !strchr(opt_table[i].names, '=')) - len += sprintf(p + len, " "); - len += sprintf(p + len, "%.*s", - len < strlen(OPT_SPACE_PAD) - ? (unsigned)strlen(OPT_SPACE_PAD) - len : 1, - OPT_SPACE_PAD); - - len += sprintf(p + len, "%s", opt_table[i].desc); - if (opt_table[i].show) { - char buf[OPT_SHOW_LEN + sizeof("...")]; - strcpy(buf + OPT_SHOW_LEN, "..."); - opt_table[i].show(buf, opt_table[i].u.arg); - len += sprintf(p + len, " (default: %s)", buf); - } - p += len; - p += sprintf(p, "\n"); + ret = add_desc(ret, &len, &max, indent, width, &opt_table[i]); } - *p = '\0'; + ret[len] = '\0'; return ret; } -- 2.39.2