]> git.draconx.ca Git - dxcommon.git/blob - scripts/gen-options.awk
Add a script for generating getopt_long option tables.
[dxcommon.git] / scripts / gen-options.awk
1 #!/bin/awk -f
2 #
3 # Copyright © 2021 Nick Bowler
4 #
5 # Generate definitions helpful when using getopt_long from an options
6 # specification file.
7 #
8 # The options specification file is processed line by line.  Any line
9 # beginning with a - character introduces a new option definition.  Each
10 # option definition specifies any or all of a short option name, a long
11 # option name, an argument specification, and an action specification.
12 #
13 # Only the long option name is mandatory.  It is not possible to define
14 # short options without a corresponding long option.
15 #
16 # The optional short option name is first, and consists of a hyphen (which
17 # must be the first character on the line) followed by the one character
18 # short option name, followed by a comma.
19 #
20 # The long option name is next on the line, which consists of two hyphens
21 # followed by the desired option name.  If the short option name was omitted,
22 # then the first hyphen of the long option name must be the first character
23 # on the line.
24 #
25 # The argument specification is next, consisting of an equals sign followed by
26 # the argument name.  The argument name can be any sequence of non-whitespace
27 # characters and only relevant for --help text.
28 #
29 # If the argument specification is surrounded by square brackets, this
30 # indicates an optional argument.  If the argument specification is omitted
31 # completely, this option has no argument.  Otherwise, the option has a
32 # mandatory argument.
33 #
34 # Finally, the optional action specification defines how the "flag" and
35 # "val" members are set in the option structure for this option.  An action
36 # specification may only be provided for options with no short name.
37 #
38 # If the action specification is omitted, then flag will be set to a null
39 # pointer and val is set to the short option character, if any, otherwise the
40 # unique enumeration constant LOPT_xxx for this option (described below).
41 #
42 # The action specification can be of the form (val) or (flag, val), where flag
43 # and val are C expressions suitable for use in an initializer for objects
44 # with static storage duration.  Neither flag nor val may contain commas or
45 # whitespace.  In the first form, the option's flag is set to a null pointer.
46 #
47 # Any amount of whitespace may follow the short option name, the argument
48 # specification, the action specification, or the comma within an action
49 # specification.  Whitespace is not permitted between a long option name
50 # and a flag specification.
51 #
52 # Examples of option specifications:
53 #
54 #   -h, --help
55 #   --do-nothing (0)
56 #   -o, --output=FILE
57 #   --pad[=VAL]
58 #   --parse-only (&parse_only, 1)
59 #
60 # Each option is assigned an enumeration constant of the form LOPT_xxx,
61 # where xxx is the long option name with all letters in uppercase and
62 # all non-alphanumeric characters replaced with underscores.  The value
63 # of the constants is unspecified, except that they will be unique across
64 # all defined options and distinct from the integer value of any short
65 # option character.
66 #
67 # The object-like macro SOPT_STRING expands to a string literal suitable
68 # for use as the optstring argument to getopt et al.
69 #
70 # The object-like macro LOPTS_INITIALIZER expands to a comma-separated
71 # sequence of struct option initializers, suitable for use in a declaration
72 # of an array of struct option elements with static storage duration.  The
73 # all-zero terminating element required by getopt_long must be added by the
74 # user.  For example:
75 #
76 #   static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
77 #
78 # The help text for an individual struct option element may be obtained by
79 # the function
80 #
81 #   struct lopt_help { const char *desc, *arg; }
82 #   *lopt_get_help(const struct option *opt);
83 #
84 # The returned desc and arg pointers point to the argument name and help text
85 # for the argument, respectively, as written in the options specification file.
86 #
87 # License WTFPL2: Do What The Fuck You Want To Public License, version 2.
88 # This is free software: you are free to do what the fuck you want to.
89 # There is NO WARRANTY, to the extent permitted by law.
90
91 END {
92   print "/*"
93   if (FILENAME) {
94     print " * Automatically generated by gen-options.awk from " FILENAME
95   } else {
96     print " * Automatically generated by gen-options.awk"
97   }
98   print " * Do not edit."
99   print " */"
100 }
101
102 BEGIN {
103   sopt_string = ""
104   num_options = 0
105   lopt = ""
106   err = 0
107 }
108
109 # Parse option specifier lines
110 $0 ~ /^-/ {
111   work = $0
112   arg = lopt = sopt = ""
113   has_arg = 0
114
115   # Extract short option name
116   if (work ~ /^-[^-]/) {
117     sopt = substr(work, 2, 1)
118     sub(/^-.,[ \t]*/, "", work)
119   }
120
121   # Extract long option name
122   if (work ~ /^--/) {
123     if (n = match(work, /[= \t[]/)) {
124       lopt = substr(work, 3, n-3)
125       work = substr(work, n)
126     } else {
127       lopt = substr(work, 3)
128       work = ""
129     }
130   }
131
132   # Extract argument name
133   if (work ~ /^\[=[^\] \t]+\]/) {
134     if (n = index(work, "]")) {
135       arg = substr(work, 3, n-3)
136       work = substr(work, n+1)
137     }
138     has_arg = 2
139   } else if (work ~ /^=/) {
140     if (n = match(work, /[ \t]/)) {
141       arg  = substr(work, 2, n-2)
142       work = substr(work, n)
143     } else {
144       arg  = substr(work, 2)
145       work = ""
146     }
147     has_arg = 1
148   }
149
150   # Extract action
151   sub(/^[ \t]*/, "", work)
152   if (!sopt && work ~ /^\([^, \t]+(,[ \t]*[^, \t]+)?\)/) {
153     n = split(work, a, /,[ \t]*/)
154     if (n == 2) {
155       flag = substr(a[1], 2) ", " substr(a[2], 1, length(a[2])-1)
156     } else if (n == 1) {
157       flag = "NULL, " substr(a[1], 2, length(a[1])-2)
158     }
159     sub(/^\([^, \t]+(,[ \t]*[^, \t]+)?/, "", work)
160   } else if (sopt) {
161     flag = "NULL, '" sopt "'"
162   } else {
163     flag = "NULL, " to_enum(lopt)
164   }
165
166   if (work) {
167     print "invalid option specification:", $0 > "/dev/stderr"
168     err = 1
169     exit
170   }
171
172   if (sopt) {
173     sopt_string = sopt_string sopt substr("::", 1, has_arg)
174   }
175   options[num_options++] = lopt
176   optionspec[lopt] = has_arg ", " flag
177   if (arg) {
178     optionarg[lopt] = arg
179   }
180
181   next
182 }
183
184 # Ignore any line beginning with a #
185 $0 ~ /^#/ { next }
186
187 lopt {
188   sub(/^[ \t]*/, "")
189   if (!$0) { next }
190
191   optionhelp[lopt] = (lopt in optionhelp ? optionhelp[lopt] "\n" : "") $0
192 }
193
194 # Exit immediately on error
195 END { if (err) { exit err } }
196
197 END {
198   print "#include <stddef.h>"
199   print "#include <limits.h>\n"
200   print "#define SOPT_STRING \"" sopt_string "\"\n"
201 }
202
203 # Generate the main options tables
204 END {
205   lopt_strings = ""
206
207   count = bucketsort(sorted_options, options)
208   for (i = 0; i < count; i++) {
209     lopt_strings = add_to_strtab(lopt_strings, sorted_options[i], offsets)
210   }
211   gsub(/[^ ]+/, "\"&", lopt_strings)
212   gsub(/ /, "\\0\"\n\t", lopt_strings)
213
214   print "static const char lopt_strings[] ="
215   print "\t" lopt_strings "\";\n"
216   print "enum {"
217   for (i = 0; i < count; i++) {
218     opt = options[i]
219     sep = (i+1 == count ? "" : ",")
220
221     print "\t" to_enum(opt), "= UCHAR_MAX+1 +", offsets[opt] sep
222   }
223   print "};"
224   print "#define lopt_str(x) (lopt_strings + (LOPT_ ## x - UCHAR_MAX - 1))\n"
225
226   print "#define LOPTS_INITIALIZER \\"
227   for (i = 0; i < count; i++) {
228     opt = options[i]
229     sep = (i+1 == count ? "" : ", \\")
230
231     print "\t/* --" opt, "*/ \\"
232     print "\t{ lopt_strings+" offsets[opt] ",", optionspec[opt] " }" sep
233   }
234 }
235
236 # Generate the help strings
237 END {
238   # First, sort out the argument names
239   arg_strings = ""
240
241   count = bucketsort(sorted_args, optionarg)
242   for (i = 0; i < count; i++) {
243     arg_strings = add_to_strtab(arg_strings, sorted_args[i], arg_offsets)
244   }
245
246   n = split(arg_strings, arg_split)
247   arg_strings = ""
248   for (i = 1; i <= n; i++) {
249     for (opt in optionarg) {
250       if (optionarg[opt] == arg_split[i]) {
251         l10narg[opt] = 1
252         break;
253       }
254     }
255
256     sep = (i < n ? "\"\\0\"" : "")
257     arg_strings = arg_strings "\n\tPN_(\"" opt "\", \"" arg_split[i] "\")" sep
258   }
259
260   print "\n#define ARG_L10N_(x)"
261   print "#ifndef PN_"
262   print "#  define PN_(c, x) x"
263   print "#endif\n"
264
265   print "static const char arg_strings[] = " arg_strings "\"\";"
266   for (opt in optionarg) {
267     if (opt in l10narg) {
268       continue
269     }
270     print "\tARG_L10N_(PN_(\"" opt "\", \"" optionarg[opt] "\"))"
271   }
272
273   # Then add in the actual descriptions
274   print "\nstatic const char help_strings[] ="
275   help = ""
276   help_pos = 0
277   for (opt in options) {
278     opt = options[opt]
279     if (opt in optionhelp) {
280       if (help) {
281         print help "\"\\0\""
282       }
283
284       help = optionhelp[opt]
285       help_offsets[opt] = help_pos
286       help_pos += length(help) + 1
287
288       gsub(/"/, "\\\"", help)
289       gsub(/\n/, "\\n\"\n\t    \"", help)
290       help = "\tPN_(\"" opt "\",\n\t    \"" help "\")"
291     }
292   }
293   print help "\"\";"
294   for (opt in options) {
295     opt = options[opt]
296     if (!(opt in optionhelp)) {
297       print "\tARG_L10N_(PN_(\"" opt "\", \"\"))"
298       help_offsets[opt] = help_pos
299     }
300   }
301
302   print "\nstatic struct lopt_help { const char *desc, *arg; }"
303   print "*lopt_get_help(const struct option *opt, struct lopt_help *out)\n{"
304   print "\tswitch ((opt->name - lopt_strings) + UCHAR_MAX + 1) {"
305   for (opt in options) {
306     opt = options[opt]
307     print "\tcase", to_enum(opt) ":"
308     print "\t\tout->desc = help_strings +", help_offsets[opt] ";"
309     if (opt in optionarg) {
310       print "\t\tout->arg = arg_strings +", arg_offsets[optionarg[opt]] ";"
311     }
312     print "\t\treturn out;"
313   }
314   print "\t}\n\n\treturn NULL;"
315   print "}"
316 }
317
318 # bucketsort(dst, src)
319 #
320 # Sort the elements of src by descending string length,
321 # placing them into dst[0] ... dst[n].
322 #
323 # Returns the number of elements.
324 function bucketsort(dst, src, buckets, max, count, i, t)
325 {
326   for (t in src) {
327     i = length(src[t])
328     if (i > max) { max = i }
329     buckets[i]++
330   }
331
332   for (i = max; i > 0; i--) {
333     if (i in buckets) {
334       t = buckets[i]
335       buckets[i] = count
336       count += t
337     }
338   }
339
340   for (t in src) {
341     i = length(t = src[t])
342     dst[buckets[i]++] = t
343   }
344
345   return count
346 }
347
348 # to_enum(lopt)
349 #
350 # Return the string LOPT_xxx, where xxx is the argument with all lowercase
351 # letters converted to uppercase, and all non-alphanumeric characters replaced
352 # with underscores.
353 function to_enum(lopt)
354 {
355   lopt = toupper(lopt)
356   gsub(/[^ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789]/, "_", lopt)
357   return "LOPT_" lopt
358 }
359
360 # add_to_strtab(strtab, str, offsets)
361 #
362 # Append string to strtab if there is not already a matching string present
363 # in the table.  Newly-added strings are separated by spaces, which must be
364 # translated into null bytes afterwards.  The updated strtab is returned, and
365 # the offsets[str] array member is updated with the position (counting from 0)
366 # of str in the strtab.
367 #
368 # For optimal results, strings should be added in descending length order.
369 function add_to_strtab(strtab, str, offsets, pos)
370 {
371     if ( (pos = index(strtab, str " ") - 1) < 0) {
372       pos = length(strtab)
373       if (pos) {
374         strtab = strtab " " str
375         pos++
376       } else {
377         strtab = strtab str
378       }
379     }
380     offsets[str] = pos
381     return strtab
382 }