From: Nick Bowler Date: Mon, 13 Sep 2021 06:30:47 +0000 (-0400) Subject: Add common option formatting routines. X-Git-Url: https://git.draconx.ca/gitweb/dxcommon.git/commitdiff_plain/843799dbec995b276a7d017bb32c053747b55400 Add common option formatting routines. 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. --- diff --git a/Makefile.am b/Makefile.am index 17cf7d5..a9f7934 100644 --- a/Makefile.am +++ b/Makefile.am @@ -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 = diff --git a/configure.ac b/configure.ac index 6150d4c..75a4b94 100644 --- a/configure.ac +++ b/configure.ac @@ -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 ], [dx_cv_have_struct_option], + [AC_COMPILE_IFELSE([AC_LANG_PROGRAM([#include ], +[[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 index 0000000..0ceb8f4 --- /dev/null +++ b/src/help.c @@ -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 +#endif +#include +#include +#include +#include +#include + +#include "help.h" + +#ifndef ENABLE_NLS +# define ENABLE_NLS 0 +#endif + +#if ENABLE_NLS +# include +# include +#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 index 0000000..9eb2654 --- /dev/null +++ b/src/help.h @@ -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 diff --git a/t/.gitignore b/t/.gitignore index b36f577..7fee3da 100644 --- a/t/.gitignore +++ b/t/.gitignore @@ -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 index 0000000..923dd2f --- /dev/null +++ b/t/helpdesc.c @@ -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 +#include +#include +#include +#include + +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 index 0000000..303e13a --- /dev/null +++ b/t/helpopt.c @@ -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 +#include +#include +#include +#include + +#include + +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; +} diff --git a/tests/functions.at b/tests/functions.at index 115b8cf..507c105 100644 --- a/tests/functions.at +++ b/tests/functions.at @@ -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 @'], + [--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