]> git.draconx.ca Git - dxcommon.git/commitdiff
Add a script for generating getopt_long option tables.
authorNick Bowler <nbowler@draconx.ca>
Fri, 26 Feb 2021 04:58:15 +0000 (23:58 -0500)
committerNick Bowler <nbowler@draconx.ca>
Fri, 26 Feb 2021 05:19:40 +0000 (00:19 -0500)
Maintaining tables of long options, short options, help text and whatnot
in C code is a little bit tedious, and the simpler ways tend to cause
annoying results on modern systems that build everything in PIC mode.

So let's automate the process of building these tables from a simple
description file format, using big string arrays to reduce the amount
of relocations needed in PIC code.

atlocal.in
configure.ac
scripts/gen-options.awk [new file with mode: 0755]
tests/scripts.at [new file with mode: 0644]
testsuite.at

index d15e74d689b7af6aa1643eba69d8033023fc9b8b..9fd159e8bfc46ad4151420ec449b0b4ce7a457f8 100644 (file)
@@ -1 +1,3 @@
+: "${AWK=@AWK@}"
 : "${CC=@CC@}"
+: "${EXEEXT=@EXEEXT@}"
index 77a22c1a9cad1bda95c6b0ca5163393c79807f22..6150d4c565332a8b86f1db37d536fec08ba94317 100644 (file)
@@ -13,6 +13,7 @@ AC_CANONICAL_HOST
 
 AC_PROG_CC
 AC_PROG_RANLIB
+AC_PROG_AWK
 
 AM_INIT_AUTOMAKE([-Wall -Wno-portability foreign subdir-objects])
 AM_SILENT_RULES([yes])
