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