219 lines
6.2 KiB
Bash
219 lines
6.2 KiB
Bash
#!/hint/bash
|
|
# Copyright (C) 2018, 2024 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 : an array of strings of the format:
|
|
#
|
|
# -s : --long : VALNAME : human description
|
|
#
|
|
# Fields are ':'-separated; whitespace is ignored;
|
|
# ':'s in a value may be escaped with a backslash.
|
|
#
|
|
# The "-s" short flag name is optional.
|
|
#
|
|
# The "--long" long flag name is *required*.
|
|
#
|
|
# The "VALNAME" controls whether the flag takes an
|
|
# argument; empty means no argument, wrapped in
|
|
# "[brackets]" means the argument is optional,
|
|
# present but not wrapped in brackets means the
|
|
# argument is required.
|
|
#
|
|
# 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 "$@"
|
|
}
|