/* Licensed under GPLv3+ - see LICENSE file for details */
#include <ccan/opt/opt.h>
+#include <sys/ioctl.h>
+#include <sys/termios.h> /* Required on Solaris for struct winsize */
+#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;
+ 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++) {
}
}
}
-
- /* 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;
}