#!/hint/bash # Copyright (C) 2018 Luke Shumaker # Copyright (C) 2023-2024 Umorpha Systems # SPDX-License-Identifier: AGPL-3.0-or-later opt_specs=() # Usage: # # argparse MYDEFAULT "$@" # case "$opt_mode" in # error) # osi.sh:print "Try '%q --help' for more information" "${0##*/}" >&2 # return $EXIT_INVALIDARGUMENT # ;; # version) # osi.sh:print "%s %s" "$NAME" "$VERSION" # return $EXIT_SUCCESS # ;; # help) # osi.sh:print 'Usage: %s [OPTIONS]' "${0##*/}" # osi.sh:print 'One line description' # echo # osi.sh:print 'Longer description.' # echo # osi.sh:print 'OPTIONS:' # echo "${opt_flaghelp}" # return $EXIT_SUCCESS # ;; # MYDEFAULT) # ... # # Inputs: # # variable: opt_specs # # Callbacks: # # functions (optional): opt_visit_early:${longname} [OPTARG] # functions (required): opt_visit:${longname} [OPTARG] # function (required): opt_positional [POSITONAL_ARGS...] # functions (optional): opt_final:${longname} # # Outputs: # # variable: opt_mode # variable: opt_flaghelp argparse() { local _a_mode_default _a_mode_default="$1" shift # Augment opt_specs. if [[ ${#opt_specs[@]} -gt 0 ]]; then opt_specs+=('') fi opt_specs+=('-h : --help : : display this help') # shellcheck disable=SC2317 opt_visit:help() { argparse:setmode help; } opt_specs+=('-V : --version : : output version information') # shellcheck disable=SC2317 opt_visit:version() { argparse:setmode version; } # These 4 variables control the flag parsing and opt_flaghelp text. local _getopt_short='' # ex: 'a::b:c' local _getopt_long=() # ex: ('airplane::' 'boat:' 'car') declare -A _getopt_short2long=() # ex: ([a]=airplane [b]=boat [c]=car) declare -A _getopt_optarg=() # ex: ([airplane]=maybe [boat]=yes [car]=no) local _help_specs=() # Parse ${opt_specs[@]} to adjust those 5 variables. local _spec for _spec in "${opt_specs[@]}"; do # Normalize whitespace _spec="$(sed -e 's/^\s*//' -e 's/\s*:\s*/:/g' -e 's/\s*$//' <<<"$_spec")" if [[ -z "${_spec}" ]]; then _help_specs+=('') continue fi local _spec_short _spec_long _spec_optarg _spec_desc # shellcheck disable=SC2162 # I want it to mangle backslashes. IFS=':' read _spec_short _spec_long _spec_optarg _spec_desc <<<"${_spec}" if [[ $_spec_long != --* ]]; then osi.sh:bug 'invalid opt spec: %q' "$_spec" fi local _g_suffix='' _s_suffix='' _l_suffix='' case "$_spec_optarg" in '') _getopt_optarg["${_spec_long#'--'}"]=no ; ;; '['*']') _getopt_optarg["${_spec_long#'--'}"]=maybe; _g_suffix='::'; _s_suffix="$_spec_optarg" ; _l_suffix="[=${_spec_optarg#'['}";; *) _getopt_optarg["${_spec_long#'--'}"]=yes ; _g_suffix=':' ; _s_suffix=" $_spec_optarg"; _l_suffix="=${_spec_optarg}";; esac _getopt_long+=("${_spec_long#'--'}${_g_suffix}") local _h_short='' if [[ -n "$_spec_short" ]]; then _getopt_short+="${_spec_short#'-'}${_g_suffix}" _getopt_short2long["${_spec_short#'-'}"]="${_spec_long#'--'}" _h_short="${_spec_short}${_s_suffix}," fi local _h_long _h_desc _h_long="${_spec_long}${_l_suffix}" _h_desc="$(gettext -- "${_spec_desc}")" _help_specs+=($'\t\t'"${_h_short}"$'\t'"${_h_long}"$'\t\t'"${_h_desc}") done # Do the actual flag parsing. local _l_arg_str local _l_mode=() if ! _l_arg_str="$(IFS=','; getopt -n "${0##*/}" -o "$_getopt_short" -l "${_getopt_long[*]}" -- "$@")"; then _l_mode=('error') fi eval "set -- $_l_arg_str" local _l_longname while true; do if [[ $1 == -? && $1 != '--' ]]; then set -- "--${_getopt_short2long["${1#'-'}"]}" "${@:2}" fi case "$1" in --) shift break ;; --*) _l_longname=${1#'--'} if [[ ${_getopt_optarg["$_l_longname"]} == no ]]; then if type opt_visit_early:"$_l_longname" &>/dev/null; then opt_visit_early:"$_l_longname" fi shift 1 else if type opt_visit_early:"$_l_longname" &>/dev/null; then opt_visit_early:"$_l_longname" "$2" fi shift 2 fi ;; esac done eval "set -- $_l_arg_str" while true; do if [[ $1 == -? && $1 != '--' ]]; then set -- "--${_getopt_short2long["${1#'-'}"]}" "${@:2}" fi case "$1" in --) shift break ;; --*) _l_longname=${1#'--'} if [[ ${_getopt_optarg["$_l_longname"]} == no ]]; then opt_visit:"$_l_longname" shift 1 else opt_visit:"$_l_longname" "$2" shift 2 fi ;; esac done # Call the opt_positional() and opt_final:*() callbacks with # opt_mode set to a preliminary value. declare -g opt_mode opt_mode=${_l_mode[0]:-"$_a_mode_default"} if [[ $opt_mode != help && $opt_mode != version ]]; then opt_positional "$@" for _l_longname in "${!_getopt_optarg[@]}"; do if type opt_final:"$_l_longname" &>/dev/null; then opt_final:"$_l_longname" fi done fi # Set the opt_mode and opt_flaghelp outputs. # shellcheck disable=SC2034 opt_mode=${_l_mode[0]:-"$_a_mode_default"} declare -g opt_flaghelp # shellcheck disable=SC2034 opt_flaghelp=$(printf '%s\n' "${_help_specs[@]}" | column --table --keep-empty-lines --separator=$'\t' --output-separator=' ' | sed 's/\s*$//') } # Usage: argparse:setmode MODE # # Use argparse:setmode in your opt_* callback functions to set the # returned opt_mode. Don't use this for the 'error' mode, use # argparse:error for that. argparse:setmode() { _l_mode+=("$1") } # Usage: argparse:error PRINTF_STRING [ARGS...] # # Use argparse:error in your opt_* callback functions to signal that # the end-user passed in an invalid argument. argparse:error() { _l_mode=('error') osi.sh:error 0 "$@" }