Browse Source

opt: much prettier usage (using terminal size)

Rusty Russell 14 years ago
parent
commit
3d45cf27cf

+ 164 - 0
ccan/opt/test/run-add_desc.c

@@ -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();
+}

+ 37 - 0
ccan/opt/test/run-consume_words.c

@@ -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();
+}

+ 1 - 1
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;

+ 9 - 0
ccan/opt/test/run-usage.c

@@ -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>

+ 170 - 69
ccan/opt/usage.c

@@ -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>
@@ -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, " <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;
 }