From d3721faf10362e006398c0d579ef5f557b71a4ad Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Mon, 26 May 2014 11:45:17 -0700 Subject: [PATCH] Close #17 and Close #16 Introduces CMDARG_ERROR_BEHAVIOR. Adds some more tests. Introduces new behavior for hash validators (see README). Fixes a few odds and ends. --- README.md | 23 +++++++++ cmdarg.sh | 100 ++++++++++++++++++++++++--------------- tests/test_types.sh | 38 +++++++++++++++ tests/test_validators.sh | 54 +++++++++++++++++++++ 4 files changed, 176 insertions(+), 39 deletions(-) create mode 100644 tests/test_validators.sh diff --git a/README.md b/README.md index 7ede370..146a63d 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,14 @@ For example, this is a valid validator: cmdarg 'x' 'x-option' 'some opt' '' "grep -E '^[0-9]+$'" +There is an exception to this form, and that is for hash arguments (e.g. 'x:{}'). In this instance, the key for the argument (e.g. -x key=value) is to be considered a part of the value, and the user may want to validate this as well as the value. In this instance, when calling a validator against a hash argument, the validator will receive a second argument, which is the key of the hash being validated. For example: + + # When we receive + cmdarg 'x:{}' 'something' 'something' my_validator + cmdarg_parse -x hashkey=hashvalue + # ... we will call + my_validator hashvalue hashkey + cmdarg_info =========== @@ -239,6 +247,21 @@ The short options for all specified arguments in cmdarg are kept in a hash ${CMD For examples of this behavior, please see ./tests/test_helpers.sh, the "shunittest_test_describe_and_usage_helper" function. +Controlling cmdarg's behavior on error +====================================== + +By default, whenever something happens that cmdarg doesn't like, it will 'return 1' up the stack to the caller. This is different from the old behavior in v1.0, which would 'exit 1'. You can control cmdarg's error behavior by setting the CMDARG_ERROR_BEHAVIOR variable to the function/builtin you want called whenever an error is encountered. + +To get the old v1.0 behavior back, you can, before calling any cmdarg functions: + + CMDARG_ERROR_BEHAVIOR=exit + +If you want cmdarg to call some function of your own when it encounters an error, you could: + + CMDARG_ERROR_BEHAVIOR=my_error_function + +CMDARG_ERROR_BEHAVIOR is treated as a function call (e.g. return or exit) with one argument, the value to return. You will be given no more context regarding the error (and, in fact, you should not expect this to be called unless a fatal error has been encountered, whether during setup or parsing). + getopt vs getopts ================= diff --git a/cmdarg.sh b/cmdarg.sh index 1ea6877..22f03c7 100644 --- a/cmdarg.sh +++ b/cmdarg.sh @@ -5,6 +5,7 @@ if (( BASH_VERSINFO[0] < 4 )); then exit 1 fi +CMDARG_ERROR_BEHAVIOR=return CMDARG_FLAG_NOARG=0 CMDARG_FLAG_REQARG=2 @@ -33,33 +34,36 @@ function cmdarg key="$2" if [[ "$shortopt" == "h" ]]; then echo "-h is reserved for cmdarg usage" >&2 - exit 1 + ${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 - exit 1 + ${CMDARG_ERROR_BEHAVIOR} 1 fi declare -A argtypemap argtypemap[':']=$CMDARG_FLAG_REQARG argtypemap['?']=$CMDARG_FLAG_OPTARG argtype=${1:1:1} - if [[ "$argtype" != "" ]]; then + if [[ "$argtype" =~ ^[\[{]$ ]]; then + 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 - exit 1 + ${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 - exit 1 + ${CMDARG_ERROR_BEHAVIOR} 1 fi CMDARG_TYPES[$key]=$CMDARG_TYPE_HASH else @@ -85,7 +89,7 @@ function cmdarg validatorfunc=${5:-} if [[ "$validatorfunc" != "" ]] && [[ "$(declare -F $validatorfunc)" == "" ]]; then echo "Validators must be bash functions accepting 1 argument (not '$validatorfunc')" >&2 - exit 1 + ${CMDARG_ERROR_BEHAVIOR} 1 fi CMDARG_VALIDATORS["$shortopt"]="$validatorfunc" CMDARG_GETOPTLIST="${CMDARG_GETOPTLIST}$1" @@ -102,7 +106,7 @@ function cmdarg_info if [ $? -ne 0 ]; then echo "cmdarg_info " >&2 echo "Where is one of $FLAGS" >&2 - exit 1 + ${CMDARG_ERROR_BEHAVIOR} 1 fi CMDARG_INFO["$1"]=$2 } @@ -152,7 +156,7 @@ function cmdarg_describe_default ;; *) echo "Unable to return string description for ${opt}; unknown type ${argtype}" >&2 - exit 1 + ${CMDARG_ERROR_BEHAVIOR} 1 ;; esac @@ -187,36 +191,66 @@ function cmdarg_usage echo "${CMDARG_INFO['footer']}" } +function cmdarg_validate +{ + set -u + local longopt value + longopt=$1 + value=$2 + hashkey=${3:-} + set +u + + 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 + return 1 + fi + fi + return 0 +} + function cmdarg_set_opt { + set -u local key arg key=$1 arg="$2" + 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) arrname="${key}" str='${#'"$arrname"'[@]}' prevlen=$(eval "echo $str") eval "${arrname}[$((prevlen + 1))]=\"$arg\"" + cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1 ;; $CMDARG_TYPE_HASH) - local arrname=${key} - declare -gA -- "$arrname" local k=${arg%%=*} local v=${arg#*=} - eval "$arrname[\$k]=\$v" + 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 - exit 1 + ${CMDARG_ERROR_BEHAVIOR} 1 ;; esac + return 0 } function cmdarg_check_empty @@ -255,7 +289,9 @@ function cmdarg_parse # # Call it EXACTLY LIKE THAT, and it will parse your arguments for you. # This function only knows about the arguments that you previously called 'cmdarg' for. - local OPTIND parsing fullopt opt optarg longopt tmpopt + local OPTIND parsing fullopt opt optarg longopt tmpopt failed missing + failed=0 + missing="" parsing=0 while [[ "$@" != "" ]]; do @@ -288,16 +324,16 @@ function cmdarg_parse echo "Malformed argument: ${fullopt}" >&2 echo "While parsing: $@" >&2 ${cmdarg_helpers['usage']} >&2 - exit 1 + ${CMDARG_ERROR_BEHAVIOR} 1 fi if [[ "$opt" == "h" ]] || [[ "$longopt" == "help" ]]; then ${cmdarg_helpers['usage']} >&2 - exit 1 + ${CMDARG_ERROR_BEHAVIOR} 1 fi if [[ $is_equals_arg -eq 1 ]]; 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 optarg=$1 shift @@ -306,47 +342,33 @@ function cmdarg_parse if [ ${CMDARG["${opt}"]+abc} ]; then cmdarg_set_opt "${CMDARG[$opt]}" "$optarg" + rc=$? + failed=$((failed + $rc)) else echo "Unknown argument or invalid value : -${opt} | --${longopt}" >&2 ${cmdarg_helpers['usage']} >&2 - exit 1 + ${CMDARG_ERROR_BEHAVIOR} 1 fi done - # --- Don't exit early during validation, tell the user + # --- Don't ${CMDARG_ERROR_BEHAVIOR} early during validation, tell the user # everything they did wrong first - failed=0 - missing="" for key in "${CMDARG_REQUIRED[@]}" do if [[ "$(cmdarg_check_empty $key)" == "" ]]; then missing="${missing} -${key}" - failed=1 + failed=$((failed + 1)) fi done - local opt - local OPTARG - for opt in "${!cmdarg_cfg[@]}" - do - shortopt=${CMDARG_REV[$opt]} - if [ "${CMDARG_VALIDATORS[$shortopt]}" != "" ]; then - OPTARG=${cmdarg_cfg[$opt]} - ( ( ${CMDARG_VALIDATORS[${shortopt}]} "$OPTARG" ) && [ "$OPTARG" != "" ]) - if [ $? -ne 0 ]; then - echo "Invalid value for -$shortopt : ${cmdarg_cfg[$opt]}" - failed=1 - fi - fi - done - if [ $failed -eq 1 ]; then + if [ $failed -gt 0 ]; then if [[ "$missing" != "" ]]; then - echo "Missing arguments : ${missing}" + echo "Missing arguments : ${missing}" >&2 fi - echo + echo >&2 ${cmdarg_helpers['usage']} >&2 - exit 1 + ${CMDARG_ERROR_BEHAVIOR} 1 fi if [ ! -z "${cmdarg_cfg[cfgfile]}" ]; then diff --git a/tests/test_types.sh b/tests/test_types.sh index d61302f..f115fc5 100644 --- a/tests/test_types.sh +++ b/tests/test_types.sh @@ -1,5 +1,26 @@ source $(dirname ${BASH_SOURCE})/../cmdarg.sh +function shunittest_flags_required +{ + # Tests that flags (:?) are required for array or hash arguments + + cmdarg_purge + declare -a something + declare -A something_else + cmdarg 'x[]' 'something' 'something' && return 1 + cmdarg 'y{}' 'something_else' 'something else' && return 1 + + cmdarg_purge + cmdarg 'x:[]' 'something' 'something' || return 1 + cmdarg 'y:{}' 'something_else' 'something' || return 1 + + cmdarg_purge + cmdarg 'x?[]' 'something' 'something' || return 1 + cmdarg 'y?{}' 'something_else' 'something' || return 1 + + return 0 +} + function shunittest_array_undefined() { # Tests that cmdarg and cmdarg_parse return an error when an array @@ -82,3 +103,20 @@ function shunittest_boolean_no_optarg [[ "${cmdarg_cfg['boolean']}" == "true" ]] || return 1 [[ "${cmdarg_argv[0]}" == "something" ]] || return 1 } + +function shunittest_hash_malformed +{ + # Checks for malformed hash arguments that pass parsing + + declare -A myhash + + function parse + { + cmdarg_purge + cmdarg 'x:{}' 'myhash' 'myhash' + cmdarg_parse "$@" + } + + parse --myhash iamjustavalue && return 1 + return 0 +} diff --git a/tests/test_validators.sh b/tests/test_validators.sh new file mode 100644 index 0000000..7f59328 --- /dev/null +++ b/tests/test_validators.sh @@ -0,0 +1,54 @@ +#!/usr/bin/bash4 + +source $(dirname ${BASH_SOURCE[0]})/../cmdarg.sh + +function shunittest_validator_for_hash +{ + function my_hash_validator + { + value=${1:-$OPTARG} + echo "my_hash_validator $value" >&2 + [[ "$value" == "value" ]] + } + + declare -A something + + cmdarg_purge + cmdarg 'x:{}' 'something' 'something' '' my_hash_validator || return 1 + set -x + cmdarg_parse --something key=notavalue && return 1 + return 0 +} + +function shunittest_validator_for_array +{ + function my_array_validator + { + value=${1:-$OPTARG} + echo "my_array_validator $value" >&2 + [[ "$value" == "value" ]] + } + + declare -a something + + cmdarg_purge + cmdarg 'x:[]' 'something' 'something' '' my_array_validator || return 1 + cmdarg_parse --something notavalue && return 1 + return 0 +} + +function shunittest_validator_failure_recognized +{ + + function my_validator + { + value=${1:-$OPTARG} + echo "my_validator $value" >&2 + [[ "$value" == "value" ]] + } + + cmdarg_purge + cmdarg 'x:' 'something' 'something' '' my_validator + cmdarg_parse --something notavalue || return 0 + return 1 +}