]> git.draconx.ca Git - dxcommon.git/commitdiff
Add common option formatting routines.
authorNick Bowler <nbowler@draconx.ca>
Mon, 13 Sep 2021 06:30:47 +0000 (02:30 -0400)
committerNick Bowler <nbowler@draconx.ca>
Mon, 13 Sep 2021 06:33:21 +0000 (02:33 -0400)
Printing a list of program options for --help output is typically
very consistent in all my programs.  Adapt the cdecl99 code to be a
bit more generic so it can (hopefully) be shared across packages.

Makefile.am
configure.ac
src/help.c [new file with mode: 0644]
src/help.h [new file with mode: 0644]
t/.gitignore
t/helpdesc.c [new file with mode: 0644]
t/helpopt.c [new file with mode: 0644]
tests/functions.at

index 17cf7d5aa8386273c4557ec8c1df5c935bd913dc..a9f793484145c4d3769e1ff319b90aece2cb5692 100644 (file)
@@ -17,6 +17,13 @@ t_packtests_SOURCES = t/packtests.c src/pack.c src/tap.c
 t_packtestu64_SOURCES = t/packtestu64.c src/pack.c src/tap.c
 t_packtests64_SOURCES = t/packtests64.c src/pack.c src/tap.c
 
+if HAVE_STRUCT_OPTION
+check_PROGRAMS += t/helpdesc t/helpopt
+endif
+
+t_helpopt_SOURCES = t/helpopt.c src/help.c src/tap.c
+t_helpdesc_SOURCES = t/helpdesc.c src/help.c src/tap.c
+
 DISTCLEANFILES =
 EXTRA_DIST =
 SUFFIXES =
index 6150d4c565332a8b86f1db37d536fec08ba94317..75a4b949139837f34e66fb6f776494bdef3ec4d4 100644 (file)
@@ -24,5 +24,13 @@ AC_CONFIG_TESTDIR([.])
 DX_PROG_AUTOTEST
 AM_CONDITIONAL([HAVE_AUTOTEST], [test x"$dx_cv_autotest_works" = x"yes"])
 
+AC_CACHE_CHECK([for struct option in <getopt.h>], [dx_cv_have_struct_option],
+  [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([#include <getopt.h>],
+[[struct option opt = { "aaaa", 2, (void *)0, 'a' };
+return opt.name[opt.flag ? opt.val : opt.has_arg];]])],
+  [dx_cv_have_struct_option=yes], [dx_cv_have_struct_option=no])])
+AM_CONDITIONAL([HAVE_STRUCT_OPTION],
+  [test x"$dx_cv_have_struct_option" = x"yes"])
+
 AC_CONFIG_FILES([Makefile atlocal])
 AC_OUTPUT
