10 Commits

Author SHA1 Message Date
cd9594264a Merge remote-tracking branch 'zaneduffield/master'
Some checks failed
CI with JUnit Report / build-and-test (push) Has been cancelled
2026-01-18 12:26:25 -05:00
b5bc40cf67 Add shellcheck action to CI 2026-01-18 12:17:00 -05:00
0c8137e474 Fix dependency installation for github CI 2026-01-18 12:08:36 -05:00
Zane Duffield
94f11c6f01 Simply copyright and author header components when empty 2026-01-14 12:27:32 +11:00
Zane Duffield
66563ad58a Add .shellcheckrc to silence lints 2026-01-14 11:54:19 +11:00
Zane Duffield
9772c9378d 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
2026-01-14 11:54:19 +11:00
Zane Duffield
f649a18a75 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.
2026-01-14 11:54:19 +11:00
Zane Duffield
1e31ed1998 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.
2026-01-14 11:54:19 +11:00
Zane Duffield
ad35f698ea 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.
2026-01-14 11:54:18 +11:00
Zane Duffield
8862ba1999 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.
2026-01-14 11:13:40 +11:00
3 changed files with 219 additions and 201 deletions

View File

@@ -11,16 +11,23 @@ jobs:
run: | run: |
set -x set -x
mkdir -p deps mkdir -p deps
mkdir -p installed
PREFIX="$(pwd)/installed"
git clone ${{ vars.VERSIONERS_URI }} deps/versioners git clone ${{ vars.VERSIONERS_URI }} deps/versioners
cd deps/versioners cd deps/versioners
make install PREFIX="$PREFIX" make install
cd ../../ cd ../../
git clone ${{ vars.SHUNIT_URI }} deps/shunit git clone ${{ vars.SHUNIT_URI }} deps/shunit
cd deps/shunit cd deps/shunit
make install PREFIX="$PREFIX" make install
cd ../../ cd ../../
make test-ci PREFIX="$PREFIX" make test-ci
cat junit.xml cat junit.xml
- name: Run ShellCheck
uses: ludeeus/action-shellcheck@master
with:
ignore_paths: deps tests installed
scandir: .
- name: Publish Test Report - name: Publish Test Report
uses: mikepenz/action-junit-report@v5 uses: mikepenz/action-junit-report@v5
if: success() || failure() # always run even if the previous step fails if: success() || failure() # always run even if the previous step fails

8
.shellcheckrc Normal file
View File

@@ -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

View File

