osi-tools/lib/argparse.sh

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 "$@"
}