diff --git a/src/help.c b/src/help.c
new file mode 100644 (file)
index 0000000..0ceb8f4
--- /dev/null
@@ -0,0 +1,157 @@
+/*
+ * Copyright © 2021 Nick Bowler
+ *
+ * Helper functions for formatting --help program output.
+ *
+ * In order to support localized output, this depends on the Gnulib gettext-h
+ * and mbswidth modules.  However, if ENABLE_NLS is not defined (or defined to
+ * 0) then these modules are not required.
+ *
+ * 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.
+ */
+
+#if HAVE_CONFIG_H
+#      include <config.h>
+#endif
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+#include <limits.h>
+#include <getopt.h>
+
+#include "help.h"
+
+#ifndef ENABLE_NLS
+#      define ENABLE_NLS 0
+#endif
+
+#if ENABLE_NLS
+#      include <gettext.h>
+#      include <mbswidth.h>
+#else
+#      define gettext(s) (s)
+#      define pgettext_expr(c, s) (s)
+#      define mbsnwidth(a, b, c) (assert(0), 0)
+#endif
+
+#define _(s) gettext(s)
+
+/* Returns a single numeric value depending on the type of option:
+ *
+ *   6 - if the option has a short option character and an optional argument
+ *   5 - if the option has a short option character and a mandatory argument
+ *   4 - if the option has a short option charater and no argument
+ *   3 - N/A
+ *   2 - if the option has no short option character and an optional argument
+ *   1 - if the option has no short option character and a mandatory argument
+ *   0 - if the option has no short option character and no argument
+ */
+static int option_type(const struct option *opt)
+{
+       return ((opt->val <= CHAR_MAX) << 2) | (opt->has_arg & 3);
+}
+
+enum {
+       OPT_SHORT_WITH_OPTIONAL_ARG  = 6,
+       OPT_SHORT_WITH_MANDATORY_ARG = 5,
+       OPT_SHORT_WITHOUT_ARG        = 4,
+       OPT_LONG_WITH_OPTIONAL_ARG   = 2,
+       OPT_LONG_WITH_MANDATORY_ARG  = 1,
+       OPT_LONG_WITHOUT_ARG         = 0,
+};
+
+int help_print_optstring(const struct option *opt, const char *argname, int l)
+{
+       char optstring[100];
+       int w;
+
+       if (!ENABLE_NLS)
+               goto no_translate;
+
+       switch (option_type(opt)) {
+       case OPT_SHORT_WITH_OPTIONAL_ARG:
+               w = snprintf(optstring, sizeof optstring,
+                            _("  -%c, --%s[=%s]"), opt->val, opt->name,
+                            pgettext_expr(opt->name, argname));
+               break;
+       case OPT_LONG_WITH_OPTIONAL_ARG:
+               w = snprintf(optstring, sizeof optstring,
+                            _("  --%s[=%s]"), opt->name,
+                            pgettext_expr(opt->name, argname));
+               break;
+       case OPT_SHORT_WITH_MANDATORY_ARG:
+               w = snprintf(optstring, sizeof optstring,
+                            _("  -%c, --%s=%s"), opt->val, opt->name,
+                            pgettext_expr(opt->name, argname));
+               break;
+       case OPT_LONG_WITH_MANDATORY_ARG:
+               w = snprintf(optstring, sizeof optstring,
+                            _("  --%s=%s"), opt->name,
+                            pgettext_expr(opt->name, argname));
+               break;
+       case OPT_SHORT_WITHOUT_ARG:
+               w = snprintf(optstring, sizeof optstring,
+                            _("  -%c, --%s"), opt->val, opt->name);
+               break;
+       case OPT_LONG_WITHOUT_ARG:
+               w = snprintf(optstring, sizeof optstring,
+                            _("  --%s"), opt->name);
+               break;
+       default:
+               assert(0);
+       }
+
+       if (w < 0)
+               goto no_translate;
+
+       w = mbsnwidth(optstring, w, 0);
+       printf("%s", optstring);
+       goto out;
+
+no_translate:
+       switch (option_type(opt)) {
+       case OPT_SHORT_WITH_OPTIONAL_ARG:
+               w = printf("  -%c, --%s[=%s]", opt->val, opt->name, argname);
+               break;
+       case OPT_LONG_WITH_OPTIONAL_ARG:
+               w = printf("  --%s[=%s]", opt->name, argname);
+               break;
+       case OPT_SHORT_WITH_MANDATORY_ARG:
+               w = printf("  -%c, --%s=%s", opt->val, opt->name, argname);
+               break;
+       case OPT_LONG_WITH_MANDATORY_ARG:
+               w = printf("  --%s=%s", opt->name, argname);
+               break;
+       case OPT_SHORT_WITHOUT_ARG:
+               w = printf("  -%c, --%s", opt->val, opt->name);
+               break;
+       case OPT_LONG_WITHOUT_ARG:
+               w = printf("  --%s", opt->name);
+               break;
+       default:
+               assert(0);
+       }
+out:
+       if (w < 0 || w > l) {
+               putchar('\n');
+               return 0;
+       }
+
+       return w;
+}
+
+void help_print_desc(const struct option *opt, const char *s, int i, int w)
+{
+       for (s = pgettext_expr(opt->name, s); *s; w = 0) {
+               const char *nl = strchr(s, '\n');
+               int n = (nl ? nl-s : -1);
+
+               printf("%*s%.*s\n", i-w, "", n, s);
+               if (!nl)
+                       break;
+
+               s = nl+1;
+       }
+}
diff --git a/src/help.h b/src/help.h
new file mode 100644 (file)
index 0000000..9eb2654
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright © 2021 Nick Bowler
+ *
+ * Helper functions for formatting --help program output.
+ *
+ * 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.
+ */
+
+#ifndef DX_HELP_H_
+#define DX_HELP_H_
+
+struct option;
+
+/*
+ * Print an option string describing the short option character (if any),
+ * the long option name, and the argument name (if applicable).  The argument
+ * name is localized (if NLS is enabled).  If the string width is more than
+ * l columns, a newline is printed and 0 is returned.  Otherwise, a newline
+ * is not printed and the string width (in columns) is returned.
+ */
+int help_print_optstring(const struct option *opt, const char *argname, int l);
+
+/*
+ * Print an option description with each line indented.  The string is first
+ * localized (if NLS is enabled).  The first line will be indented by i-w
+ * spaces (to account for the cursor being in some other column), all other
+ * lines are indented by i spaces.
+ */
+void help_print_desc(const struct option *opt, const char *desc, int i, int w);
+
+static inline void help_print_option(const struct option *opt,
+                                     const char *argname, const char *desc,
+                                    int w)
+{
+       if (w < 2)
+               w = 2;
+
+       help_print_desc(opt, desc, w,
+                       help_print_optstring(opt, argname, w-2));
+}
+
+#endif
index b36f57784ebabb7587651dbad46fe63e8c05ab73..7fee3da6ceb3278f704ca44405c49dcaa2c2ebb5 100644 (file)
@@ -1,2 +1,4 @@
-packtest[su]
-packtest[su]64
+/helpdesc
+/helpopt
+/packtest[su]
+/packtest[su]64
diff --git a/t/helpdesc.c b/t/helpdesc.c
new file mode 100644 (file)
index 0000000..923dd2f
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * Read some text from standard input and format it with help_print_desc,
+ * for testing.  Each pair of program arguments is converted to an int and
+ * passed as the two integer arguments to help_print_desc.
+ */
+#include "help.h"
+#include "tap.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <errno.h>
+
+static char buf[1000];
+
+int arg_to_int(const char *s)
+{
+       char *end;
+       long val;
+
+       errno = 0;
+       val = strtol(s, &end, 0);
+       if (*end != 0)
+               tap_bail_out("%s: numeric argument expected", s);
+       else if (val < INT_MIN || val > INT_MAX || errno == ERANGE)
+               tap_bail_out("%s: %s", s, strerror(ERANGE));
+       else if (errno)
+               tap_bail_out("%s: %s", s, strerror(errno));
+
+       return val;
+}
+
+int main(int argc, char **argv)
+{
+       long a, b;
+       size_t len;
+       int i;
+
+       len = fread(buf, 1, sizeof buf - 1, stdin);
+       if (len == sizeof buf - 1)
+               tap_bail_out("too much input text");
+       if (ferror(stdin))
+               tap_bail_out("error reading from stdin: %s", strerror(errno));
+
+       for (i = 1; i < argc; i += 2) {
+               int indent = arg_to_int(argv[i]);
+               int sub = i+1 < argc ? arg_to_int(argv[i+1]) : 0;
+
+               help_print_desc(NULL, buf, indent, sub);
+       }
+
+       return 0;
+}
diff --git a/t/helpopt.c b/t/helpopt.c
new file mode 100644 (file)
index 0000000..303e13a
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Read some text from standard input and format it with help_print_desc,
+ * for testing.  Each pair of program arguments is converted to an int and
+ * passed as the two integer arguments to help_print_desc.
+ */
+#include "help.h"
+#include "tap.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <errno.h>
+
+#include <getopt.h>
+
+static char buf[1000];
+
+int arg_to_int(const char *s)
+{
+       char *end;
+       long val;
+
+       errno = 0;
+       val = strtol(s, &end, 0);
+       if (*end != 0)
+               tap_bail_out("%s: numeric argument expected", s);
+       else if (val < INT_MIN || val > INT_MAX || errno == ERANGE)
+               tap_bail_out("%s: %s", s, strerror(ERANGE));
+       else if (errno)
+               tap_bail_out("%s: %s", s, strerror(errno));
+
+       return val;
+}
+
+void print_opt(struct option *opt, const char *argname, int w)
+{
+       w = help_print_optstring(opt, argname, w);
+       printf("\t%d\n", w);
+}
+
+int main(int argc, char **argv)
+{
+       struct option opt = {0};
+       const char *argname = 0;
+       int i, w = 20;
+
+       for (i = 1; i < argc; i++) {
+               if (argv[i][0] == '-' && argv[i][1] == '-') {
+                       if (opt.name)
+                               print_opt(&opt, argname, w);
+                       opt.val = UCHAR_MAX+1;
+                       opt.has_arg = 0;
+                       opt.name = argv[i]+2;
+               } else if (argv[i][0] == '-') {
+                       opt.val = argv[i][1];
+               } else if (argv[i][0] == '[') {
+                       char *c;
+
+                       argname = argv[i]+1;
+                       if ((c = strchr(argname, ']')))
+                               *c = 0;
+                       opt.has_arg = 2;
+               } else if (argv[i][0] >= '0' && argv[i][0] <= '9') {
+                       w = arg_to_int(argv[i]);
+               } else {
+                       argname = argv[i];
+                       opt.has_arg = 1;
+               }
+       }
+
+       if (opt.name)
+               print_opt(&opt, argname, w);
+
+       return 0;
+}
index 115b8cffa08593530fb533d928a5cc15d27b0351..507c10571a3c79eae484baebd74d66a145e3cfac 100644 (file)
@@ -1,4 +1,4 @@
-dnl Copyright © 2015 Nick Bowler
+dnl Copyright © 2015, 2021 Nick Bowler
 dnl
 dnl License WTFPL2: Do What The Fuck You Want To Public License, version 2.
 dnl This is free software: you are free to do what the fuck you want to.