diff --git a/scripts/gen-options.awk b/scripts/gen-options.awk
new file mode 100755 (executable)
index 0000000..30b8202
--- /dev/null
@@ -0,0 +1,382 @@
+#!/bin/awk -f
+#
+# Copyright © 2021 Nick Bowler
+#
+# Generate definitions helpful when using getopt_long from an options
+# specification file.
+#
+# The options specification file is processed line by line.  Any line
+# beginning with a - character introduces a new option definition.  Each
+# option definition specifies any or all of a short option name, a long
+# option name, an argument specification, and an action specification.
+#
+# Only the long option name is mandatory.  It is not possible to define
+# short options without a corresponding long option.
+#
+# The optional short option name is first, and consists of a hyphen (which
+# must be the first character on the line) followed by the one character
+# short option name, followed by a comma.
+#
+# The long option name is next on the line, which consists of two hyphens
+# followed by the desired option name.  If the short option name was omitted,
+# then the first hyphen of the long option name must be the first character
+# on the line.
+#
+# The argument specification is next, consisting of an equals sign followed by
+# the argument name.  The argument name can be any sequence of non-whitespace
+# characters and only relevant for --help text.
+#
+# If the argument specification is surrounded by square brackets, this
+# indicates an optional argument.  If the argument specification is omitted
+# completely, this option has no argument.  Otherwise, the option has a
+# mandatory argument.
+#
+# Finally, the optional action specification defines how the "flag" and
+# "val" members are set in the option structure for this option.  An action
+# specification may only be provided for options with no short name.
+#
+# If the action specification is omitted, then flag will be set to a null
+# pointer and val is set to the short option character, if any, otherwise the
+# unique enumeration constant LOPT_xxx for this option (described below).
+#
+# The action specification can be of the form (val) or (flag, val), where flag
+# and val are C expressions suitable for use in an initializer for objects
+# with static storage duration.  Neither flag nor val may contain commas or
+# whitespace.  In the first form, the option's flag is set to a null pointer.
+#
+# Any amount of whitespace may follow the short option name, the argument
+# specification, the action specification, or the comma within an action
+# specification.  Whitespace is not permitted between a long option name
+# and a flag specification.
+#
+# Examples of option specifications:
+#
+#   -h, --help
+#   --do-nothing (0)
+#   -o, --output=FILE
+#   --pad[=VAL]
+#   --parse-only (&parse_only, 1)
+#
+# Each option is assigned an enumeration constant of the form LOPT_xxx,
+# where xxx is the long option name with all letters in uppercase and
+# all non-alphanumeric characters replaced with underscores.  The value
+# of the constants is unspecified, except that they will be unique across
+# all defined options and distinct from the integer value of any short
+# option character.
+#
+# The object-like macro SOPT_STRING expands to a string literal suitable
+# for use as the optstring argument to getopt et al.
+#
+# The object-like macro LOPTS_INITIALIZER expands to a comma-separated
+# sequence of struct option initializers, suitable for use in a declaration
+# of an array of struct option elements with static storage duration.  The
+# all-zero terminating element required by getopt_long must be added by the
+# user.  For example:
+#
+#   static const struct option lopts[] = { LOPTS_INITIALIZER, {0} };
+#
+# The help text for an individual struct option element may be obtained by
+# the function
+#
+#   struct lopt_help { const char *desc, *arg; }
+#   *lopt_get_help(const struct option *opt);
+#
+# The returned desc and arg pointers point to the argument name and help text
+# for the argument, respectively, as written in the options specification file.
+#
+# License WTFPL2: Do What The Fuck You Want To Public License, version 2.
+# This is free software: you are free to do what the fuck you want to.
+# There is NO WARRANTY, to the extent permitted by law.
+
+END {
+  print "/*"
+  if (FILENAME) {
+    print " * Automatically generated by gen-options.awk from " FILENAME
+  } else {
+    print " * Automatically generated by gen-options.awk"
+  }
+  print " * Do not edit."
+  print " */"
+}
+
+BEGIN {
+  sopt_string = ""
+  num_options = 0
+  lopt = ""
+  err = 0
+}
+
+# Parse option specifier lines
+$0 ~ /^-/ {
+  work = $0
+  arg = lopt = sopt = ""
+  has_arg = 0
+
+  # Extract short option name
+  if (work ~ /^-[^-]/) {
+    sopt = substr(work, 2, 1)
+    sub(/^-.,[ \t]*/, "", work)
+  }
+
+  # Extract long option name
+  if (work ~ /^--/) {
+    if (n = match(work, /[= \t[]/)) {
+      lopt = substr(work, 3, n-3)
+      work = substr(work, n)
+    } else {
+      lopt = substr(work, 3)
+      work = ""
+    }
+  }
+
+  # Extract argument name
+  if (work ~ /^\[=[^\] \t]+\]/) {
+    if (n = index(work, "]")) {
+      arg = substr(work, 3, n-3)
+      work = substr(work, n+1)
+    }
+    has_arg = 2
+  } else if (work ~ /^=/) {
+    if (n = match(work, /[ \t]/)) {
+      arg  = substr(work, 2, n-2)
+      work = substr(work, n)
+    } else {
+      arg  = substr(work, 2)
+      work = ""
+    }
+    has_arg = 1
+  }
+
+  # Extract action
+  sub(/^[ \t]*/, "", work)
+  if (!sopt && work ~ /^\([^, \t]+(,[ \t]*[^, \t]+)?\)/) {
+    n = split(work, a, /,[ \t]*/)
+    if (n == 2) {
+      flag = substr(a[1], 2) ", " substr(a[2], 1, length(a[2])-1)
+    } else if (n == 1) {
+      flag = "NULL, " substr(a[1], 2, length(a[1])-2)
+    }
+    sub(/^\([^, \t]+(,[ \t]*[^, \t]+)?/, "", work)
+  } else if (sopt) {
+    flag = "NULL, '" sopt "'"
+  } else {
+    flag = "NULL, " to_enum(lopt)
+  }
+
+  if (work) {
+    print "invalid option specification:", $0 > "/dev/stderr"
+    err = 1
+    exit
+  }
+
+  if (sopt) {
+    sopt_string = sopt_string sopt substr("::", 1, has_arg)
+  }
+  options[num_options++] = lopt
+  optionspec[lopt] = has_arg ", " flag
+  if (arg) {
+    optionarg[lopt] = arg
+  }
+
+  next
+}
+
+# Ignore any line beginning with a #
+$0 ~ /^#/ { next }
+
+lopt {
+  sub(/^[ \t]*/, "")
+  if (!$0) { next }
+
+  optionhelp[lopt] = (lopt in optionhelp ? optionhelp[lopt] "\n" : "") $0
+}
+
+# Exit immediately on error
+END { if (err) { exit err } }
+
+END {
+  print "#include <stddef.h>"
+  print "#include <limits.h>\n"
+  print "#define SOPT_STRING \"" sopt_string "\"\n"
+}
+
+# Generate the main options tables
+END {
+  lopt_strings = ""
+
+  count = bucketsort(sorted_options, options)
+  for (i = 0; i < count; i++) {
+    lopt_strings = add_to_strtab(lopt_strings, sorted_options[i], offsets)
+  }
+  gsub(/[^ ]+/, "\"&", lopt_strings)
+  gsub(/ /, "\\0\"\n\t", lopt_strings)
+
+  print "static const char lopt_strings[] ="
+  print "\t" lopt_strings "\";\n"
+  print "enum {"
+  for (i = 0; i < count; i++) {
+    opt = options[i]
+    sep = (i+1 == count ? "" : ",")
+
+    print "\t" to_enum(opt), "= UCHAR_MAX+1 +", offsets[opt] sep
+  }
+  print "};"
+  print "#define lopt_str(x) (lopt_strings + (LOPT_ ## x - UCHAR_MAX - 1))\n"
+
+  print "#define LOPTS_INITIALIZER \\"
+  for (i = 0; i < count; i++) {
+    opt = options[i]
+    sep = (i+1 == count ? "" : ", \\")
+
+    print "\t/* --" opt, "*/ \\"
+    print "\t{ lopt_strings+" offsets[opt] ",", optionspec[opt] " }" sep
+  }
+}
+
+# Generate the help strings
+END {
+  # First, sort out the argument names
+  arg_strings = ""
+
+  count = bucketsort(sorted_args, optionarg)
+  for (i = 0; i < count; i++) {
+    arg_strings = add_to_strtab(arg_strings, sorted_args[i], arg_offsets)
+  }
+
+  n = split(arg_strings, arg_split)
+  arg_strings = ""
+  for (i = 1; i <= n; i++) {
+    for (opt in optionarg) {
+      if (optionarg[opt] == arg_split[i]) {
+        l10narg[opt] = 1
+        break;
+      }
+    }
+
+    sep = (i < n ? "\"\\0\"" : "")
+    arg_strings = arg_strings "\n\tPN_(\"" opt "\", \"" arg_split[i] "\")" sep
+  }
+
+  print "\n#define ARG_L10N_(x)"
+  print "#ifndef PN_"
+  print "#  define PN_(c, x) x"
+  print "#endif\n"
+
+  print "static const char arg_strings[] = " arg_strings "\"\";"
+  for (opt in optionarg) {
+    if (opt in l10narg) {
+      continue
+    }
+    print "\tARG_L10N_(PN_(\"" opt "\", \"" optionarg[opt] "\"))"
+  }
+
+  # Then add in the actual descriptions
+  print "\nstatic const char help_strings[] ="
+  help = ""
+  help_pos = 0
+  for (opt in options) {
+    opt = options[opt]
+    if (opt in optionhelp) {
+      if (help) {
+        print help "\"\\0\""
+      }
+
+      help = optionhelp[opt]
+      help_offsets[opt] = help_pos
+      help_pos += length(help) + 1
+
+      gsub(/"/, "\\\"", help)
+      gsub(/\n/, "\\n\"\n\t    \"", help)
+      help = "\tPN_(\"" opt "\",\n\t    \"" help "\")"
+    }
+  }
+  print help "\"\";"
+  for (opt in options) {
+    opt = options[opt]
+    if (!(opt in optionhelp)) {
+      print "\tARG_L10N_(PN_(\"" opt "\", \"\"))"
+      help_offsets[opt] = help_pos
+    }
+  }
+
+  print "\nstatic struct lopt_help { const char *desc, *arg; }"
+  print "*lopt_get_help(const struct option *opt, struct lopt_help *out)\n{"
+  print "\tswitch ((opt->name - lopt_strings) + UCHAR_MAX + 1) {"
+  for (opt in options) {
+    opt = options[opt]
+    print "\tcase", to_enum(opt) ":"
+    print "\t\tout->desc = help_strings +", help_offsets[opt] ";"
+    if (opt in optionarg) {
+      print "\t\tout->arg = arg_strings +", arg_offsets[optionarg[opt]] ";"
+    }
+    print "\t\treturn out;"
+  }
+  print "\t}\n\n\treturn NULL;"
+  print "}"
+}
+
+# bucketsort(dst, src)
+#
+# Sort the elements of src by descending string length,
+# placing them into dst[0] ... dst[n].
+#
+# Returns the number of elements.
+function bucketsort(dst, src, buckets, max, count, i, t)
+{
+  for (t in src) {
+    i = length(src[t])
+    if (i > max) { max = i }
+    buckets[i]++
+  }
+
+  for (i = max; i > 0; i--) {
+    if (i in buckets) {
+      t = buckets[i]
+      buckets[i] = count
+      count += t
+    }
+  }
+
+  for (t in src) {
+    i = length(t = src[t])
+    dst[buckets[i]++] = t
+  }
+
+  return count
+}
+
+# to_enum(lopt)
+#
+# Return the string LOPT_xxx, where xxx is the argument with all lowercase
+# letters converted to uppercase, and all non-alphanumeric characters replaced
+# with underscores.
+function to_enum(lopt)
+{
+  lopt = toupper(lopt)
+  gsub(/[^ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789]/, "_", lopt)
+  return "LOPT_" lopt
+}
+
+# add_to_strtab(strtab, str, offsets)
+#
+# Append string to strtab if there is not already a matching string present
+# in the table.  Newly-added strings are separated by spaces, which must be
+# translated into null bytes afterwards.  The updated strtab is returned, and
+# the offsets[str] array member is updated with the position (counting from 0)
+# of str in the strtab.
+#
+# For optimal results, strings should be added in descending length order.
+function add_to_strtab(strtab, str, offsets, pos)
+{
+    if ( (pos = index(strtab, str " ") - 1) < 0) {
+      pos = length(strtab)
+      if (pos) {
+        strtab = strtab " " str
+        pos++
+      } else {
+        strtab = strtab str
+      }
+    }
+    offsets[str] = pos
+    return strtab
+}
diff --git a/tests/scripts.at b/tests/scripts.at
new file mode 100644 (file)
index 0000000..29471d8
--- /dev/null
@@ -0,0 +1,214 @@
+AT_BANNER([Script tests])
+
+AT_SETUP([gen-options.awk])
+
+AT_DATA([options.def],
+[[--option-only
+--option-with-val (5)
+--option-with-flagval (&x, 5)
+--option-with-arg=ARG
+some "text" goes here
+--option-with-optional-arg[=OPTIONAL]
+
+hello
+-a, --option-with-sopt
+
+-b, --option-with-sopt-and-arg=SOPTARG
+-c, --option-with-sopt-and-optional-arg[=SOPTOPTIONAL]
+--option-with-arg-and-val=ARGVAL (42)
+--option-with-arg-and-flagval=ARGFLAGVAL (&a[1], 'x')
+--option-with-optional-arg-and-val[=OPTIONALARGVAL] (54)
+--option-with-optional-arg-and-flagval[=OPTIONALFLAGVAL] (0, 0)
+--with-sopt
+Here is a help string
+    that has a line randomly indented
+# with a comment
+    @&t@
+and a blank line
+--with-arg=ARG
+do stuff with ARG
+--flagval
+]])
+
+AT_CHECK([$AWK -f "$builddir/scripts/gen-options.awk" <options.def >options.h])
+
+AT_DATA([context.h],
+[[struct option { const char *name; int has_arg; int *flag; int val; };
+int x, a[5];
+]])
+
+# test 0: sanity test
+AT_DATA([test0.c],
+[[#include "context.h"
+#include "options.h"
+
+static const char sopts[] = SOPT_STRING;
+static const struct option opts[] = { LOPTS_INITIALIZER, {0} };
+
+int main(void)
+{
+  return 0;
+}
+]])
+AT_CHECK([$CC -o test0$EXEEXT test0.c && ./test0$EXEEXT], [0], [], [ignore])
+
+# test 1: long option names and help text
+AT_DATA([test1.c],
+[[#include <stdio.h>
+#include <stdlib.h>
+
+#include "context.h"
+#include "options.h"
+
+static const struct option opts[] = { LOPTS_INITIALIZER };
+
+int main(void)
+{
+  unsigned i;
+
+  for (i = 0; i < sizeof opts / sizeof opts[0]; i++) {
+    struct lopt_help help = { "INVALID", "INVALID" };
+
+    if (!lopt_get_help(&opts[i], &help))
+      return EXIT_FAILURE;
+
+    printf("--%s", opts[i].name);
+    if (opts[i].has_arg)
+      printf("=%s", help.arg);
+    printf("\n%s", help.desc);
+    if (help.desc[0])
+      putchar('\n');
+  }
+
+  return 0;
+}
+]])
+
+# pick out interesting bits from the definitions file
+sed -n '/^-/s/^.*--\([[^\= @<:@]]*\).*$/\1/p' options.def >options
+sed -n '/^-/{
+  s/[[^=]]*\(=[[^@:>@ ]]*\).*$/\1/
+  s/^[[^=]].*//
+  s/^=//
+  p
+}' options.def >argnames
+
+AS_ECHO(["-"]) | sed -n '1s/^-.*//p
+1,/^-/d
+t clear
+:clear
+s/^-.*//p
+t
+s/^#.*//
+s/^ *//
+t next
+:next
+N
+s/\n-.*//
+t done
+s/\n#.*//
+s/\n */\n/g
+t next
+:done
+s/"/\\\\"/g
+s/[[^\n]][[^\n]]*/\\"&\\"/g
+s/^\n*//
+s/\n*$//
+s/\n\n*/ /g
+p
+' options.def - >helptext
+
+exec 3<options 4<argnames 5<helptext 6>expout
+while read opt <&3 && read arg <&4 && read help <&5; do
+  if test ${arg:+y}; then
+    AS_ECHO(["--$opt=$arg"]) >&6
+  else
+    AS_ECHO(["--$opt"]) >&6
+  fi
+  eval "set x $help"; shift
+  for arg
+  do
+    AS_ECHO(["$arg"]) >&6
+  done
+done
+exec 3<&- 4<&- 5<&- 6>&-
+
+AT_CHECK([$CC -o test1$EXEEXT test1.c && ./test1$EXEEXT],
+  [0], [expout], [ignore])
+
+# test 2: short option string
+AT_DATA([test2.c],
+[[#include <stdio.h>
+#include <stdlib.h>
+
+#include "context.h"
+#include "options.h"
+
+int main(void)
+{
+  struct option lopts[] = {LOPTS_INITIALIZER};
+  unsigned i, j;
+
+  for (i = 0; i < sizeof SOPT_STRING - 1; i++) {
+    if (SOPT_STRING[i] != ':') {
+      for (j = 0; j < sizeof lopts / sizeof lopts[0]; j++) {
+        if (lopts[j].val == SOPT_STRING[i]) {
+          printf("--%s ", lopts[j].name);
+          break;
+        }
+      }
+    }
+    putchar(SOPT_STRING[i]);
+    if (SOPT_STRING[i+1] != ':')
+      putchar('\n');
+  }
+}
+]])
+
+sed -n '/^-/{
+  s/=.*/:/
+  s/[[@<:@]]/:/
+  s/^-\([[^-]]\)[[^:]]*/\1/p
+  s/^-.*//p
+}' options.def >sopts
+
+exec 3<options 4<sopts 5>expout
+while read lopt <&3 && read sopt <&4; do
+  if test ${sopt:+y}; then
+    AS_ECHO(["--$lopt $sopt"]) >&5
+  fi
+done
+exec 3<&- 4<&- 5>&-
+
+AT_CHECK([$CC -o test2$EXEEXT test2.c && ./test2$EXEEXT],
+  [0], [expout], [ignore])
+
+# Check that all help strings are translatable
+sed 's/\([[^\\]]\\\)" /\1\\n\\" /g' helptext >help-po
+exec 3<options 4<argnames 5<help-po 6>expected.po
+while read opt <&3 && read arg <&4 && read help <&5; do
+  if test ${arg:+y}; then
+    AS_ECHO(["msgctxt \"$opt\" msgid \"$arg\""]) >&6
+  fi
+  AS_ECHO(["msgctxt \"$opt\" msgid${help:+ }$help"]) >&6
+done
+exec 3<&- 4<&- 5<&- 6>&-
+
+AT_CHECK([xgettext --keyword=PN_:1c,2 options.h
+  test -f messages.po || exit 77])
+
+LC_ALL=C sort expected.po >expout
+AT_CHECK([sed -n '/^msgctxt/{
+t next
+:next
+N
+s/\nmsgstr.*//
+t done
+s/\s*""//
+s/\n/ /
+t next
+:done
+p
+}' messages.po | LC_ALL=C sort], [0], [expout])
+
+AT_CLEANUP
index eff4408aab14ef39f3ee06ab8b9c7fb31bbb53d3..0ff22f447a6979558e380bcfec1d26765b0b8d4f 100644 (file)
@@ -30,3 +30,4 @@ m4_include([tests/macros.at])
 m4_include([tests/functions.at])
 m4_include([tests/programs.at])
 m4_include([tests/libs.at])
+m4_include([tests/scripts.at])