changeset 803:2cd233b137ec

common.subr: Implement postprocess_getopts_for_long() to process long options in combination with the Shell's internal getopts()
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 04 Nov 2024 11:08:15 +0100
parents 31ff8e5efdd6
children f406b3b76b62
files share/local-bsdtools/common.subr tests/common.t
diffstat 2 files changed, 540 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- 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:
--- /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=" ]