# HG changeset patch # User Franz Glasner # Date 1730714895 -3600 # Node ID 2cd233b137ec5b1e6e153a25f2ccdd1499613080 # Parent 31ff8e5efdd6c512263c36be7f63cdd41cfc65dd common.subr: Implement postprocess_getopts_for_long() to process long options in combination with the Shell's internal getopts() diff -r 31ff8e5efdd6 -r 2cd233b137ec share/local-bsdtools/common.subr --- a/share/local-bsdtools/common.subr Sun Nov 03 16:59:50 2024 +0100 +++ b/share/local-bsdtools/common.subr Mon Nov 04 11:08:15 2024 +0100 @@ -248,6 +248,230 @@ #: +#: Helper for `getopts` to process long options also. +#: +#: Invocation: +#: postprocess_getopts_for_long shortopts varname longopt-spec* '' "$@" +#: +#: The `shortopts` *must* contain ``-:`` to allow a "short" option with +#: name ``-`` and further arguments. This is used by this function to +#: process them further and augment `OPTARG` and `varname` accordingly. +#: +#: Input (Globals): +#: - The variable with name `varname` +#: - OPTARG +#: +#: Output (Globals): +#: - The variable with name `varname` +#: - OPTARG +#: +#: Returns: +#: int: 1 if an internal processing error occurs or if the long options +#: are not terminated by a null string, +#: 0 otherwise +#: +#: Heavily inspired by +#: https://stackoverflow.com/questions/402377/using-getopts-to-process-long-and-short-command-line-options +#: +#: Error handling is in the line of the POSIX spec for a shell's `getopts`: +#: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/getopts.html. +#: In addition to "normal" error handling, if a long options that does not +#: accept an option argument has one the same type of error is reported +#: as if a required option argument were missing. +#: +#: A leading ``:`` in `shortopts` is handled also if an alternate error +#: handling is to be employed. +#: +#: Arguments for long options *must* use the form ``--long-option=value``. +#: +#: There is no default relation between short and long options. The evaluation +#: by the caller is responsible for this. +#: +#: A long option may not contain the ``=`` character. But if a `longopt-spec` +#: has a trailing ``=`` the long option requires an argument in the same +#: way as the ``:`` for a short option. +#: +#: If a long option is specified more than once in `longopt-spec` the first +#: one is taken into account. +#: +#: The `shortopts`, the `varname` must be the same as given to `getopts`. +#: This is also true for the arguments to be analyzed. +#: +#: Example: +#: +#: :: +#: while getopts "a:bc-:" opt "$@"; do +#: postprocess_getopts_for_long "a:bc-:" opt "long-a=" "long-b" "long-d" "$@" +#: case "${opt}" in +#: a|long-a) +#: a_value="${OPTARG}";; +#: b|long-b) +#: opt_b=1;; +#: c) +#: opt_c=1;; +#: long-d) +#: opt_d=1;; +#: \?) +#: # handle the error (message already printed to stderr) +#: exit 2;; +#: *) +#: # other inconsistency +#: exit 2;; +#: esac +#: done +#: +postprocess_getopts_for_long() { + # $1 $2 $3... "" $n... + local - + + local __ppgofl_shortopts __ppgofl_varname \ + __ppgofl_opt __ppgofl_longspec __ppgofl_longopt __ppgofl_longoptarg \ + __ppgofl_caller_reports_error \ + __ppgofl_base + + __ppgofl_shortopts="${1}" + __ppgofl_varname="${2}" + eval __ppgofl_opt=\"\$\{"${__ppgofl_varname}"\}\" + + # Check whether it we currently handle a long option + [ "${__ppgofl_opt}" = '-' ] || return 0 + + # + # long option: reformulate OPT and OPTARG + # + + case "${__ppgofl_shortopts}" in + :*) + __ppgofl_caller_reports_error=yes;; + *) + __ppgofl_caller_reports_error='';; + esac + + __ppgofl_longopt='' + case "${OPTARG}" in + *=*) + __ppgofl_longopt="${OPTARG%%=*}" + __ppgofl_longoptarg="${OPTARG#*=}" + ;; + *) + __ppgofl_longopt="${OPTARG}" + unset __ppgofl_longoptarg + ;; + esac + + shift 2 + + # + # Early check for null termination of long options. + # Also check that the `=' character is not part of the name of a long + # option. + # + __ppgofl_base=0 + for __ppgofl_longspec in "$@"; do + __ppgofl_base=$((__ppgofl_base + 1)) + # [ -z "${__ppgofl_longspec}" ] && break + case "${__ppgofl_longspec}" in + '') + break + ;; + *=*[!=]|*=*=) + # Error: option contains a = character + if [ -n "${__ppgofl_caller_reports_error}" ]; then + setvar "${__ppgofl_varname}" '?' # XXX FIXME this char??? + OPTARG="${__ppgofl_longspec}" + else + setvar "${__ppgofl_varname}" '?' + unset OPTARG + err "Long option specification contains a forbidden \`=' character: ${__ppgofl_longspec}" + fi + return 1 + ;; + *) + : + ;; + esac + done + if [ -n "${__ppgofl_longspec}" ]; then + if [ -n "${__ppgofl_caller_reports_error}" ]; then + setvar "${__ppgofl_varname}" ':' + OPTARG='' + else + setvar "${__ppgofl_varname}" '?' + unset OPTARG + err "Missing null terminator for long option specifications" + fi + return 1 + fi + +# # Print the "real" arguments that will be analyzed +# i=1 +# while [ $((i + __ppgofl_base)) -le $# ]; do +# eval v=\"\$\{$((i + __ppgofl_base))\}\" +# echo "HUHU: ${v}" +# i=$((i + 1)) +# done +# return 0 + + for __ppgofl_longspec in "$@"; do + if [ -z "${__ppgofl_longspec}" ]; then + # We did not hit a spec because we return ealy in the hit case + if [ -n "${__ppgofl_caller_reports_error}" ]; then + setvar "${__ppgofl_varname}" '?' + OPTARG="${__ppgofl_longopt}" + else + setvar "${__ppgofl_varname}" '?' + unset OPTARG + err "Illegal option --${__ppgofl_longopt}" + fi + return 0 + elif [ "${__ppgofl_longspec}" = "${__ppgofl_longopt}=" ]; then + # Need an argument value + if [ -z "${__ppgofl_longoptarg+SET}" ]; then + # + # Error + # + # Looking for an option like --long-option option-value + # would need to change OPTIND. This is unspecified in + # POSIX. So we handle only --long-option=option-value. + # + if [ -n "${__ppgofl_caller_reports_error}" ]; then + setvar "${__ppgofl_varname}" ':' + OPTARG="${__ppgofl_longopt}" + else + setvar "${__ppgofl_varname}" '?' + unset OPTARG + err "option argument required for option --${__ppgofl_longopt}" + fi + return 0 + fi + setvar "${__ppgofl_varname}" "${__ppgofl_longopt}" + OPTARG="${__ppgofl_longoptarg}" + return 0 + elif [ "${__ppgofl_longspec}" = "${__ppgofl_longopt}" ]; then + # No argument allowed + if [ -n "${__ppgofl_longoptarg+SET}" ]; then + # Error + if [ -n "${__ppgofl_caller_reports_error}" ]; then + setvar "${__ppgofl_varname}" ':' + OPTARG="${__ppgofl_longopt}" + else + setvar "${__ppgofl_varname}" '?' + unset OPTARG + err "no option argument allowed for option --${__ppgofl_longopt}" + fi + return 0 + fi + setvar "${__ppgofl_varname}" "${__ppgofl_longopt}" + unset OPTARG + return 0 + fi + done + # Processing error + return 1 +} + + +#: #: Determine some important dataset properties that are set locally #: #: Args: diff -r 31ff8e5efdd6 -r 2cd233b137ec tests/common.t --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/common.t Mon Nov 04 11:08:15 2024 +0100 @@ -0,0 +1,316 @@ +Basic tests of common.subr + +Shell is /bin/sh. + + +Setup +===== + + $ set -u + $ . "${TESTDIR}/testsetup.sh" + $ _p_datadir="${TESTDIR}/../share/local-bsdtools" + $ . "${_p_datadir}/common.subr" + + +Getopts +======= + +Normal successfull calls + + $ error='' + > opt_a=no + > opt_b=no + > unset opt_b_val + > OPTIND=1 + > while getopts "ab:-:" opt --long-a --long-b=value-of-b arg ; do + > postprocess_getopts_for_long "ab:-:" opt "long-a" "long-b=" "" --long-b=value-of-b arg + > case "$opt" in + > a|long-a) opt_a=yes + > [ -z "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > ;; + > b|long-b) opt_b=yes + > [ -n "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > opt_b_val="${OPTARG}" + > ;; + > \?) + > error=foo + > ;; + > *) + > error=bar + > ;; + > esac + > done + + $ checkyesno opt_a + $ checkyesno opt_b + $ [ "${opt_b_val}" = "value-of-b" ] + $ [ -z "${error}" ] + +Not null-terminated long options speccifications + + $ OPTIND=1 + > getopts "ab:-:" opt --long-a --long-b arg + > postprocess_getopts_for_long "ab:-:" opt "long-a" "long-b" + /bin/sh: ERROR: Missing null terminator for long option specifications + [1] + $ [ "${opt}" = '?' ] + $ [ -z "${OPTARG+SET}" ] + +Not null-terminated long options specs (alternate error handling) + + $ OPTIND=1 + > getopts ":ab:-:" opt --long-a --long-b arg + > postprocess_getopts_for_long ":ab:-:" opt "long-a" "long-b" + [1] + $ [ "${opt}" = ':' ] + $ [ -n "${OPTARG+SET}" ] + $ [ -z "${OPTARG}" ] + +Illegal long option + + $ error='' + > opt_a=no + > opt_b=no + > unset opt_b_val + > OPTIND=1 + > while getopts "ab:-:" opt --long-unknown ; do + > postprocess_getopts_for_long "ab:-:" opt "long-a" "long-b=" "" --long-unknown + > case "$opt" in + > a|long-a) opt_a=yes + > [ -z "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > ;; + > b|long-b) opt_b=yes + > [ -n "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > opt_b_val="${OPTARG}" + > ;; + > \?) + > error=foo + > ;; + > *) + > error=bar + > ;; + > esac + > done + /bin/sh: ERROR: Illegal option --long-unknown + $ [ "${error}" = "foo" ] + +Illegal long option (alternate error handling) + + $ error='' + > opt_a=no + > opt_b=no + > unset opt_b_val + > OPTIND=1 + > while getopts ":ab:-:" opt --long-unknown ; do + > postprocess_getopts_for_long ":ab:-:" opt "long-a" "long-b=" "" --long-unknown + > case "$opt" in + > a|long-a) opt_a=yes + > [ -z "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > ;; + > b|long-b) opt_b=yes + > [ -n "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > opt_b_val="${OPTARG}" + > ;; + > \?) + > error="${OPTARG}" + > ;; + > *) + > error=bar + > ;; + > esac + > done + $ [ "${error}" = "long-unknown" ] + +Missing required option argument + + $ error='' + > opt_a=no + > opt_b=no + > unset opt_b_val + > OPTIND=1 + > while getopts "ab:-:" opt --long-a --long-b arg ; do + > postprocess_getopts_for_long "ab:-:" opt "long-a" "long-b=" "" --long-b arg + > case "$opt" in + > a|long-a) opt_a=yes + > [ -z "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > ;; + > b|long-b) opt_b=yes + > [ -n "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > opt_b_val="${OPTARG}" + > ;; + > ?) + > error=foo + > ;; + > *) + > error=bar + > ;; + > esac + > done + /bin/sh: ERROR: option argument required for option --long-b + $ [ "${error}" = "foo" ] + +Missing required option argument (alternate error handling) + + $ error='' + > opt_a=no + > opt_b=no + > unset opt_b_val + > OPTIND=1 + > while getopts ":ab:-:" opt --long-a --long-b arg ; do + > postprocess_getopts_for_long ":ab:-:" opt "long-a" "long-b=" "" --long-b arg + > case "$opt" in + > a|long-a) opt_a=yes + > [ -z "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > ;; + > b|long-b) opt_b=yes + > [ -n "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > opt_b_val="${OPTARG}" + > ;; + > :) + > error="${OPTARG}" + > ;; + > *) + > error=bar + > ;; + > esac + > done + $ [ "${error}" = "long-b" ] + +Option argument value not allowed + + $ error='' + > opt_a=no + > opt_b=no + > unset opt_b_val + > OPTIND=1 + > while getopts "ab:-:" opt --long-a=a-value arg ; do + > postprocess_getopts_for_long "ab:-:" opt "long-a" "long-b=" "" --long-a=a-value arg + > case "$opt" in + > a|long-a) opt_a=yes + > [ -z "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > ;; + > b|long-b) opt_b=yes + > [ -n "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > opt_b_val="${OPTARG}" + > ;; + > ?) + > error=foo + > ;; + > *) + > error=bar + > ;; + > esac + > done + /bin/sh: ERROR: no option argument allowed for option --long-a + $ [ "${error}" = "foo" ] + +Missing required option argument (alternate error handling) + + $ error='' + > opt_a=no + > opt_b=no + > unset opt_b_val + > OPTIND=1 + > while getopts ":ab:-:" opt --long-a=a-value arg ; do + > postprocess_getopts_for_long ":ab:-:" opt "long-a" "long-b=" "" --long-a=a-value arg + > case "$opt" in + > a|long-a) opt_a=yes + > [ -z "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > ;; + > b|long-b) opt_b=yes + > [ -n "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > opt_b_val="${OPTARG}" + > ;; + > :) + > error="${OPTARG}" + > ;; + > *) + > error=bar + > ;; + > esac + > done + $ [ "${error}" = "long-a" ] + +Invalid character in long option + + $ error='' + > opt_a=no + > OPTIND=1 + > while getopts "ab:-:" opt --long=a ; do + > postprocess_getopts_for_long "ab:-:" opt "long=a" "" --long=a arg + > case "$opt" in + > a|long=a) opt_a=yes + > [ -z "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > ;; + > \?) + > error=foo + > ;; + > *) + > error=bar + > ;; + > esac + > done + /bin/sh: ERROR: Long option specification contains a forbidden `=' character: long=a + $ [ "${error}" = "foo" ] + +Invalid character in long option (alternate error handling) + + $ error='' + > opt_a=no + > OPTIND=1 + > while getopts ":a-:" opt --long=a ; do + > postprocess_getopts_for_long ":a-:" opt "long=a" "" --long=a arg + > case "$opt" in + > a|long=a) opt_a=yes + > [ -z "${OPTARG+SET}" ] || echo "ERROR: OPTARG" + > ;; + > \?) + > error="${OPTARG}" + > ;; + > *) + > error=bar + > ;; + > esac + > done + $ [ "${error}" = "long=a" ] + +Invalid character in long option + + $ error='' + > opt_b=no + > OPTIND=1 + > while getopts "b:-:" opt --long=a ; do + > postprocess_getopts_for_long "b:-:" opt "long=b=" "" --long=b=value arg + > case "$opt" in + > b|long=b) opt_b=yes + > ;; + > \?) + > error=foo + > ;; + > *) + > error=bar + > ;; + > esac + > done + /bin/sh: ERROR: Long option specification contains a forbidden `=' character: long=b= + $ [ "${error}" = "foo" ] + +Invalid character in long option (alternate error handling) + + $ error='' + > opt_b=no + > OPTIND=1 + > while getopts ":b:-:" opt --long=a ; do + > postprocess_getopts_for_long ":b:-:" opt "long=b=" "" --long=b=value arg + > case "$opt" in + > b|long=b) opt_b=yes + > ;; + > \?) + > error="${OPTARG}" + > ;; + > *) + > error=bar + > ;; + > esac + > done + $ [ "${error}" = "long=b=" ]