From 8862ba199910773a446e85463f2296a2c9d0ee40 Mon Sep 17 00:00:00 2001 From: Zane Duffield Date: Wed, 14 Jan 2026 11:13:36 +1100 Subject: [PATCH 1/7] Convert tabs to spaces The script used mixed indentation, meaning that it only made sense to read when the tab width was set properly (to 8). Using consistent indentation makes the script easier to read. --- cmdarg.sh | 384 +++++++++++++++++++++++++++--------------------------- 1 file changed, 192 insertions(+), 192 deletions(-) diff --git a/cmdarg.sh b/cmdarg.sh index 282b05c..ff00361 100644 --- a/cmdarg.sh +++ b/cmdarg.sh @@ -33,14 +33,14 @@ function cmdarg local shortopt=${1:0:1} local key="$2" if [[ "$shortopt" == "h" ]]; then - echo "-h is reserved for cmdarg usage" >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 + echo "-h is reserved for cmdarg usage" >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 fi if [[ "$(type -t cmdarg_$key)" != "" ]] || \ - [[ "${CMDARG_FLAGS[$shortopt]}" != "" ]] || \ - [[ "${CMDARG_TYPES[$key]}" != "" ]]; then - echo "command line key '$shortopt ($key)' is reserved by cmdarg or defined twice" >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 + [[ "${CMDARG_FLAGS[$shortopt]}" != "" ]] || \ + [[ "${CMDARG_TYPES[$key]}" != "" ]]; then + echo "command line key '$shortopt ($key)' is reserved by cmdarg or defined twice" >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 fi declare -A argtypemap @@ -48,31 +48,31 @@ function cmdarg argtypemap['?']=$CMDARG_FLAG_OPTARG local argtype=${1:1:1} if [[ "$argtype" =~ ^[\[{]$ ]]; then - echo "Flags required [:?] when specifying Hash or Array arguments (${argtype})" >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 + echo "Flags required [:?] when specifying Hash or Array arguments (${argtype})" >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 elif [[ "$argtype" != "" ]]; then - CMDARG_FLAGS[$shortopt]=${argtypemap["$argtype"]} - if [[ "${1:2:4}" == "[]" ]]; then - declare -p ${key} >/dev/null 2>&1 - if [[ $? -ne 0 ]]; then - echo 'Array variable '"${key}"' does not exist. Array variables MUST be declared by the user!' >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 - fi - CMDARG_TYPES[$key]=$CMDARG_TYPE_ARRAY - elif [[ "${1:2:4}" == "{}" ]]; then - declare -p ${key} >/dev/null 2>&1 - if [[ $? -ne 0 ]]; then - echo 'Hash variable '"${key}"' does not exist. Hash variables MUST be declared by the user!' >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 - fi - CMDARG_TYPES[$key]=$CMDARG_TYPE_HASH - else - CMDARG_TYPES[$key]=$CMDARG_TYPE_STRING - fi + CMDARG_FLAGS[$shortopt]=${argtypemap["$argtype"]} + if [[ "${1:2:4}" == "[]" ]]; then + declare -p ${key} >/dev/null 2>&1 + if [[ $? -ne 0 ]]; then + echo 'Array variable '"${key}"' does not exist. Array variables MUST be declared by the user!' >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 + fi + CMDARG_TYPES[$key]=$CMDARG_TYPE_ARRAY + elif [[ "${1:2:4}" == "{}" ]]; then + declare -p ${key} >/dev/null 2>&1 + if [[ $? -ne 0 ]]; then + echo 'Hash variable '"${key}"' does not exist. Hash variables MUST be declared by the user!' >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 + fi + CMDARG_TYPES[$key]=$CMDARG_TYPE_HASH + else + CMDARG_TYPES[$key]=$CMDARG_TYPE_STRING + fi else - CMDARG_FLAGS[$shortopt]=$CMDARG_FLAG_NOARG - CMDARG_TYPES[$key]=$CMDARG_TYPE_BOOLEAN - cmdarg_cfg[$key]=false + CMDARG_FLAGS[$shortopt]=$CMDARG_FLAG_NOARG + CMDARG_TYPES[$key]=$CMDARG_TYPE_BOOLEAN + cmdarg_cfg[$key]=false fi CMDARG["$shortopt"]=$2 @@ -80,16 +80,16 @@ function cmdarg CMDARG_DESC["$shortopt"]=$3 CMDARG_DEFAULT["$shortopt"]=${4:-} if [[ ${CMDARG_FLAGS[$shortopt]} -eq $CMDARG_FLAG_REQARG ]] && [[ "${4:-}" == "" ]]; then - CMDARG_REQUIRED+=($shortopt) + CMDARG_REQUIRED+=($shortopt) else - CMDARG_OPTIONAL+=($shortopt) + CMDARG_OPTIONAL+=($shortopt) fi cmdarg_cfg["$2"]="${4:-}" local validatorfunc validatorfunc=${5:-} if [[ "$validatorfunc" != "" ]] && [[ "$(declare -F $validatorfunc)" == "" ]]; then - echo "Validators must be bash functions accepting 1 argument (not '$validatorfunc')" >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 + echo "Validators must be bash functions accepting 1 argument (not '$validatorfunc')" >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 fi CMDARG_VALIDATORS["$shortopt"]="$validatorfunc" CMDARG_GETOPTLIST="${CMDARG_GETOPTLIST}$1" @@ -103,9 +103,9 @@ function cmdarg_info # local flags="header|copyright|footer|author" if [[ ! "$1" =~ $flags ]]; then - echo "cmdarg_info " >&2 - echo "Where is one of $flags" >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 + echo "cmdarg_info " >&2 + echo "Where is one of $flags" >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 fi CMDARG_INFO["$1"]=$2 } @@ -136,25 +136,25 @@ function cmdarg_describe_default set +u if [ "${default}" != "" ]; then - default="(Default \"${default}\")" + default="(Default \"${default}\")" fi case ${argtype} in - $CMDARG_TYPE_STRING) - echo "-${opt},--${longopt} v : String. ${description} ${default}" - ;; - $CMDARG_TYPE_BOOLEAN) - echo "-${opt},--${longopt} : Boolean. ${description} ${default}" - ;; - $CMDARG_TYPE_ARRAY) - echo "-${opt},--${longopt} v[, ...] : Array. ${description}. Pass this argument multiple times for multiple values. ${default}" - ;; - $CMDARG_TYPE_HASH) - echo "-${opt},--${longopt} k=v{, ..} : Hash. ${description}. Pass this argument multiple times for multiple key/value pairs. ${default}" - ;; - *) - echo "Unable to return string description for ${opt}; unknown type ${argtype}" >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 - ;; + $CMDARG_TYPE_STRING) + echo "-${opt},--${longopt} v : String. ${description} ${default}" + ;; + $CMDARG_TYPE_BOOLEAN) + echo "-${opt},--${longopt} : Boolean. ${description} ${default}" + ;; + $CMDARG_TYPE_ARRAY) + echo "-${opt},--${longopt} v[, ...] : Array. ${description}. Pass this argument multiple times for multiple values. ${default}" + ;; + $CMDARG_TYPE_HASH) + echo "-${opt},--${longopt} k=v{, ..} : Hash. ${description}. Pass this argument multiple times for multiple key/value pairs. ${default}" + ;; + *) + echo "Unable to return string description for ${opt}; unknown type ${argtype}" >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 + ;; esac } @@ -170,19 +170,19 @@ function cmdarg_usage echo local key if [[ "${#CMDARG_REQUIRED[@]}" -ne 0 ]]; then - echo "Required Arguments:" - for key in "${CMDARG_REQUIRED[@]}" - do - echo " $(cmdarg_describe $key)" - done - echo + echo "Required Arguments:" + for key in "${CMDARG_REQUIRED[@]}" + do + echo " $(cmdarg_describe $key)" + done + echo fi if [[ "${#CMDARG_OPTIONAL[@]}" -ne 0 ]]; then - echo "Optional Arguments": - for key in "${CMDARG_OPTIONAL[@]}" - do - echo " $(cmdarg_describe $key)" - done + echo "Optional Arguments": + for key in "${CMDARG_OPTIONAL[@]}" + do + echo " $(cmdarg_describe $key)" + done fi echo echo "${CMDARG_INFO['footer']}" @@ -199,10 +199,10 @@ function cmdarg_validate local shortopt=${CMDARG_REV[$longopt]} if [ "${CMDARG_VALIDATORS[$shortopt]}" != "" ]; then ( ${CMDARG_VALIDATORS[${shortopt}]} "$value" "$hashkey") - if [ $? -ne 0 ]; then - echo "Invalid value for -$shortopt : ${value}" >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 - fi + if [ $? -ne 0 ]; then + echo "Invalid value for -$shortopt : ${value}" >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 + fi fi return 0 } @@ -215,35 +215,35 @@ function cmdarg_set_opt set +u case ${CMDARG_TYPES[$key]} in - $CMDARG_TYPE_STRING) - cmdarg_cfg[$key]=$arg - cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 - ;; - $CMDARG_TYPE_BOOLEAN) - cmdarg_cfg[$key]=true - cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 - ;; - $CMDARG_TYPE_ARRAY) - local arrname="${key}" - local str='${#'"$arrname"'[@]}' - local prevlen=$(eval "echo $str") - eval "${arrname}[$((prevlen + 1))]=\"$arg\"" - cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 - ;; - $CMDARG_TYPE_HASH) - local k=${arg%%=*} - local v=${arg#*=} - if [[ "$k" == "$arg" ]] && [[ "$v" == "$arg" ]] && [[ "$k" == "$v" ]]; then - echo "Malformed hash argument: $arg" >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 - fi - eval "$key[\$k]=\$v" - cmdarg_validate "$key" "$v" "$k" || ${CMDARG_ERROR_BEHAVIOR} 1 - ;; - *) - echo "Unable to return string description for ${key}; unknown type ${CMDARG_TYPES[$key]}" >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 - ;; + $CMDARG_TYPE_STRING) + cmdarg_cfg[$key]=$arg + cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 + ;; + $CMDARG_TYPE_BOOLEAN) + cmdarg_cfg[$key]=true + cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 + ;; + $CMDARG_TYPE_ARRAY) + local arrname="${key}" + local str='${#'"$arrname"'[@]}' + local prevlen=$(eval "echo $str") + eval "${arrname}[$((prevlen + 1))]=\"$arg\"" + cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 + ;; + $CMDARG_TYPE_HASH) + local k=${arg%%=*} + local v=${arg#*=} + if [[ "$k" == "$arg" ]] && [[ "$v" == "$arg" ]] && [[ "$k" == "$v" ]]; then + echo "Malformed hash argument: $arg" >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 + fi + eval "$key[\$k]=\$v" + cmdarg_validate "$key" "$v" "$k" || ${CMDARG_ERROR_BEHAVIOR} 1 + ;; + *) + echo "Unable to return string description for ${key}; unknown type ${CMDARG_TYPES[$key]}" >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 + ;; esac return 0 } @@ -255,25 +255,25 @@ function cmdarg_check_empty local type=${CMDARG_TYPES[$longopt]} case $type in - $CMDARG_TYPE_STRING) + $CMDARG_TYPE_STRING) echo ${cmdarg_cfg[$longopt]} ;; - $CMDARG_TYPE_BOOLEAN) - echo ${cmdarg_cfg[$longopt]} - ;; - $CMDARG_TYPE_ARRAY) - local arrname="${longopt}" - local lval='${!'"${arrname}"'[@]}' - eval "echo $lval" - ;; - $CMDARG_TYPE_HASH) - local arrname="${longopt}" - local lval='${!'"${arrname}"'[@]}' - eval "echo $lval" - ;; - *) - echo "${cmdarg_cfg[$longopt]}" - ;; + $CMDARG_TYPE_BOOLEAN) + echo ${cmdarg_cfg[$longopt]} + ;; + $CMDARG_TYPE_ARRAY) + local arrname="${longopt}" + local lval='${!'"${arrname}"'[@]}' + eval "echo $lval" + ;; + $CMDARG_TYPE_HASH) + local arrname="${longopt}" + local lval='${!'"${arrname}"'[@]}' + eval "echo $lval" + ;; + *) + echo "${cmdarg_cfg[$longopt]}" + ;; esac } @@ -288,61 +288,61 @@ function cmdarg_parse local parsing=0 while [[ $# -ne 0 ]]; do - local optarg="" - local opt="" - local longopt="" - local fullopt=$1 - local is_equals_arg=1 + local optarg="" + local opt="" + local longopt="" + local fullopt=$1 + local is_equals_arg=1 - shift - if [[ "${fullopt}" =~ ^(--[a-zA-Z0-9_\-]+|^-[a-zA-Z0-9])= ]]; then - local tmpopt=$fullopt - fullopt=${tmpopt%%=*} - optarg=${tmpopt##*=} - is_equals_arg=0 - fi + shift + if [[ "${fullopt}" =~ ^(--[a-zA-Z0-9_\-]+|^-[a-zA-Z0-9])= ]]; then + local tmpopt=$fullopt + fullopt=${tmpopt%%=*} + optarg=${tmpopt##*=} + is_equals_arg=0 + fi - if [[ "$fullopt" == "--" ]] && [[ $parsing -eq 0 ]]; then - cmdarg_argv+=($@) - break - elif [[ "${fullopt:0:2}" == "--" ]]; then - longopt=${fullopt:2} - opt=${CMDARG_REV[$longopt]} - elif [[ "${fullopt:0:1}" == "-" ]] && [[ ${#fullopt} -eq 2 ]]; then - opt=${fullopt:1} - longopt=${CMDARG[$opt]} - elif [[ "${fullopt:0:1}" != "-" ]]; then - cmdarg_argv+=("$fullopt") - continue - else - echo "Malformed argument: ${fullopt}" >&2 - echo "While parsing: $@" >&2 - ${cmdarg_helpers['usage']} >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 - fi + if [[ "$fullopt" == "--" ]] && [[ $parsing -eq 0 ]]; then + cmdarg_argv+=($@) + break + elif [[ "${fullopt:0:2}" == "--" ]]; then + longopt=${fullopt:2} + opt=${CMDARG_REV[$longopt]} + elif [[ "${fullopt:0:1}" == "-" ]] && [[ ${#fullopt} -eq 2 ]]; then + opt=${fullopt:1} + longopt=${CMDARG[$opt]} + elif [[ "${fullopt:0:1}" != "-" ]]; then + cmdarg_argv+=("$fullopt") + continue + else + echo "Malformed argument: ${fullopt}" >&2 + echo "While parsing: $@" >&2 + ${cmdarg_helpers['usage']} >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 + fi - if [[ "$opt" == "h" ]] || [[ "$longopt" == "help" ]]; then - ${cmdarg_helpers['usage']} >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 - fi + if [[ "$opt" == "h" ]] || [[ "$longopt" == "help" ]]; then + ${cmdarg_helpers['usage']} >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 + fi - if [[ $is_equals_arg -eq 1 ]]; then - if [[ ${CMDARG_FLAGS[$opt]} -eq ${CMDARG_FLAG_REQARG} ]] || \ - [[ ${CMDARG_FLAGS[$opt]} -eq ${CMDARG_FLAG_OPTARG} ]]; then - optarg=$1 - shift - fi - fi + if [[ $is_equals_arg -eq 1 ]]; then + if [[ ${CMDARG_FLAGS[$opt]} -eq ${CMDARG_FLAG_REQARG} ]] || \ + [[ ${CMDARG_FLAGS[$opt]} -eq ${CMDARG_FLAG_OPTARG} ]]; then + optarg=$1 + shift + fi + fi - if [ ${CMDARG["${opt}"]+abc} ]; then - cmdarg_set_opt "${CMDARG[$opt]}" "$optarg" - local rc=$? - failed=$((failed + $rc)) - else - echo "Unknown argument or invalid value : -${opt} | --${longopt}" >&2 - ${cmdarg_helpers['usage']} >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 - fi + if [ ${CMDARG["${opt}"]+abc} ]; then + cmdarg_set_opt "${CMDARG[$opt]}" "$optarg" + local rc=$? + failed=$((failed + $rc)) + else + echo "Unknown argument or invalid value : -${opt} | --${longopt}" >&2 + ${cmdarg_helpers['usage']} >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 + fi done # --- Don't ${CMDARG_ERROR_BEHAVIOR} early during validation, tell the user @@ -351,19 +351,19 @@ function cmdarg_parse local key for key in "${CMDARG_REQUIRED[@]}" do - if [[ "$(cmdarg_check_empty $key)" == "" ]]; then - missing="${missing} -${key}" - failed=$((failed + 1)) - fi + if [[ "$(cmdarg_check_empty $key)" == "" ]]; then + missing="${missing} -${key}" + failed=$((failed + 1)) + fi done if [ $failed -gt 0 ]; then - if [[ "$missing" != "" ]]; then - echo "Missing arguments : ${missing}" >&2 - fi - echo >&2 - ${cmdarg_helpers['usage']} >&2 - ${CMDARG_ERROR_BEHAVIOR} 1 + if [[ "$missing" != "" ]]; then + echo "Missing arguments : ${missing}" >&2 + fi + echo >&2 + ${cmdarg_helpers['usage']} >&2 + ${CMDARG_ERROR_BEHAVIOR} 1 fi } @@ -374,9 +374,9 @@ function cmdarg_traceback local FRAMES=${#BASH_LINENO[@]} # FRAMES-2 skips main, the last one in arrays for ((i=FRAMES-2; i>=1; i--)); do - echo ' File' \"${BASH_SOURCE[i+1]}\", line ${BASH_LINENO[i]}, probably in ${FUNCNAME[i+1]} >&2 - # Grab the source code of the line - sed -n "${BASH_LINENO[i]}{s/^/ /;p}" "${BASH_SOURCE[i+1]}" >&2 + echo ' File' \"${BASH_SOURCE[i+1]}\", line ${BASH_LINENO[i]}, probably in ${FUNCNAME[i+1]} >&2 + # Grab the source code of the line + sed -n "${BASH_LINENO[i]}{s/^/ /;p}" "${BASH_SOURCE[i+1]}" >&2 done echo " Error: $LASTERR" unset FRAMES @@ -394,20 +394,20 @@ function cmdarg_dump for key in ${!cmdarg_cfg[@]} do - repr="${key}:${CMDARG_TYPES[$key]}" - if [[ ${CMDARG_TYPES[$key]} == $CMDARG_TYPE_ARRAY ]] || [[ ${CMDARG_TYPES[$key]} == $CMDARG_TYPE_HASH ]] ; then - arrname="${key}" - echo "${repr} => " - keys='${!'"$arrname"'[@]}' - for idx in $(eval "echo $keys") - do - ref='${'"$arrname"'[$idx]}' - value=$(eval "echo $ref") - echo " ${idx} => $value" - done - else - echo "${repr} => ${cmdarg_cfg[$key]}" - fi + repr="${key}:${CMDARG_TYPES[$key]}" + if [[ ${CMDARG_TYPES[$key]} == $CMDARG_TYPE_ARRAY ]] || [[ ${CMDARG_TYPES[$key]} == $CMDARG_TYPE_HASH ]] ; then + arrname="${key}" + echo "${repr} => " + keys='${!'"$arrname"'[@]}' + for idx in $(eval "echo $keys") + do + ref='${'"$arrname"'[$idx]}' + value=$(eval "echo $ref") + echo " ${idx} => $value" + done + else + echo "${repr} => ${cmdarg_cfg[$key]}" + fi done } @@ -420,7 +420,7 @@ function cmdarg_purge arrays="$arrays CMDARG_FLAGS CMDARG_TYPES cmdarg_argv cmdarg_helpers" for arr in $arrays do - eval "$arr=()" + eval "$arr=()" done cmdarg_helpers['describe']=cmdarg_describe_default cmdarg_helpers['usage']=cmdarg_usage From ad35f698ea4b3beab480fbb30e87c1c1ee82fcf3 Mon Sep 17 00:00:00 2001 From: Zane Duffield Date: Wed, 14 Jan 2026 11:15:44 +1100 Subject: [PATCH 2/7] Quote variables to avoid word splitting a glob matching In all cases here, the contents of the variables are single words and do not contain special glob characters, so it's mostly a change for consistency and best practice. --- cmdarg.sh | 54 +++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/cmdarg.sh b/cmdarg.sh index ff00361..976fb7c 100644 --- a/cmdarg.sh +++ b/cmdarg.sh @@ -36,7 +36,7 @@ function cmdarg echo "-h is reserved for cmdarg usage" >&2 ${CMDARG_ERROR_BEHAVIOR} 1 fi - if [[ "$(type -t cmdarg_$key)" != "" ]] || \ + if [[ "$(type -t cmdarg_"$key")" != "" ]] || \ [[ "${CMDARG_FLAGS[$shortopt]}" != "" ]] || \ [[ "${CMDARG_TYPES[$key]}" != "" ]]; then echo "command line key '$shortopt ($key)' is reserved by cmdarg or defined twice" >&2 @@ -53,14 +53,14 @@ function cmdarg elif [[ "$argtype" != "" ]]; then CMDARG_FLAGS[$shortopt]=${argtypemap["$argtype"]} if [[ "${1:2:4}" == "[]" ]]; then - declare -p ${key} >/dev/null 2>&1 + declare -p "${key}" >/dev/null 2>&1 if [[ $? -ne 0 ]]; then echo 'Array variable '"${key}"' does not exist. Array variables MUST be declared by the user!' >&2 ${CMDARG_ERROR_BEHAVIOR} 1 fi CMDARG_TYPES[$key]=$CMDARG_TYPE_ARRAY elif [[ "${1:2:4}" == "{}" ]]; then - declare -p ${key} >/dev/null 2>&1 + declare -p "${key}" >/dev/null 2>&1 if [[ $? -ne 0 ]]; then echo 'Hash variable '"${key}"' does not exist. Hash variables MUST be declared by the user!' >&2 ${CMDARG_ERROR_BEHAVIOR} 1 @@ -80,14 +80,14 @@ function cmdarg CMDARG_DESC["$shortopt"]=$3 CMDARG_DEFAULT["$shortopt"]=${4:-} if [[ ${CMDARG_FLAGS[$shortopt]} -eq $CMDARG_FLAG_REQARG ]] && [[ "${4:-}" == "" ]]; then - CMDARG_REQUIRED+=($shortopt) + CMDARG_REQUIRED+=("$shortopt") else - CMDARG_OPTIONAL+=($shortopt) + CMDARG_OPTIONAL+=("$shortopt") fi cmdarg_cfg["$2"]="${4:-}" local validatorfunc validatorfunc=${5:-} - if [[ "$validatorfunc" != "" ]] && [[ "$(declare -F $validatorfunc)" == "" ]]; then + if [[ "$validatorfunc" != "" ]] && [[ "$(declare -F "$validatorfunc")" == "" ]]; then echo "Validators must be bash functions accepting 1 argument (not '$validatorfunc')" >&2 ${CMDARG_ERROR_BEHAVIOR} 1 fi @@ -120,7 +120,7 @@ function cmdarg_describe local flags="${CMDARG_FLAGS[$opt]}" local validator="${CMDARG_VALIDATORS[$opt]}" - ${cmdarg_helpers['describe']} $longopt $opt $argtype "${default}" "${description}" "${flags}" "${validator}" + ${cmdarg_helpers['describe']} "$longopt" "$opt" "$argtype" "${default}" "${description}" "${flags}" "${validator}" } function cmdarg_describe_default @@ -139,16 +139,16 @@ function cmdarg_describe_default default="(Default \"${default}\")" fi case ${argtype} in - $CMDARG_TYPE_STRING) + "$CMDARG_TYPE_STRING") echo "-${opt},--${longopt} v : String. ${description} ${default}" ;; - $CMDARG_TYPE_BOOLEAN) + "$CMDARG_TYPE_BOOLEAN") echo "-${opt},--${longopt} : Boolean. ${description} ${default}" ;; - $CMDARG_TYPE_ARRAY) + "$CMDARG_TYPE_ARRAY") echo "-${opt},--${longopt} v[, ...] : Array. ${description}. Pass this argument multiple times for multiple values. ${default}" ;; - $CMDARG_TYPE_HASH) + "$CMDARG_TYPE_HASH") echo "-${opt},--${longopt} k=v{, ..} : Hash. ${description}. Pass this argument multiple times for multiple key/value pairs. ${default}" ;; *) @@ -164,7 +164,7 @@ function cmdarg_usage # cmdarg_usage # # Prints a very helpful usage message about the current program. - echo "$(basename $0) ${CMDARG_INFO['copyright']} : ${CMDARG_INFO['author']}" + echo "$(basename "$0") ${CMDARG_INFO['copyright']} : ${CMDARG_INFO['author']}" echo echo "${CMDARG_INFO['header']}" echo @@ -173,7 +173,7 @@ function cmdarg_usage echo "Required Arguments:" for key in "${CMDARG_REQUIRED[@]}" do - echo " $(cmdarg_describe $key)" + echo " $(cmdarg_describe "$key")" done echo fi @@ -181,7 +181,7 @@ function cmdarg_usage echo "Optional Arguments": for key in "${CMDARG_OPTIONAL[@]}" do - echo " $(cmdarg_describe $key)" + echo " $(cmdarg_describe "$key")" done fi echo @@ -215,22 +215,22 @@ function cmdarg_set_opt set +u case ${CMDARG_TYPES[$key]} in - $CMDARG_TYPE_STRING) + "$CMDARG_TYPE_STRING") cmdarg_cfg[$key]=$arg cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 ;; - $CMDARG_TYPE_BOOLEAN) + "$CMDARG_TYPE_BOOLEAN") cmdarg_cfg[$key]=true cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 ;; - $CMDARG_TYPE_ARRAY) + "$CMDARG_TYPE_ARRAY") local arrname="${key}" local str='${#'"$arrname"'[@]}' local prevlen=$(eval "echo $str") eval "${arrname}[$((prevlen + 1))]=\"$arg\"" cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 ;; - $CMDARG_TYPE_HASH) + "$CMDARG_TYPE_HASH") local k=${arg%%=*} local v=${arg#*=} if [[ "$k" == "$arg" ]] && [[ "$v" == "$arg" ]] && [[ "$k" == "$v" ]]; then @@ -255,18 +255,18 @@ function cmdarg_check_empty local type=${CMDARG_TYPES[$longopt]} case $type in - $CMDARG_TYPE_STRING) - echo ${cmdarg_cfg[$longopt]} + "$CMDARG_TYPE_STRING") + echo "${cmdarg_cfg[$longopt]}" ;; - $CMDARG_TYPE_BOOLEAN) - echo ${cmdarg_cfg[$longopt]} + "$CMDARG_TYPE_BOOLEAN") + echo "${cmdarg_cfg[$longopt]}" ;; - $CMDARG_TYPE_ARRAY) + "$CMDARG_TYPE_ARRAY") local arrname="${longopt}" local lval='${!'"${arrname}"'[@]}' eval "echo $lval" ;; - $CMDARG_TYPE_HASH) + "$CMDARG_TYPE_HASH") local arrname="${longopt}" local lval='${!'"${arrname}"'[@]}' eval "echo $lval" @@ -351,7 +351,7 @@ function cmdarg_parse local key for key in "${CMDARG_REQUIRED[@]}" do - if [[ "$(cmdarg_check_empty $key)" == "" ]]; then + if [[ "$(cmdarg_check_empty "$key")" == "" ]]; then missing="${missing} -${key}" failed=$((failed + 1)) fi @@ -392,10 +392,10 @@ function cmdarg_dump local ref local value - for key in ${!cmdarg_cfg[@]} + for key in "${!cmdarg_cfg[@]}" do repr="${key}:${CMDARG_TYPES[$key]}" - if [[ ${CMDARG_TYPES[$key]} == $CMDARG_TYPE_ARRAY ]] || [[ ${CMDARG_TYPES[$key]} == $CMDARG_TYPE_HASH ]] ; then + if [[ ${CMDARG_TYPES[$key]} == "$CMDARG_TYPE_ARRAY" ]] || [[ ${CMDARG_TYPES[$key]} == "$CMDARG_TYPE_HASH" ]] ; then arrname="${key}" echo "${repr} => " keys='${!'"$arrname"'[@]}' From 1e31ed1998f596b43e256ba4f28957e7935b6486 Mon Sep 17 00:00:00 2001 From: Zane Duffield Date: Wed, 14 Jan 2026 11:16:54 +1100 Subject: [PATCH 3/7] Quote array expansion when appending to cmdarg_argv Without this change, excess arguments are split and glob-expanded when appended to the cmdarg_argv array. --- cmdarg.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmdarg.sh b/cmdarg.sh index 976fb7c..d909948 100644 --- a/cmdarg.sh +++ b/cmdarg.sh @@ -303,7 +303,7 @@ function cmdarg_parse fi if [[ "$fullopt" == "--" ]] && [[ $parsing -eq 0 ]]; then - cmdarg_argv+=($@) + cmdarg_argv+=("$@") break elif [[ "${fullopt:0:2}" == "--" ]]; then longopt=${fullopt:2} From f649a18a75b164b88072774418c318360dfa3267 Mon Sep 17 00:00:00 2001 From: Zane Duffield Date: Wed, 14 Jan 2026 11:20:55 +1100 Subject: [PATCH 4/7] Fix array expansion errors on unknown parameter names When unexpected parameters are passed to cmdarg, the shell (bash 5.1.8) spits out some error messages about bad array subscripts, for example > cmdarg.sh: line 333: CMDARG_FLAGS: bad array subscript > cmdarg.sh: line 334: CMDARG_FLAGS: bad array subscript > cmdarg.sh: line 340: CMDARG: bad array subscript This occurs because the array is being indexed by a null value, so to prevent it we must guard all such uses with a check. --- cmdarg.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmdarg.sh b/cmdarg.sh index d909948..cee3f35 100644 --- a/cmdarg.sh +++ b/cmdarg.sh @@ -326,7 +326,7 @@ function cmdarg_parse ${CMDARG_ERROR_BEHAVIOR} 1 fi - if [[ $is_equals_arg -eq 1 ]]; then + if [[ $is_equals_arg -eq 1 && -n "$opt" ]]; then if [[ ${CMDARG_FLAGS[$opt]} -eq ${CMDARG_FLAG_REQARG} ]] || \ [[ ${CMDARG_FLAGS[$opt]} -eq ${CMDARG_FLAG_OPTARG} ]]; then optarg=$1 @@ -334,7 +334,7 @@ function cmdarg_parse fi fi - if [ ${CMDARG["${opt}"]+abc} ]; then + if [ -n "$opt" ] && [ ${CMDARG["${opt}"]+abc} ]; then cmdarg_set_opt "${CMDARG[$opt]}" "$optarg" local rc=$? failed=$((failed + $rc)) From 9772c9378da379d2bdf33ba3d89634519d5d8edc Mon Sep 17 00:00:00 2001 From: Zane Duffield Date: Wed, 14 Jan 2026 11:40:26 +1100 Subject: [PATCH 5/7] Fix misc shellcheck issues * make clear that array expansion was not desired in one case * change `$@` to `$*` in one case * remove unnecessary `$` in arithmetic context * fix quoting in complex traceback message --- cmdarg.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmdarg.sh b/cmdarg.sh index cee3f35..39d7156 100644 --- a/cmdarg.sh +++ b/cmdarg.sh @@ -237,7 +237,7 @@ function cmdarg_set_opt echo "Malformed hash argument: $arg" >&2 ${CMDARG_ERROR_BEHAVIOR} 1 fi - eval "$key[\$k]=\$v" + eval "${key}[\$k]=\$v" cmdarg_validate "$key" "$v" "$k" || ${CMDARG_ERROR_BEHAVIOR} 1 ;; *) @@ -316,7 +316,7 @@ function cmdarg_parse continue else echo "Malformed argument: ${fullopt}" >&2 - echo "While parsing: $@" >&2 + echo "While parsing: $*" >&2 ${cmdarg_helpers['usage']} >&2 ${CMDARG_ERROR_BEHAVIOR} 1 fi @@ -337,7 +337,7 @@ function cmdarg_parse if [ -n "$opt" ] && [ ${CMDARG["${opt}"]+abc} ]; then cmdarg_set_opt "${CMDARG[$opt]}" "$optarg" local rc=$? - failed=$((failed + $rc)) + failed=$((failed + rc)) else echo "Unknown argument or invalid value : -${opt} | --${longopt}" >&2 ${cmdarg_helpers['usage']} >&2 @@ -374,7 +374,7 @@ function cmdarg_traceback local FRAMES=${#BASH_LINENO[@]} # FRAMES-2 skips main, the last one in arrays for ((i=FRAMES-2; i>=1; i--)); do - echo ' File' \"${BASH_SOURCE[i+1]}\", line ${BASH_LINENO[i]}, probably in ${FUNCNAME[i+1]} >&2 + echo " File \"${BASH_SOURCE[i+1]}\", line ${BASH_LINENO[i]}, probably in ${FUNCNAME[i+1]}" >&2 # Grab the source code of the line sed -n "${BASH_LINENO[i]}{s/^/ /;p}" "${BASH_SOURCE[i+1]}" >&2 done From 66563ad58af020b531de5942d88ee3c0fd3e7b63 Mon Sep 17 00:00:00 2001 From: Zane Duffield Date: Wed, 14 Jan 2026 11:41:05 +1100 Subject: [PATCH 6/7] Add .shellcheckrc to silence lints --- .shellcheckrc | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .shellcheckrc diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..7bc8420 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1,8 @@ +# Many array expressions are constructed and eval-ed +disable=SC2016 +# False positive on "CMDARG_ERROR_BEHAVIOR=return" +disable=SC2209 +# Checking exit code with $? is more of a preference when the script doesn't try to support the errexit shell option +disable=SC2181 +# Masking exit codes isn't much of a problem in this script +disable=SC2155 From 94f11c6f01cbf72d2c82076f9f8fbe74bf583601 Mon Sep 17 00:00:00 2001 From: Zane Duffield Date: Wed, 14 Jan 2026 12:27:32 +1100 Subject: [PATCH 7/7] Simply copyright and author header components when empty --- cmdarg.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/cmdarg.sh b/cmdarg.sh index 39d7156..940a062 100644 --- a/cmdarg.sh +++ b/cmdarg.sh @@ -164,7 +164,10 @@ function cmdarg_usage # cmdarg_usage # # Prints a very helpful usage message about the current program. - echo "$(basename "$0") ${CMDARG_INFO['copyright']} : ${CMDARG_INFO['author']}" + local copyright=${CMDARG_INFO['copyright']:+ ${CMDARG_INFO['copyright']}} + local author=${CMDARG_INFO['author']:+ : ${CMDARG_INFO['author']}} + + echo "$(basename "$0")$copyright$author" echo echo "${CMDARG_INFO['header']}" echo