#!/usr/bin/env bash
# vim: filetype=bash : -*- mode: sh; sh-shell: bash; -*-
###############################################################################
# GetOptLong: Getopt Library for Bash Script
# Copyright 2025 Office TECOLI, LLC <https://github.com/tecolicom/getoptlong>
# MIT License: See <https://opensource.org/licenses/MIT>
: ${GOL_VERSION:=0.5.0}
###############################################################################
# Check for nameref support (bash 4.3+)
declare -n > /dev/null 2>&1 || { echo "Does not support ${BASH_VERSION}" >&2 ; exit 1 ; }
_gol_warn() { echo "$@" >&2 ; }
_gol_die() { _gol_warn "$@" ; exit 1 ; }
_gol_opts() {
local _key="$1"
[[ $_key =~ ^[$_MARKS]$ ]] && _key+="$2" && shift
(($# == 2)) && _opts["$_key"]="$2" && return 0
[[ -v _opts[$_key] ]] && echo "${_opts[$_key]}" || return 1
}
_gol_alias() { _gol_opts "$_MK_ALIAS" "$@" ; }
_gol_saila() { _gol_opts "$_MK_SAILA" "$@" ; }
_gol_trig() { _gol_opts "$_MK_TRIG" "$@" ; }
_gol_hook() { _gol_opts "$_MK_HOOK" "$@" ; }
_gol_rule() { _gol_opts "$_MK_RULE" "$@" ; }
_gol_help() { _gol_opts "$_MK_HELP" "$@" ; }
_gol_ival() { _gol_opts "$_MK_INIT" "$@" ; }
_gol_dest() {
[[ $1 =~ ^[[:alpha:]] ]] || _gol_die "$1: variable name must start with alphabet"
_gol_opts "$_MK_DEST" "$@"
}
_gol_type() { [[ ${_opts["$1"]} =~ ^([^[:alnum:]]+) ]] && echo "${_MATCH[1]}" || _gol_die "$1: unexpected" ; }
_gol_debug() { [[ ${_opts["&DEBUG"]:-} ]] && _gol_warn DEBUG: "${@}" || : ; }
_gol_plusone() { [[ $1 =~ ^[0-9]+$ ]] && echo $(( $1 + 1 )) || echo 1 ; }
_gol_vstr() { printf "%03d." ${1//./ } ; }
# Main redirection function - sets up environment and delegates to implementation
_gol_redirect() { local _name ;
declare -n _opts=$GOL_OPTHASH
declare -n _MATCH=BASH_REMATCH
_gol_debug "${FUNCNAME[1]}(${@@Q})"
local _MARKS='><()&=#^.' _MK_ALIAS='>' _MK_SAILA='<' _MK_TRIG='(' _MK_HOOK=')' _MK_CONF='&' _MK_RULE='=' _MK_HELP='#' _MK_INIT='^' _MK_DEST='.' \
_IS_ANY='+:?@%' _IS_MOD='!>' _IS_REQ=':@%' _IS_FLAG='+' _IS_NEED=':' _IS_MAYB='?' _IS_LIST='@' _IS_HASH='%' _IS_HOOK='!' _IS_PASS='>' \
_CONFIG=(EXIT_ON_ERROR SILENT PERMUTE REQUIRE DEBUG PREFIX DELIM USAGE HELP)
for _name in "${_CONFIG[@]}" ; do declare _$_name="${_opts[&$_name]=}" ; done
"${FUNCNAME[1]}_" "$@"
}
gol_dump () { _gol_redirect "$@" ; }
gol_dump_() { local _key ;
if [[ ${1-} =~ ^(-a|--all)$ ]] ; then
for _key in "${!_opts[@]}" ; do
printf '[%s]=%s\n' "${_key}" "${_opts["$_key"]@Q}"
done | sort
fi
gol_vdump_
}
gol_vdump () { _gol_redirect "$@" ; }
gol_vdump_() { local _declare ; declare -A _seen ;
for _key in "${!_opts[@]}" ; do
[[ $_key =~ ^[$_MK_DEST](.*) ]] && [[ -z ${_seen[${_opts[$_key]}]-} ]] || continue
_declare=$(declare -p "${_opts[$_key]}" 2> /dev/null) && echo "$_declare"
_seen[${_opts[$_key]}]=1
done | sort
}
gol_init() { local _key ;
(( $# == 0 )) && { echo '(( ${#FUNCNAME[@]} > 0 )) && local GOL_OPTHASH OPTIND=1 || OPTIND=1' ; return ; }
declare -n _opts=$1
declare -A GOL_CONFIG=([PERMUTE]=GOL_ARGV [EXIT_ON_ERROR]=1 [DELIM]=$' \t,' [HELP]='help|h!#show HELP')
for _key in "${!GOL_CONFIG[@]}" ; do : ${_opts["&$_key"]="${GOL_CONFIG[$_key]}"} ; done
GOL_OPTHASH=$1
(( $# > 1 )) && gol_configure "${@:2}"
_gol_redirect
}
################################################################################
gol_init_() { local _key _aliases _alias _help ;
[[ $_REQUIRE && $(_gol_vstr $GOL_VERSION) < $(_gol_vstr $_REQUIRE) ]] && _gol_die "getoptlong version $GOL_VERSION < $_REQUIRE"
for _key in "${!_opts[@]}" ; do
[[ $_key =~ ^[$_MARKS] ]] && continue
_gol_init_entry "$_key"
done
if [[ $_HELP =~ ^( *)([[:alpha:]]+) ]] && _help=${_MATCH[2]} && [[ ! -v _opts[$_help] ]] ; then
_gol_init_entry "$_HELP"
declare -F $_help > /dev/null || eval "$_help() { getoptlong help ; exit ; }"
fi
return 0
}
_gol_init_entry() { local _entry="$1" _pass= _name _vname _dtype ;
[[ $_entry =~ ^([-_ \|[:alnum:]]+)([$_IS_ANY]*[$_IS_MOD]*[_[:alnum:]]*)( *)(=([if]|\(.*\)))?( *)(# *(.*[^[:space:]]))? ]] \
|| _gol_die "[$_entry] -- invalid"
local _names=${_MATCH[1]} _vtype=${_MATCH[2]} _type=${_MATCH[5]} _comment=${_MATCH[8]}
local _initial="${_opts[$_entry]-}"
IFS=$' \t|' read -a _aliases <<< ${_names}
_name=${_aliases[0]}
_gol_ival $_name "$_initial"
unset _opts["$_entry"]
[[ $_vtype =~ ([$_IS_ANY]*[$_IS_MOD]*)([_[:alnum:]]+)$ ]] && { _vtype=${_MATCH[1]} ; _vname=${_MATCH[2]} ; }
[[ $_vtype =~ $_IS_PASS ]] && { _vtype=${_vtype//$_IS_PASS/} ; _pass="$_IS_PASS" ; }
[[ $_vtype =~ $_IS_HOOK ]] && { _vtype=${_vtype//$_IS_HOOK/} ; _gol_hook $_name ${_vname-$_name} ; }
_gol_dest $_name ${_vname="${_PREFIX}${_name//-/_}"}
: ${_vtype:=$_IS_FLAG} ${_dtype:=${_pass:+$_IS_LIST}}
case ${_dtype:-$_vtype} in
"$_IS_MAYB")
[[ $_initial ]] && _gol_die "$_initial: optional parameter can't be initialized" ;;
"$_IS_LIST"|"$_IS_HASH")
[[ $_vtype == $_IS_LIST && ! -v $_vname ]] && declare -ga $_vname
[[ $_vtype == $_IS_HASH && ! -v $_vname ]] && declare -gA $_vname
if [[ $_initial =~ ^\(.*\)$ ]] ; then
eval "$_vname=$_initial"
else
[[ $_vtype == $_IS_LIST ]] && _gol_set_array $_vname ${_initial:+"$_initial"}
[[ $_vtype == $_IS_HASH ]] && [[ $_initial ]] && _gol_die "$_initial: invalid hash data"
fi
;;
"$_IS_NEED"|"$_IS_FLAG") _gol_value $_vname "$_initial" ;;
*) _gol_die "$_vtype: unknown option type" ;;
esac
_opts[$_name]="${_vtype}${_pass}${_vname}"
[[ $_type ]] && _gol_rule $_name "$_type"
for _alias in "${_aliases[@]:1}" ; do
_opts[$_alias]="${_opts[$_name]}"
_gol_alias $_alias $_name
done
_gol_saila $_name "${_aliases[*]:1}"
[[ $_comment ]] && _gol_help "$_name" "$_comment"
return 0
}
gol_configure () { _gol_redirect "$@" ; }
gol_configure_() { local _param _key _val ;
for _param in "$@" ; do
[[ $_param =~ ^[[:alnum:]] ]] || _gol_die "$_param -- invalid config parameter"
_key="${_MK_CONF}${_param%%=*}"
[[ $_param =~ =(.*) ]] && _val="${_MATCH[1]}" || _val=1
[[ -v _opts[$_key] ]] || _gol_die "$_param -- invalid config parameter"
_opts[$_key]="$_val"
done
return 0
}
gol_optstring_() { local _key _string ;
for _key in "${!_opts[@]}" ; do
[[ $_key =~ ^[[:alnum:]]$ ]] && _string+=$_key || continue
[[ ${_opts[$_key]} =~ [${_IS_REQ}] ]] && _string+=:
done
echo "${_SILENT:+:}${_string:- }-:"
}
gol_getopts () { _gol_redirect "$@" ; }
gol_getopts_() { local _non _optname _val _vtype _vname _name _callback _trigger _pass ;
local _opt="$1"; shift;
case $_opt in
[:?]) _callback=$(_gol_hook "$_opt") && [[ $_callback ]] && $_callback "$OPTARG"
[[ $_EXIT_ON_ERROR ]] && exit 1 || return 1 ;;
-) _gol_getopts_long "$@" || return $? ;;
*) _gol_getopts_short || return $? ;;
esac
[[ -v _val || $_pass ]] || _val="$(_gol_plusone "$(_gol_value $_vname)")"
_name=$(_gol_alias ${_optname:-$_opt}) || _name=${_optname:=$_opt}
_trigger="$(_gol_trig $_name)" && _gol_call_hook "$_trigger" "$_name"
[[ $_pass ]] && _gol_getopts_passthru || _gol_getopts_store
_callback="$(_gol_hook $_name)" && _gol_call_hook "$_callback" "$_name" "$_val"
return 0
}
_gol_call_hook() {
local _call=($1)
local exec=("${_call[0]}" "$2" "${_call[@]:1}" "${@:3}")
declare -F "${_call[0]}" > /dev/null \
&& "${exec[@]}" || _gol_die "callback function ${_call[0]}() is not defined"
}
_gol_getopts_long() { local _param ;
[[ $OPTARG =~ ^(no-)?([-_[:alnum:]]+)(=(.*))? ]] || _gol_die "$OPTARG: unrecognized option"
_non="${_MATCH[1]}" _optname="${_MATCH[2]}" _param="${_MATCH[3]}"
[[ $_param ]] && _val="${_MATCH[4]}"
[[ $(_gol_opts $_optname) =~ ^([$_IS_ANY]+)([$_IS_MOD]?)([_[:alnum:]]+) ]] || {
[[ $_EXIT_ON_ERROR ]] && _gol_die "no such option -- --$_optname" || return 2
}
_vtype=${_MATCH[1]} _pass="${_MATCH[2]}" _vname=${_MATCH[3]}
if [[ $_param ]] ; then
[[ $_vtype =~ [${_IS_REQ}${_IS_MAYB}] ]] || _gol_die "does not take an argument -- $_optname"
else
case $_vtype in
[$_IS_MAYB]) _val= ;;
[$_IS_REQ])
if [[ $_non ]] ; then _val= ; else
(( OPTIND > $# )) && _gol_die "option requires an argument -- $_optname"
_val="${@:$((OPTIND++)):1}"
fi ;;
*) [[ $_non ]] && _val= ;;
esac
fi
return 0
}
_gol_getopts_short() {
[[ ${_opts[$_opt]-} =~ ^([$_IS_ANY])([$_IS_MOD]?)([_[:alnum:]]+) ]] || {
[[ $_EXIT_ON_ERROR ]] && _gol_die "no such option -- -$_opt" || return 3
}
_vtype=${_MATCH[1]} _pass="${_MATCH[2]}" _vname=${_MATCH[3]}
[[ $_vtype =~ [${_IS_MAYB}${_IS_REQ}] ]] && _val="${OPTARG:-}"
return 0
}
_gol_getopts_store() { local _vals _v ;
local _check=$(_gol_rule $_name)
case $_vtype in
[$_IS_LIST]|[$_IS_HASH])
[[ $_val =~ $'\n' ]] && readarray -t _vals <<< ${_val%$'\n'} \
|| IFS="${_DELIM}" read -a _vals <<< ${_val}
for _v in "${_vals[@]}" ; do
[[ $_check ]] && _gol_validate "$_check" "$_v"
case $_vtype in
[$_IS_LIST]) _gol_set_array $_vname "$_v" ;;
[$_IS_HASH])
[[ $_v =~ = ]] && _gol_set_hash $_vname "${_v%%=*}" "${_v#*=}" \
|| _gol_set_hash $_vname "$_v" 1 ;;
esac
done
;;
*) [[ $_check ]] && _gol_validate "$_check" "$_val"
_gol_value $_vname "$_val" ;;
esac
}
_gol_getopts_passthru() { local _options=() ;
local _option=${_optname-$_opt}
(( ${#_option} > 1 )) && _options=(--${_non-}$_option) || _options=(-$_option)
[[ $_vtype =~ [$_IS_REQ] ]] && _options+=($_val)
_gol_set_array $_vname "${_options[@]}"
}
_gol_value() {
declare -n __target__="$1"
(( $# > 1 )) && __target__="$2" || echo "$__target__"
}
_gol_set_array() { declare -n __target__="$1" ; __target__+=("${@:2}") ; }
_gol_set_hash() { declare -n __target__="$1" ; __target__["$2"]="$3" ; }
_gol_validate() {
case $1 in
i) [[ "$2" =~ ^[-+]?[0-9]+$ ]] || _gol_die "$2: not an integer" ;;
f) [[ "$2" =~ ^[-+]?[0-9]*(\.[0-9]+)?$ ]] || _gol_die "$2: not a number" ;;
\(*) declare -a error=([1]="$2: invalid argument" [2]="$1: something wrong")
eval "[[ \"$2\" =~ $1 ]]" || _gol_die "${error[$?]}" ;;
*) _gol_die "$1: unknown validation pattern" ;;
esac
}
gol_callback () { _gol_redirect "$@" ; }
gol_callback_() {
local _setter=_gol_hook
case ${1-} in -b|--before) _setter=_gol_trig ; shift ;; esac
while (($# > 0)) ; do
local _name=$1 _callback=${2:-$1}
[[ $_callback =~ ^[_[:alnum:]] ]] || _callback=$_name
local args=($_callback)
$_setter "$_name" "${args[0]//-/_} ${args[@]:1}"
shift $(( $# >= 2 ? 2 : 1 ))
done
return 0
}
gol_help () { _gol_redirect "$@" ; }
gol_help_() {
(( $# < 2 )) && { _gol_show_help "$@" ; return 0 ; }
while (($# > 1)) ; do _gol_help "$1" "$2" ; shift 2 ; done
}
_gol_show_help() { local _key _aliases _init= _default= _column _flag _msg ;
echo "${1:-${_USAGE:-$(basename $0) [ options ] args}}"
_column=($(command -v column)) && _column+=(-s $'\t' -t) || _column=(cat)
for _key in "${!_opts[@]}" ; do
_aliases="$(_gol_saila "$_key")" || continue
_msg="$(_gol_help $_key)" || {
_init="$(_gol_ival $_key)" && _default="${_init:+ (default:$_init)}"
[[ $_init =~ ^[0-9]+$ ]] && _flag=bump || _flag=enable
[[ "${_opts[$_key]}" =~ ([^[:alnum:]]+)(.*) ]] || _gol_die "${_opts[$_key]}: invalid entry"
case "${_MATCH[1]}" in
*[$_IS_PASS]) _msg="passthrough to ${_MATCH[2]^^}" ;;
*[$_IS_FLAG]) _msg="$_flag ${_key^^}$_default" ;;
*[$_IS_NEED]) _msg="set ${_key^^}$_default" ;;
*[$_IS_LIST]) _msg="add item(s) to ${_key^^}" ;;
*[$_IS_HASH]) _msg="set KEY=VALUE(s) in ${_key^^}" ;;
*[$_IS_MAYB]) _msg="enable/set ${_key^^}" ;;
esac
}
printf ' %s\t%1s\t%s\n' "$(_gol_optize $_key)" "$(_gol_optize $_aliases)" "$_msg"
done | sort | "${_column[@]}"
}
_gol_optize() { local _name _opt _optlist _eq ;
for _name in "$@"; do
(( ${#_name} > 1 )) && _opt=--$_name _eq='=' || _opt=-$_name _eq=
case "$(_gol_type $_name)" in
[$_IS_NEED]) _opt+="$_eq#" ;;
[$_IS_LIST]) _opt+="$_eq#[,#]" ;;
[$_IS_HASH]) _opt+="$_eq#=#" ;;
[$_IS_MAYB]) (( ${#_name} > 1 )) && _opt+="[=#]" ;;
esac
_optlist+=("$_opt")
done
printf '%s\n' "${_optlist[*]}"
}
gol_parse () { _gol_redirect "$@" ; }
gol_parse_() { local gol_OPT SAVEARG=() SAVEIND= ;
local optstring="$(gol_optstring_)" ; _gol_debug "OPTSTRING=$optstring" ;
for (( OPTIND=1 ; OPTIND <= $# ; OPTIND++ )) ; do
while getopts "$optstring" gol_OPT ; do
gol_getopts_ "$gol_OPT" "$@" || {
_gol_debug "SAVE ERROR: ${@:$((OPTIND-1)):1}"
SAVEARG+=("${@:$((OPTIND-1)):1}")
}
done
: ${SAVEIND:=$OPTIND}
[[ ! $_PERMUTE || $OPTIND > $# || ${@:$(($OPTIND-1)):1} == -- ]] && break
_gol_debug "SAVE PARAM: ${!OPTIND}"
SAVEARG+=("${!OPTIND}")
done
set -- "${SAVEARG[@]}" "${@:$OPTIND}"
OPTIND=${SAVEIND:-$OPTIND}
_gol_debug "ARGV=(${@@Q})"
[[ $_PERMUTE ]] && { declare -n _gol_argv=$_PERMUTE ; _gol_argv=("$@") ; }
return 0
}
gol_set () { _gol_redirect "$@" ; }
gol_set_() {
[[ $_PERMUTE ]] && printf 'set -- "${%s[@]}"\n' "$_PERMUTE" \
|| echo 'set -- "${@:$OPTIND}"'
}
# Main entry point - dispatch to appropriate subcommand
getoptlong () {
case $1 in
init|parse|set|configure|getopts|callback|dump|help) gol_$1 "${@:2}" ;;
version) echo ${GOL_VERSION} ;;
*) _gol_die "unknown subcommand -- $1" ;;
esac
}
# Auto-initialization if first argument is an associative array
if [[ $(declare -p "${1-}" 2> /dev/null) =~ ^declare\ -A ]] ; then
gol_init "$1" && shift && gol_parse "$@" && eval "$(gol_set)"
fi