]> git.draconx.ca Git - dxcommon.git/blob - scripts/gen-options.awk
f2cdfe7825ab6f600334a7bb76fbe32408b6f1a4
[dxcommon.git] / scripts / gen-options.awk
1 #!/bin/awk -f
2 #
3 # Copyright © 2021, 2023 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 # If none of the options have action specifications, then an alternate
79 # set of macros is also defined, which encode the struct option array
80 # into a more compact format that can be used to generate the full
81 # 'struct option' array at runtime:
82 #
83 #   * the object-like macro LOPT_PACK_BITS expands to an integer constant
84 #     expression, suitable for use in #if directives, that specifies the
85 #     minimum number of bits required by the encoding.  LOPT_PACK_BITS2
86 #     is the same, but rounded up to the next power of two greater than
87 #     or equal to 8.
88 #
89 #   * the object-like macro LOPTS_PACKED_INITIALIZER expands to a
90 #     comma-separated sequence of integer constant expressions, suitable
91 #     for initializing an array of integers.  All values are less than
92 #     2^LOPT_PACK_BITS.
93 #
94 #   * the function-like macro LOPT_UNPACK(opt, x), where opt is an
95 #     lvalue of type 'struct option', and x is one of the array
96 #     elements initialized by LOPTS_PACKED_INITIALIZER.  This expands
97 #     the encoded value and sets the name, has_arg and val members of
98 #     opt appopriately.  The caller should ensure that the flag member
99 #     is set to zero.
100 #
101 # The help text for an individual struct option element may be obtained by
102 # the function
103 #
104 #   struct lopt_help { const char *desc, *arg; }
105 #   *lopt_get_help(const struct option *opt);
106 #
107 # The returned desc and arg pointers point to the argument name and help text
108 # for the argument, respectively, as written in the options specification file.
109 #
110 # License WTFPL2: Do What The Fuck You Want To Public License, version 2.
111 # This is free software: you are free to do what the fuck you want to.
112 # There is NO WARRANTY, to the extent permitted by law.
113
114 END {
115   print "/*"
116   if (FILENAME) {
117     print " * Automatically generated by gen-options.awk from " FILENAME
118   } else {
119     print " * Automatically generated by gen-options.awk"
120   }
121   print " * Do not edit."
122   print " */"
123 }
124
125 BEGIN {
126   # Check if "\\\\" in substitutions gives just one backslash.
127   bs = "x"; sub(/x/, "\\\\", bs);
128   bs = (length(bs) == 1 ? "\\\\" : "\\");
129
130   has_actions = 0
131   sopt_string = ""
132   num_options = 0
133   lopt = ""
134   err = 0
135 }
136
137 # Parse option specifier lines
138 $0 ~ /^-/ {
139   work = $0
140   arg = lopt = sopt = ""
141   has_arg = 0
142
143   # Extract short option name
144   if (work ~ /^-[^-]/) {
145     sopt = substr(work, 2, 1)
146     sub(/^-.,[ \t]*/, "", work)
147   }
148
149   # Extract long option name
150   if (work ~ /^--/) {
151     if (n = match(work, /[= \t[]/)) {
152       lopt = substr(work, 3, n-3)
153       work = substr(work, n)
154     } else {
155       lopt = substr(work, 3)
156       work = ""
157     }
158   }
159
160   # Extract argument name
161   if (work ~ /^\[=[^ \t]+\]/ && sub(/\]/, "&", work) == 1) {
162     if (n = index(work, "]")) {
163       arg = substr(work, 3, n-3)
164       work = substr(work, n+1)
165     }
166     has_arg = 2
167   } else if (work ~ /^=/) {
168     if (n = match(work, /[ \t]/)) {
169       arg  = substr(work, 2, n-2)
170       work = substr(work, n)
171     } else {
172       arg  = substr(work, 2)
173       work = ""
174     }
175     has_arg = 1
176   }
177
178   # Extract action
179   sub(/^[ \t]*/, "", work)
180   if (!sopt && work ~ /^\([^, \t]+(,[ \t]*[^, \t]+)?\)/) {
181     # packed form is not possible w/ actions
182     has_actions = 1;
183
184     n = split(work, a, /,[ \t]*/)
185     if (n == 2) {
186       flag = substr(a[1], 2) ", " substr(a[2], 1, length(a[2])-1)
187     } else if (n == 1) {
188       flag = "NULL, " substr(a[1], 2, length(a[1])-2)
189     }
190     sub(/^\([^, \t]+(,[ \t]*[^, \t]+)?/, "", work)
191   } else if (sopt) {
192     flag = "NULL, '" sopt "'"
193   } else {
194     flag = "NULL, " to_enum(lopt)
195   }
196
197   if (work) {
198     print "invalid option specification:", $0 > "/dev/stderr"
199     err = 1
200     exit
201   }
202
203   if (sopt) {
204     sopt_string = sopt_string sopt substr("::", 1, has_arg)
205   }
206   options[num_options++] = lopt
207   optionspec[lopt] = has_arg ", " flag
208   if (arg) {
209     optionarg[lopt] = arg
210   }
211
212   next
213 }
214
215 # Ignore any line beginning with a #
216 $0 ~ /^#/ { next }
217
218 lopt {
219   sub(/^[ \t]*/, "")
220   if (!$0) { next }
221
222   if (lopt in optionhelp)
223     $0 = "\n" $0;
224   optionhelp[lopt] = optionhelp[lopt] $0;
225 }
226
227 # Exit immediately on error
228 END { if (err) { exit err } }
229
230 END {
231   print "#include <stddef.h>"
232   print "#include <limits.h>\n"
233   print "#define SOPT_STRING \"" sopt_string "\"\n"
234 }
235
236 # Generate the main options tables
237 END {
238   lopt_strings = ""
239
240   count = bucketsort(sorted_options, options)
241   for (i = 0; i < count; i++) {
242     lopt_strings = add_to_strtab(lopt_strings, sorted_options[i], offsets)
243   }
244   gsub(/[^ ]+/, "\"&", lopt_strings)
245   gsub(/ /, bs"0\"\n\t", lopt_strings)
246
247   print "static const char lopt_strings[] ="
248   print "\t" lopt_strings "\";\n"
249   print "enum {"
250   for (i = 0; i < count; i++) {
251     opt = options[i]
252     sep = (i+1 == count ? "" : ",")
253
254     print "\t" to_enum(opt), "= UCHAR_MAX+1 +", offsets[opt] sep
255   }
256   print "};"
257   print "#define lopt_str(x) (lopt_strings + (LOPT_ ## x - UCHAR_MAX - 1))"
258
259   if (!has_actions) {
260     output_packed_macros()
261   }
262
263   print "\n#define LOPTS_INITIALIZER \\"
264   for (i = 0; i < count; i++) {
265     opt = options[i]
266     sep = (i+1 == count ? "" : ", \\")
267
268     print "\t/* --" opt, "*/ \\"
269     print "\t{ lopt_strings+" offsets[opt] ",", optionspec[opt] " }" sep
270   }
271 }
272
273 # Generate the help strings
274 END {
275   # First, sort out the argument names
276   arg_strings = ""
277
278   count = bucketsort(sorted_args, optionarg)
279   for (i = 0; i < count; i++) {
280     arg_strings = add_to_strtab(arg_strings, sorted_args[i], arg_offsets)
281   }
282
283   n = split(arg_strings, arg_split)
284   arg_strings = ""
285   for (i = 1; i <= n; i++) {
286     for (opt in optionarg) {
287       if (optionarg[opt] == arg_split[i]) {
288         l10narg[opt] = 1
289         break;
290       }
291     }
292
293     sep = (i < n ? "\"\\0\"" : "")
294     arg_strings = arg_strings "\n\tPN_(\"" opt "\", \"" arg_split[i] "\")" sep
295   }
296
297   print "\n#define ARG_L10N_(x)"
298   print "#ifndef PN_"
299   print "#  define PN_(c, x) x"
300   print "#endif\n"
301
302   print "static const char arg_strings[] = " arg_strings "\"\";"
303   for (opt in optionarg) {
304     if (opt in l10narg) {
305       continue
306     }
307     print "\tARG_L10N_(PN_(\"" opt "\", \"" optionarg[opt] "\"))"
308   }
309
310   # Then add in the actual descriptions
311   print "\nstatic const char help_strings[] ="
312   help = ""
313   help_pos = 0
314   for (opt in options) {
315     opt = options[opt]
316     if (opt in optionhelp) {
317       if (help) {
318         print help "\"\\0\""
319       }
320
321       help = optionhelp[opt]
322       help_offsets[opt] = help_pos
323       help_pos += length(help) + 1
324
325       gsub(/"/, bs"\"", help)
326       gsub(/\n/, bs"n\"\n\t    \"", help)
327       help = "\tPN_(\"" opt "\",\n\t    \"" help "\")"
328     }
329   }
330   print help "\"\";"
331   for (opt in options) {
332     opt = options[opt]
333     if (!(opt in optionhelp)) {
334       print "\tARG_L10N_(PN_(\"" opt "\", \"\"))"
335       help_offsets[opt] = help_pos - 1
336     }
337   }
338
339   print "\nstatic struct lopt_help { const char *desc, *arg; }"
340   print "*lopt_get_help(const struct option *opt, struct lopt_help *out)\n{"
341   print "\tswitch ((opt->name - lopt_strings) + UCHAR_MAX + 1) {"
342   for (opt in options) {
343     opt = options[opt]
344     print "\tcase", to_enum(opt) ":"
345     print "\t\tout->desc = help_strings +", help_offsets[opt] ";"
346     if (opt in optionarg) {
347       print "\t\tout->arg = arg_strings +", arg_offsets[optionarg[opt]] ";"
348     }
349     print "\t\treturn out;"
350   }
351   print "\t}\n\n\treturn NULL;"
352   print "}"
353 }
354
355 # Emit the packed initializer macros.  This is used as an array initializer
356 # that encodes the following information:
357 #
358 #   - short option character offset
359 #   - arg value (0, 1 or 2), and
360 #   - long option string offset
361 #
362 # as a single integer value for each option, in as few bits as practical.
363 #
364 # Currently, this only works if none of the options use action specifications
365 # (as these would require encoding user-specified pointer expressions and
366 # arbitrary int values).
367 function output_packed_macros(i, tmp, accum, max, totalbits)
368 {
369   print "";
370
371   # determine number of bits to encode offsets in SOPT_STRING
372   max = length(sopt_string);
373   totalbits = accum = 0;
374   for (i = 1; i <= max; i *= 2) {
375     accum++;
376   }
377   print "#define LOPT_SC_BITS " accum;
378   totalbits += accum;
379
380   # determine number of bits to encode has_arg values
381   max = 0;
382   for (i in optionspec) {
383     tmp = optionspec[i]; sub(/,.*/, "", tmp);
384     if (tmp > max)
385       max = tmp;
386   }
387   accum = (max > 1 ? 2 : max > 0 ? 1 : 0);
388   print "#define LOPT_HA_BITS " accum;
389   totalbits += accum;
390
391   # determine number of bits to encode offsets in lopt_strings
392   max = 0;
393   for (i in offsets) {
394     if (offsets[i] > max)
395       max = offsets[i];
396   }
397
398   accum = 0;
399   for (i = 1; i <= max; i *= 2) {
400     accum++;
401   }
402   print "#define LOPT_LS_BITS " accum;
403   totalbits += accum;
404
405   print "#define LOPT_PACK_BITS " totalbits;
406   for (i = 8; i < totalbits; i *= 2)
407     ;
408   print "#define LOPT_PACK_BITS2 " i;
409
410   # Now emit the packed initializer macro
411   print "\n#define LOPTS_PACKED_INITIALIZER \\";
412   accum = "";
413   for (i = 0; i < count; i++) {
414     if (accum)
415       print "\t" accum ", \\";
416
417     tmp = options[i];
418     accum = "("offsets[tmp] "ul" "<<LOPT_HA_BITS)";
419     max = tmp = optionspec[tmp];
420     sub(/,.*/, "", max)
421     accum = "((" accum "|" max ")<<LOPT_SC_BITS)";
422
423     sub(/.*[, ]/, "", tmp);
424     if (tmp ~ /^[']/) {
425       tmp = index(sopt_string, substr(tmp, 2, 1)) - 1;
426     } else {
427       tmp = length(sopt_string);
428     }
429     accum = accum "|" tmp;
430   }
431
432   if (accum)
433     print "\t" accum;
434
435   # Finally, the unpack helper macros
436   tmp = "(x) & ((1ul<<LOPT_SC_BITS)-1)";
437   print "\n#define LOPT_UNPACK_VAL(x) \\"
438   print "\t( SOPT_STRING[" tmp "] \\";
439   print "\t? SOPT_STRING[" tmp "] \\";
440   print "\t: 1u + UCHAR_MAX + ((x)>>(LOPT_SC_BITS+LOPT_HA_BITS)))";
441
442   print "\n#define LOPT_UNPACK_ARG(x) \\";
443   print "\t(((x)>>LOPT_SC_BITS)&((1ul<<LOPT_HA_BITS)-1))";
444
445   print "\n#define LOPT_UNPACK_NAME(x) \\"
446   print "\t(lopt_strings+((x)>>(LOPT_SC_BITS+LOPT_HA_BITS)))";
447
448   print "\n#define LOPT_UNPACK(opt, x) do { \\";
449   print "\t(opt).name = LOPT_UNPACK_NAME(x); \\"
450   print "\t(opt).has_arg = LOPT_UNPACK_ARG(x); \\"
451   print "\t(opt).val = LOPT_UNPACK_VAL(x); \\"
452   print "} while (0)";
453 }
454
455 # bucketsort(dst, src)
456 #
457 # Sort the elements of src by descending string length,
458 # placing them into dst[0] ... dst[n].
459 #
460 # Returns the number of elements.
461 function bucketsort(dst, src, max, count, i, t)
462 {
463   # Note: ULTRIX 4.5 nawk does not support local array parameters
464   split("", bucketsort_buckets);
465
466   for (t in src) {
467     i = length(src[t])
468     if (i > max) { max = i }
469     bucketsort_buckets[i]++
470   }
471
472   for (i = max; i > 0; i--) {
473     if (i in bucketsort_buckets) {
474       t = bucketsort_buckets[i]
475       bucketsort_buckets[i] = count
476       count += t
477     }
478   }
479
480   for (t in src) {
481     i = length(t = src[t])
482     dst[bucketsort_buckets[i]++] = t
483   }
484
485   return count
486 }
487
488 # to_enum(lopt)
489 #
490 # Return the string LOPT_xxx, where xxx is the argument with all lowercase
491 # letters converted to uppercase, and all non-alphanumeric characters replaced
492 # with underscores.
493 function to_enum(lopt)
494 {
495   lopt = toupper(lopt)
496   gsub(/[^ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789]/, "_", lopt)
497   return "LOPT_" lopt
498 }
499
500 # add_to_strtab(strtab, str, offsets)
501 #
502 # Append string to strtab if there is not already a matching string present
503 # in the table.  Newly-added strings are separated by spaces, which must be
504 # translated into null bytes afterwards.  The updated strtab is returned, and
505 # the offsets[str] array member is updated with the position (counting from 0)
506 # of str in the strtab.
507 #
508 # For optimal results, strings should be added in descending length order.
509 function add_to_strtab(strtab, str, offsets, pos)
510 {
511   if ( (pos = index(strtab, str " ") - 1) < 0) {
512     pos = length(strtab)
513     if (pos) {
514       strtab = strtab " " str
515       pos++
516     } else {
517       strtab = strtab str
518     }
519   }
520
521   offsets[str] = pos
522   return strtab
523 }