@@ -36,7 +36,7 @@ function cmdarg
echo "-h is reserved for cmdarg usage" >&2 echo "-h is reserved for cmdarg usage" >&2
${CMDARG_ERROR_BEHAVIOR} 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
if [[ "$(type -t cmdarg_$key)" != "" ]] || \ if [[ "$(type -t cmdarg_"$key")" != "" ]] || \
[[ "${CMDARG_FLAGS[$shortopt]}" != "" ]] || \ [[ "${CMDARG_FLAGS[$shortopt]}" != "" ]] || \
[[ "${CMDARG_TYPES[$key]}" != "" ]]; then [[ "${CMDARG_TYPES[$key]}" != "" ]]; then
echo "command line key '$shortopt ($key)' is reserved by cmdarg or defined twice" >&2 echo "command line key '$shortopt ($key)' is reserved by cmdarg or defined twice" >&2
@@ -53,14 +53,14 @@ function cmdarg
elif [[ "$argtype" != "" ]]; then elif [[ "$argtype" != "" ]]; then
CMDARG_FLAGS[$shortopt]=${argtypemap["$argtype"]} CMDARG_FLAGS[$shortopt]=${argtypemap["$argtype"]}
if [[ "${1:2:4}" == "[]" ]]; then if [[ "${1:2:4}" == "[]" ]]; then
declare -p ${key} >/dev/null 2>&1 declare -p "${key}" >/dev/null 2>&1
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo 'Array variable '"${key}"' does not exist. Array variables MUST be declared by the user!' >&2 echo 'Array variable '"${key}"' does not exist. Array variables MUST be declared by the user!' >&2
${CMDARG_ERROR_BEHAVIOR} 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
CMDARG_TYPES[$key]=$CMDARG_TYPE_ARRAY CMDARG_TYPES[$key]=$CMDARG_TYPE_ARRAY
elif [[ "${1:2:4}" == "{}" ]]; then elif [[ "${1:2:4}" == "{}" ]]; then
declare -p ${key} >/dev/null 2>&1 declare -p "${key}" >/dev/null 2>&1
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo 'Hash variable '"${key}"' does not exist. Hash variables MUST be declared by the user!' >&2 echo 'Hash variable '"${key}"' does not exist. Hash variables MUST be declared by the user!' >&2
${CMDARG_ERROR_BEHAVIOR} 1 ${CMDARG_ERROR_BEHAVIOR} 1
@@ -80,14 +80,14 @@ function cmdarg
CMDARG_DESC["$shortopt"]=$3 CMDARG_DESC["$shortopt"]=$3
CMDARG_DEFAULT["$shortopt"]=${4:-} CMDARG_DEFAULT["$shortopt"]=${4:-}
if [[ ${CMDARG_FLAGS[$shortopt]} -eq $CMDARG_FLAG_REQARG ]] && [[ "${4:-}" == "" ]]; then if [[ ${CMDARG_FLAGS[$shortopt]} -eq $CMDARG_FLAG_REQARG ]] && [[ "${4:-}" == "" ]]; then
CMDARG_REQUIRED+=($shortopt) CMDARG_REQUIRED+=("$shortopt")
else else
CMDARG_OPTIONAL+=($shortopt) CMDARG_OPTIONAL+=("$shortopt")
fi fi
cmdarg_cfg["$2"]="${4:-}" cmdarg_cfg["$2"]="${4:-}"
local validatorfunc local validatorfunc
validatorfunc=${5:-} 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 echo "Validators must be bash functions accepting 1 argument (not '$validatorfunc')" >&2
${CMDARG_ERROR_BEHAVIOR} 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
@@ -120,7 +120,7 @@ function cmdarg_describe
local flags="${CMDARG_FLAGS[$opt]}" local flags="${CMDARG_FLAGS[$opt]}"
local validator="${CMDARG_VALIDATORS[$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 function cmdarg_describe_default
@@ -139,16 +139,16 @@ function cmdarg_describe_default
default="(Default \"${default}\")" default="(Default \"${default}\")"
fi fi
case ${argtype} in case ${argtype} in
$CMDARG_TYPE_STRING) "$CMDARG_TYPE_STRING")
echo "-${opt},--${longopt} v : String. ${description} ${default}" echo "-${opt},--${longopt} v : String. ${description} ${default}"
;; ;;
$CMDARG_TYPE_BOOLEAN) "$CMDARG_TYPE_BOOLEAN")
echo "-${opt},--${longopt} : Boolean. ${description} ${default}" 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}" 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}" echo "-${opt},--${longopt} k=v{, ..} : Hash. ${description}. Pass this argument multiple times for multiple key/value pairs. ${default}"
;; ;;
*) *)
@@ -164,7 +164,10 @@ function cmdarg_usage
# cmdarg_usage # cmdarg_usage
# #
# Prints a very helpful usage message about the current program. # 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
echo "${CMDARG_INFO['header']}" echo "${CMDARG_INFO['header']}"
echo echo
@@ -173,7 +176,7 @@ function cmdarg_usage
echo "Required Arguments:" echo "Required Arguments:"
for key in "${CMDARG_REQUIRED[@]}" for key in "${CMDARG_REQUIRED[@]}"
do do
echo " $(cmdarg_describe $key)" echo " $(cmdarg_describe "$key")"
done done
echo echo
fi fi
@@ -181,7 +184,7 @@ function cmdarg_usage
echo "Optional Arguments": echo "Optional Arguments":
for key in "${CMDARG_OPTIONAL[@]}" for key in "${CMDARG_OPTIONAL[@]}"
do do
echo " $(cmdarg_describe $key)" echo " $(cmdarg_describe "$key")"
done done
fi fi
echo echo
@@ -215,29 +218,29 @@ function cmdarg_set_opt
set +u set +u
case ${CMDARG_TYPES[$key]} in case ${CMDARG_TYPES[$key]} in
$CMDARG_TYPE_STRING) "$CMDARG_TYPE_STRING")
cmdarg_cfg[$key]=$arg cmdarg_cfg[$key]=$arg
cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1
;; ;;
$CMDARG_TYPE_BOOLEAN) "$CMDARG_TYPE_BOOLEAN")
cmdarg_cfg[$key]=true cmdarg_cfg[$key]=true
cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1
;; ;;
$CMDARG_TYPE_ARRAY) "$CMDARG_TYPE_ARRAY")
local arrname="${key}" local arrname="${key}"
local str='${#'"$arrname"'[@]}' local str='${#'"$arrname"'[@]}'
local prevlen=$(eval "echo $str") local prevlen=$(eval "echo $str")
eval "${arrname}[$((prevlen + 1))]=\"$arg\"" eval "${arrname}[$((prevlen + 1))]=\"$arg\""
cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1
;; ;;
$CMDARG_TYPE_HASH) "$CMDARG_TYPE_HASH")
local k=${arg%%=*} local k=${arg%%=*}
local v=${arg#*=} local v=${arg#*=}
if [[ "$k" == "$arg" ]] && [[ "$v" == "$arg" ]] && [[ "$k" == "$v" ]]; then if [[ "$k" == "$arg" ]] && [[ "$v" == "$arg" ]] && [[ "$k" == "$v" ]]; then
echo "Malformed hash argument: $arg" >&2 echo "Malformed hash argument: $arg" >&2
${CMDARG_ERROR_BEHAVIOR} 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
eval "$key[\$k]=\$v" eval "${key}[\$k]=\$v"
cmdarg_validate "$key" "$v" "$k" || ${CMDARG_ERROR_BEHAVIOR} 1 cmdarg_validate "$key" "$v" "$k" || ${CMDARG_ERROR_BEHAVIOR} 1
;; ;;
*) *)
@@ -255,18 +258,18 @@ function cmdarg_check_empty
local type=${CMDARG_TYPES[$longopt]} local type=${CMDARG_TYPES[$longopt]}
case $type in case $type in
$CMDARG_TYPE_STRING) "$CMDARG_TYPE_STRING")
echo ${cmdarg_cfg[$longopt]} echo "${cmdarg_cfg[$longopt]}"
;; ;;
$CMDARG_TYPE_BOOLEAN) "$CMDARG_TYPE_BOOLEAN")
echo ${cmdarg_cfg[$longopt]} echo "${cmdarg_cfg[$longopt]}"
;; ;;
$CMDARG_TYPE_ARRAY) "$CMDARG_TYPE_ARRAY")
local arrname="${longopt}" local arrname="${longopt}"
local lval='${!'"${arrname}"'[@]}' local lval='${!'"${arrname}"'[@]}'
eval "echo $lval" eval "echo $lval"
;; ;;
$CMDARG_TYPE_HASH) "$CMDARG_TYPE_HASH")
local arrname="${longopt}" local arrname="${longopt}"
local lval='${!'"${arrname}"'[@]}' local lval='${!'"${arrname}"'[@]}'
eval "echo $lval" eval "echo $lval"
@@ -303,7 +306,7 @@ function cmdarg_parse
fi fi
if [[ "$fullopt" == "--" ]] && [[ $parsing -eq 0 ]]; then if [[ "$fullopt" == "--" ]] && [[ $parsing -eq 0 ]]; then
cmdarg_argv+=($@) cmdarg_argv+=("$@")
break break
elif [[ "${fullopt:0:2}" == "--" ]]; then elif [[ "${fullopt:0:2}" == "--" ]]; then
longopt=${fullopt:2} longopt=${fullopt:2}
@@ -316,7 +319,7 @@ function cmdarg_parse
continue continue
else else
echo "Malformed argument: ${fullopt}" >&2 echo "Malformed argument: ${fullopt}" >&2
echo "While parsing: $@" >&2 echo "While parsing: $*" >&2
${cmdarg_helpers['usage']} >&2 ${cmdarg_helpers['usage']} >&2
${CMDARG_ERROR_BEHAVIOR} 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
@@ -326,7 +329,7 @@ function cmdarg_parse
${CMDARG_ERROR_BEHAVIOR} 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi 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} ]] || \ if [[ ${CMDARG_FLAGS[$opt]} -eq ${CMDARG_FLAG_REQARG} ]] || \
[[ ${CMDARG_FLAGS[$opt]} -eq ${CMDARG_FLAG_OPTARG} ]]; then [[ ${CMDARG_FLAGS[$opt]} -eq ${CMDARG_FLAG_OPTARG} ]]; then
optarg=$1 optarg=$1
@@ -334,10 +337,10 @@ function cmdarg_parse
fi fi
fi fi
if [ ${CMDARG["${opt}"]+abc} ]; then if [ -n "$opt" ] && [ ${CMDARG["${opt}"]+abc} ]; then
cmdarg_set_opt "${CMDARG[$opt]}" "$optarg" cmdarg_set_opt "${CMDARG[$opt]}" "$optarg"
local rc=$? local rc=$?
failed=$((failed + $rc)) failed=$((failed + rc))
else else
echo "Unknown argument or invalid value : -${opt} | --${longopt}" >&2 echo "Unknown argument or invalid value : -${opt} | --${longopt}" >&2
${cmdarg_helpers['usage']} >&2 ${cmdarg_helpers['usage']} >&2
@@ -351,7 +354,7 @@ function cmdarg_parse
local key local key
for key in "${CMDARG_REQUIRED[@]}" for key in "${CMDARG_REQUIRED[@]}"
do do
if [[ "$(cmdarg_check_empty $key)" == "" ]]; then if [[ "$(cmdarg_check_empty "$key")" == "" ]]; then
missing="${missing} -${key}" missing="${missing} -${key}"
failed=$((failed + 1)) failed=$((failed + 1))
fi fi
@@ -374,7 +377,7 @@ function cmdarg_traceback
local FRAMES=${#BASH_LINENO[@]} local FRAMES=${#BASH_LINENO[@]}
# FRAMES-2 skips main, the last one in arrays # FRAMES-2 skips main, the last one in arrays
for ((i=FRAMES-2; i>=1; i--)); do 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 # Grab the source code of the line
sed -n "${BASH_LINENO[i]}{s/^/ /;p}" "${BASH_SOURCE[i+1]}" >&2 sed -n "${BASH_LINENO[i]}{s/^/ /;p}" "${BASH_SOURCE[i+1]}" >&2
done done
@@ -392,10 +395,10 @@ function cmdarg_dump
local ref local ref
local value local value
for key in ${!cmdarg_cfg[@]} for key in "${!cmdarg_cfg[@]}"
do do
repr="${key}:${CMDARG_TYPES[$key]}" 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}" arrname="${key}"
echo "${repr} => " echo "${repr} => "
keys='${!'"$arrname"'[@]}' keys='${!'"$arrname"'[@]}'