Commit 3d45cf27 authored by Rusty Russell's avatar Rusty Russell

opt: much prettier usage (using terminal size)

parent fba46ae0
#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();
}
#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();
}
...@@ -1024,7 +1024,7 @@ int main(int argc, char *argv[]) ...@@ -1024,7 +1024,7 @@ int main(int argc, char *argv[])
} }
ok1(strstr(output, "[args]")); ok1(strstr(output, "[args]"));
ok1(strstr(output, argv[0])); ok1(strstr(output, argv[0]));
ok1(strstr(output, "[-a]")); ok1(strstr(output, "\n-a"));
free(output); free(output);
free(argv); free(argv);
output = NULL; output = NULL;
......
...@@ -4,6 +4,15 @@ ...@@ -4,6 +4,15 @@
#include <stdlib.h> #include <stdlib.h>
#include <stdarg.h> #include <stdarg.h>
#include "utils.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/opt.c>
#include <ccan/opt/usage.c> #include <ccan/opt/usage.c>
#include <ccan/opt/helpers.c> #include <ccan/opt/helpers.c>
......
/* Licensed under GPLv3+ - see LICENSE file for details */ /* Licensed under GPLv3+ - see LICENSE file for details */
#include <ccan/opt/opt.h> #include <ccan/opt/opt.h>
#include <sys/ioctl.h>
#include <string.h> #include <string.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
...@@ -9,26 +10,177 @@ ...@@ -9,26 +10,177 @@
/* We only use this for pointer comparisons. */ /* We only use this for pointer comparisons. */
const char opt_hidden[1]; 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; struct winsize w;
const char *p; 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)) { /* Use at least one word, even if it takes us over maxlen. */
if (opt_table[i].desc != opt_hidden) oldlen = len = strcspn(words, " ");
str[num++] = *p; 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) char *opt_usage(const char *argv0, const char *extra)
{ {
unsigned int i, num, len; unsigned int i;
char *ret, *p; 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) { if (!extra) {
extra = ""; extra = "";
for (i = 0; i < opt_count; i++) { for (i = 0; i < opt_count; i++) {
...@@ -39,71 +191,20 @@ char *opt_usage(const char *argv0, const char *extra) ...@@ -39,71 +191,20 @@ char *opt_usage(const char *argv0, const char *extra)
} }
} }
} }
ret = add_str(ret, &len, &max, " ");
/* An overestimate of our length. */ ret = add_str(ret, &len, &max, extra);
len = strlen("Usage: %s ") + strlen(argv0) ret = add_str(ret, &len, &max, "\n");
+ 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");
for (i = 0; i < opt_count; i++) { for (i = 0; i < opt_count; i++) {
if (opt_table[i].desc == opt_hidden) if (opt_table[i].desc == opt_hidden)
continue; continue;
if (opt_table[i].type == OPT_SUBTABLE) { 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; continue;
} }
len = sprintf(p, "%s", opt_table[i].names); ret = add_desc(ret, &len, &max, indent, width, &opt_table[i]);
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");
} }
*p = '\0'; ret[len] = '\0';
return ret; return ret;
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment