opt: test HAVE_SYS_TERMIOS_H via #if
[ccan] / ccan / opt / usage.c
index f321ed2949a8bfb7cd5cdbce5955780dba1cd233..ff0545591ed4050a5153aa5c8d6db2041a1295e4 100644 (file)
@@ -1,5 +1,10 @@
 /* Licensed under GPLv3+ - see LICENSE file for details */
 #include <ccan/opt/opt.h>
+#if HAVE_SYS_TERMIOS_H
+#include <sys/ioctl.h>
+#include <sys/termios.h> /* Required on Solaris for struct winsize */
+#endif
+#include <sys/unistd.h> /* Required on Solaris for ioctl */
 #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;
+       int ws_col = 0;
+       const char *env = getenv("COLUMNS");
+
+       if (env)
+               ws_col = atoi(env);
+
+#ifdef TIOCGWINSZ
+       if (!ws_col)
+       {
+               struct winsize w;
+               if (ioctl(0, TIOCGWINSZ, &w) != -1)
+                       ws_col = w.ws_col;
+       }
+#endif
+       if (!ws_col)
+               ws_col = 80;
+
+       return 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 = opt_alloc.realloc(base, *max = (*max * 2 + slen + 1));
+       memcpy(base + *len, str, slen);
+       *len += slen;
+       return base;
+}
+
+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 = opt_alloc.realloc(base, *max = (*max * 2 + indent + 1));
+       memset(base + *len, ' ', indent);
+       *len += indent;
+       return base;
 }
 
-#define OPT_SPACE_PAD "                    "
+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,74 +200,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);
-       if (!ret)
-               return NULL;
-
-       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;
 }