]> git.ozlabs.org Git - ccan/commitdiff
opt: much prettier usage (using terminal size)
authorRusty Russell <rusty@rustcorp.com.au>
Mon, 19 Dec 2011 01:21:45 +0000 (11:51 +1030)
committerRusty Russell <rusty@rustcorp.com.au>
Mon, 19 Dec 2011 01:21:45 +0000 (11:51 +1030)
ccan/opt/test/run-add_desc.c [new file with mode: 0644]
ccan/opt/test/run-consume_words.c [new file with mode: 0644]
ccan/opt/test/run-helpers.c
ccan/opt/test/run-usage.c
ccan/opt/usage.c

diff --git a/ccan/opt/test/run-add_desc.c b/ccan/opt/test/run-add_desc.c
new file mode 100644 (file)
index 0000000..ded3f88
--- /dev/null
@@ -0,0 +1,164 @@
+#include <ccan/tap/tap.h>
+#include <ccan/opt/opt.c>
+#include <ccan/opt/usage.c>
+#include <ccan/opt/helpers.c>
+#include <ccan/opt/parse.c>
+
+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 " <arg>".  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 <arg>  0123456789 0\n") == 0);
+       free(ret); len = max = 0;
+
+       /* With added " <arg>".  Name doesn't quite fit. */
+       ret = add_desc(NULL, &len, &max, 12, 25, &opt);
+       ok1(len < max);
+       ret[len] = '\0';
+       ok1(strcmp(ret,
+                  "01234 <arg>\n"
+                  "            0123456789 0\n") == 0);
+       free(ret); len = max = 0;
+
+       /* With added " <arg>".  Desc doesn't quite fit. */
+       ret = add_desc(NULL, &len, &max, 13, 24, &opt);
+       ok1(len < max);
+       ret[len] = '\0';
+       ok1(strcmp(ret,
+                  "01234 <arg>  0123456789\n"
+                  "             0\n") == 0);
+       free(ret); len = max = 0;
+
+       /* Empty description, with <arg> 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 <arg>   (default: XXXXXXXXXX)\n") == 0);
+       free(ret); len = max = 0;
+
+       /* Empty description, with <arg> 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 <arg>  \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 (file)
index 0000000..ae4d5d3
--- /dev/null
@@ -0,0 +1,37 @@
+#include <ccan/tap/tap.h>
+#include <ccan/opt/opt.c>
+#include <ccan/opt/usage.c>
+#include <ccan/opt/helpers.c>
+#include <ccan/opt/parse.c>
+
+/* 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();
+}
index de60ac9ff08e4fbac8e46943fe36e404f2301361..10b241908add534cd0f816457214debe28be1fea 100644 (file)
@@ -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;
index fa1dc64c112fe46dc36e02cc4b140de50681372c..7d94ced219b59868883801cb26bd7f362019e4f7 100644 (file)
@@ -4,6 +4,15 @@
 #include <stdlib.h>
 #include <stdarg.h>
 #include "utils.h"
+
+/* Ensure width is sane. */
+static const char *getenv_override(const char *name)
+{
+       return "100";
+}
+
+#define getenv getenv_override
+
 #include <ccan/opt/opt.c>
 #include <ccan/opt/usage.c>
 #include <ccan/opt/helpers.c>
index 873ee5db1856cf0bf326a8ed4ba9f19aa22c2db6..d9b2ee59cc3e4ba9a10df5a5eb358a74b5b4fb89 100644 (file)
@@ -1,5 +1,6 @@
 /* Licensed under GPLv3+ - see LICENSE file for details */
 #include <ccan/opt/opt.h>
+#include <sys/ioctl.h>
 #include <string.h>
 #include <stdlib.h>
 #include <stdio.h>
 /* 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, " <arg>");
+               off += strlen(" <arg>");
+       }
+
+       /* 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(" <arg>");
+               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(" <arg>");
-                       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, " <arg>");
-               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;
 }