@@ -28,3 +28,55 @@ TEST_TAP_SIMPLE([signed unpacking], [packtests], [], [pack])
 TEST_TAP_SIMPLE([unsigned unpacking], [packtestu], [], [pack])
 TEST_TAP_SIMPLE([64-bit signed unpacking], [packtests64], [], [pack])
 TEST_TAP_SIMPLE([64-bit unsigned unpacking], [packtestu64], [], [pack])
+
+AT_BANNER([Help formatting functions])
+
+AT_SETUP([help_print_desc])
+
+AT_SKIP_IF([test ! -x "$builddir/t/helpdesc"])
+
+AT_DATA([test.txt],
+[[this is the first line
+this is the second line
+this is the third line
+and so on
+]])
+
+sed -e '5,$s/^/     /' -e '6,$s/^/     /' \
+  -e '10,$s/^/                    /' \
+  -e '13s/^ *//' -e '14,$s/^/          /' \
+  test.txt test.txt test.txt test.txt >expout
+
+AT_CHECK(["$builddir/t/helpdesc" 0 0 10 5 30 20 40 40 <test.txt],
+  [0], [expout])
+
+AT_CLEANUP
+
+AT_SETUP([help_print_optstring])
+
+AT_SKIP_IF([test ! -x "$builddir/t/helpopt"])
+
+AT_CHECK([m4_join([ ],
+  ["$builddir/t/helpopt"],
+  [--foo],
+  [--bar -b],
+  [--baz ARG],
+  [--baz -B ARG],
+  [--quux '@<:@ARG@:>@'],
+  [--quux -q '@<:@ARG@:>@'],
+  [--hello-this-is-a-very-long-option 20],
+  [--hello-this-is-a-very-long-option 50],
+  [--not-long 12])], [0],
+[[  --foo      7
+  -b, --bar    11
+  --baz=ARG    11
+  -B, --baz=ARG        15
+  --quux[=ARG] 14
+  -q, --quux[=ARG]     18
+  --hello-this-is-a-very-long-option
+       0
+  --hello-this-is-a-very-long-option   36
+  --not-long   12
+]])
+
+AT_CLEANUP