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 diff --git a/cmdarg.sh b/cmdarg.sh index 282b05c..940a062 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 + 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 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 + if [[ "$validatorfunc" != "" ]] && [[ "$(declare -F "$validatorfunc")" == "" ]]; then + 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 } @@ -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 @@ -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 } @@ -164,25 +164,28 @@ 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 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 +202,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 +218,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 +258,25 @@ 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_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 +291,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 && -n "$opt" ]]; 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 [ -n "$opt" ] && [ ${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 +354,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 +377,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 @@ -392,22 +395,22 @@ 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 - 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 +423,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