]> git.ozlabs.org Git - ccan/blob - ccan/opt/usage.c
opt: much prettier usage (using terminal size)
[ccan] / ccan / opt / usage.c
1 /* Licensed under GPLv3+ - see LICENSE file for details */
2 #include <ccan/opt/opt.h>
3 #include <sys/ioctl.h>
4 #include <string.h>
5 #include <stdlib.h>
6 #include <stdio.h>
7 #include <stdint.h>
8 #include "private.h"
9
10 /* We only use this for pointer comparisons. */
11 const char opt_hidden[1];
12
13 #define MIN_DESC_WIDTH 40
14 #define MIN_TOTAL_WIDTH 50
15
16 static unsigned int get_columns(void)
17 {
18         struct winsize w;
19         const char *env = getenv("COLUMNS");
20
21         w.ws_col = 0;
22         if (env)
23                 w.ws_col = atoi(env);
24         if (!w.ws_col)
25                 if (ioctl(0, TIOCGWINSZ, &w) == -1)
26                         w.ws_col = 0;
27         if (!w.ws_col)
28                 w.ws_col = 80;
29
30         return w.ws_col;
31 }
32
33 /* Return number of chars of words to put on this line.
34  * Prefix is set to number to skip at start, maxlen is max width, returns
35  * length (after prefix) to put on this line. */
36 static size_t consume_words(const char *words, size_t maxlen, size_t *prefix)
37 {
38         size_t oldlen, len;
39
40         /* Swallow leading whitespace. */
41         *prefix = strspn(words, " ");
42         words += *prefix;
43
44         /* Use at least one word, even if it takes us over maxlen. */
45         oldlen = len = strcspn(words, " ");
46         while (len <= maxlen) {
47                 oldlen = len;
48                 len += strspn(words+len, " ");
49                 len += strcspn(words+len, " ");
50                 if (len == oldlen)
51                         break;
52         }
53
54         return oldlen;
55 }
56
57 static char *add_str_len(char *base, size_t *len, size_t *max,
58                          const char *str, size_t slen)
59 {
60         if (slen >= *max - *len)
61                 base = realloc(base, *max = (*max * 2 + slen + 1));
62         memcpy(base + *len, str, slen);
63         *len += slen;
64         return base;
65 }
66
67 static char *add_str(char *base, size_t *len, size_t *max, const char *str)
68 {
69         return add_str_len(base, len, max, str, strlen(str));
70 }
71
72 static char *add_indent(char *base, size_t *len, size_t *max, size_t indent)
73 {
74         if (indent >= *max - *len)
75                 base = realloc(base, *max = (*max * 2 + indent + 1));
76         memset(base + *len, ' ', indent);
77         *len += indent;
78         return base;
79 }
80
81 static char *add_desc(char *base, size_t *len, size_t *max,
82                       unsigned int indent, unsigned int width,
83                       const struct opt_table *opt)
84 {
85         size_t off, prefix, l;
86         const char *p;
87         bool same_line = false;
88
89         base = add_str(base, len, max, opt->names);
90         off = strlen(opt->names);
91         if (opt->type == OPT_HASARG
92             && !strchr(opt->names, ' ')
93             && !strchr(opt->names, '=')) {
94                 base = add_str(base, len, max, " <arg>");
95                 off += strlen(" <arg>");
96         }
97
98         /* Do we start description on next line? */
99         if (off + 2 > indent) {
100                 base = add_str(base, len, max, "\n");
101                 off = 0;
102         } else {
103                 base = add_indent(base, len, max, indent - off);
104                 off = indent;
105                 same_line = true;
106         }
107
108         /* Indent description. */
109         p = opt->desc;
110         while ((l = consume_words(p, width - indent, &prefix)) != 0) {
111                 if (!same_line)
112                         base = add_indent(base, len, max, indent);
113                 p += prefix;
114                 base = add_str_len(base, len, max, p, l);
115                 base = add_str(base, len, max, "\n");
116                 off = indent + l;
117                 p += l;
118                 same_line = false;
119         }
120
121         /* Empty description?  Make it match normal case. */
122         if (same_line)
123                 base = add_str(base, len, max, "\n");
124
125         if (opt->show) {
126                 char buf[OPT_SHOW_LEN + sizeof("...")];
127                 strcpy(buf + OPT_SHOW_LEN, "...");
128                 opt->show(buf, opt->u.arg);
129
130                 /* If it doesn't fit on this line, indent. */
131                 if (off + strlen(" (default: ") + strlen(buf) + strlen(")")
132                     > width) {
133                         base = add_indent(base, len, max, indent);
134                 } else {
135                         /* Remove \n. */
136                         (*len)--;
137                 }
138
139                 base = add_str(base, len, max, " (default: ");
140                 base = add_str(base, len, max, buf);
141                 base = add_str(base, len, max, ")\n");
142         }
143         return base;
144 }
145
146 char *opt_usage(const char *argv0, const char *extra)
147 {
148         unsigned int i;
149         size_t max, len, width, indent;
150         char *ret;
151
152         width = get_columns();
153         if (width < MIN_TOTAL_WIDTH)
154                 width = MIN_TOTAL_WIDTH;
155
156         /* Figure out longest option. */
157         indent = 0;
158         for (i = 0; i < opt_count; i++) {
159                 size_t l;
160                 if (opt_table[i].desc == opt_hidden)
161                         continue;
162                 if (opt_table[i].type == OPT_SUBTABLE)
163                         continue;
164                 l = strlen(opt_table[i].names);
165                 if (opt_table[i].type == OPT_HASARG
166                     && !strchr(opt_table[i].names, ' ')
167                     && !strchr(opt_table[i].names, '='))
168                         l += strlen(" <arg>");
169                 if (l + 2 > indent)
170                         indent = l + 2;
171         }
172
173         /* Now we know how much to indent */
174         if (indent + MIN_DESC_WIDTH > width)
175                 indent = width - MIN_DESC_WIDTH;
176
177         len = max = 0;
178         ret = NULL;
179
180         ret = add_str(ret, &len, &max, "Usage: ");
181         ret = add_str(ret, &len, &max, argv0);
182
183         /* Find usage message from among registered options if necessary. */
184         if (!extra) {
185                 extra = "";
186                 for (i = 0; i < opt_count; i++) {
187                         if (opt_table[i].cb == (void *)opt_usage_and_exit
188                             && opt_table[i].u.carg) {
189                                 extra = opt_table[i].u.carg;
190                                 break;
191                         }
192                 }
193         }
194         ret = add_str(ret, &len, &max, " ");
195         ret = add_str(ret, &len, &max, extra);
196         ret = add_str(ret, &len, &max, "\n");
197
198         for (i = 0; i < opt_count; i++) {
199                 if (opt_table[i].desc == opt_hidden)
200                         continue;
201                 if (opt_table[i].type == OPT_SUBTABLE) {
202                         ret = add_str(ret, &len, &max, opt_table[i].desc);
203                         ret = add_str(ret, &len, &max, ":\n");
204                         continue;
205                 }
206                 ret = add_desc(ret, &len, &max, indent, width, &opt_table[i]);
207         }
208         ret[len] = '\0';
209         return ret;
210 }