#!/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) # print "Try '%q --help' for more information" "${0##*/}" >&2 # return $EXIT_INVALIDARGUMENT # ;; # version) # print "%s %s" "$NAME" "$VERSION" # return $EXIT_SUCCESS # ;; # help) # print 'Usage: %s [OPTIONS]' "${0##*/}" # print 'One line description' # echo # print 'Longer description.' # echo # print 'OPTIONS:' # echo "${opt_flaghelp}" # return $EXIT_SUCCESS # ;; # MYDEFAULT) # ... # # Inputs: # # variable: opt_specs # # Callbacks: # # functions: opt_visit:${longname} [OPTARG] # function: opt_positional [POSITONAL_ARGS...] # functions: opt_final:${longname} # # Outputs: # # variable: opt_mode # variable: opt_flaghelp argparse() { local IFS 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; } # shellcheck disable=SC2317 opt_final:help() { :; } opt_specs+=('-V : --version : : output version information') # shellcheck disable=SC2317 opt_visit:version() { argparse:setmode version; } # shellcheck disable=SC2317 opt_final: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 IFS=':' # shellcheck disable=SC2162 # I want int to mangle backslashes. read _spec_short _spec_long _spec_optarg _spec_desc <<<"${_spec}" if [[ $_spec_long != --* ]]; then error "$EXIT_FAILURE" 'Internal error. The programmer writing this tool screwed up.' 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 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 opt_final:"$_l_longname" 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') error 0 "$@" }