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.
This commit is contained in:
2014-05-26 11:45:17 -07:00
parent 5c926c54e2
commit d3721faf10
4 changed files with 176 additions and 39 deletions

View File

@@ -100,6 +100,14 @@ For example, this is a valid validator:
cmdarg 'x' 'x-option' 'some opt' '' "grep -E '^[0-9]+$'" 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 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. 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 getopt vs getopts
================= =================

100
cmdarg.sh
View File

@@ -5,6 +5,7 @@ if (( BASH_VERSINFO[0] < 4 )); then
exit 1 exit 1
fi fi
CMDARG_ERROR_BEHAVIOR=return
CMDARG_FLAG_NOARG=0 CMDARG_FLAG_NOARG=0
CMDARG_FLAG_REQARG=2 CMDARG_FLAG_REQARG=2
@@ -33,33 +34,36 @@ function cmdarg
key="$2" key="$2"
if [[ "$shortopt" == "h" ]]; then if [[ "$shortopt" == "h" ]]; then
echo "-h is reserved for cmdarg usage" >&2 echo "-h is reserved for cmdarg usage" >&2
exit 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
exit 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
declare -A argtypemap declare -A argtypemap
argtypemap[':']=$CMDARG_FLAG_REQARG argtypemap[':']=$CMDARG_FLAG_REQARG
argtypemap['?']=$CMDARG_FLAG_OPTARG argtypemap['?']=$CMDARG_FLAG_OPTARG
argtype=${1:1:1} 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"]} 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
exit 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
exit 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
CMDARG_TYPES[$key]=$CMDARG_TYPE_HASH CMDARG_TYPES[$key]=$CMDARG_TYPE_HASH
else else
@@ -85,7 +89,7 @@ function cmdarg
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
exit 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
CMDARG_VALIDATORS["$shortopt"]="$validatorfunc" CMDARG_VALIDATORS["$shortopt"]="$validatorfunc"
CMDARG_GETOPTLIST="${CMDARG_GETOPTLIST}$1" CMDARG_GETOPTLIST="${CMDARG_GETOPTLIST}$1"
@@ -102,7 +106,7 @@ function cmdarg_info
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "cmdarg_info <flag> <value>" >&2 echo "cmdarg_info <flag> <value>" >&2
echo "Where <flag> is one of $FLAGS" >&2 echo "Where <flag> is one of $FLAGS" >&2
exit 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
CMDARG_INFO["$1"]=$2 CMDARG_INFO["$1"]=$2
} }
@@ -152,7 +156,7 @@ function cmdarg_describe_default
;; ;;
*) *)
echo "Unable to return string description for ${opt}; unknown type ${argtype}" >&2 echo "Unable to return string description for ${opt}; unknown type ${argtype}" >&2
exit 1 ${CMDARG_ERROR_BEHAVIOR} 1
;; ;;
esac esac
@@ -187,36 +191,66 @@ function cmdarg_usage
echo "${CMDARG_INFO['footer']}" 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 function cmdarg_set_opt
{ {
set -u
local key arg local key arg
key=$1 key=$1
arg="$2" arg="$2"
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_TYPE_BOOLEAN) $CMDARG_TYPE_BOOLEAN)
cmdarg_cfg[$key]=true cmdarg_cfg[$key]=true
cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1
;; ;;
$CMDARG_TYPE_ARRAY) $CMDARG_TYPE_ARRAY)
arrname="${key}" arrname="${key}"
str='${#'"$arrname"'[@]}' str='${#'"$arrname"'[@]}'
prevlen=$(eval "echo $str") prevlen=$(eval "echo $str")
eval "${arrname}[$((prevlen + 1))]=\"$arg\"" eval "${arrname}[$((prevlen + 1))]=\"$arg\""
cmdarg_validate "$key" "$arg" || ${CMDARG_ERROR_BEHAVIOR} 1
;; ;;
$CMDARG_TYPE_HASH) $CMDARG_TYPE_HASH)
local arrname=${key}
declare -gA -- "$arrname"
local k=${arg%%=*} local k=${arg%%=*}
local v=${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 echo "Unable to return string description for ${key}; unknown type ${CMDARG_TYPES[$key]}" >&2
exit 1 ${CMDARG_ERROR_BEHAVIOR} 1
;; ;;
esac esac
return 0
} }
function cmdarg_check_empty function cmdarg_check_empty
@@ -255,7 +289,9 @@ function cmdarg_parse
# #
# Call it EXACTLY LIKE THAT, and it will parse your arguments for you. # 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. # 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 parsing=0
while [[ "$@" != "" ]]; do while [[ "$@" != "" ]]; do
@@ -288,16 +324,16 @@ function cmdarg_parse
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
exit 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
if [[ "$opt" == "h" ]] || [[ "$longopt" == "help" ]]; then if [[ "$opt" == "h" ]] || [[ "$longopt" == "help" ]]; then
${cmdarg_helpers['usage']} >&2 ${cmdarg_helpers['usage']} >&2
exit 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
if [[ $is_equals_arg -eq 1 ]]; then 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 [[ ${CMDARG_FLAGS[$opt]} -eq ${CMDARG_FLAG_OPTARG} ]]; then
optarg=$1 optarg=$1
shift shift
@@ -306,47 +342,33 @@ function cmdarg_parse
if [ ${CMDARG["${opt}"]+abc} ]; then if [ ${CMDARG["${opt}"]+abc} ]; then
cmdarg_set_opt "${CMDARG[$opt]}" "$optarg" cmdarg_set_opt "${CMDARG[$opt]}" "$optarg"
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
exit 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
done 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 # everything they did wrong first
failed=0
missing=""
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=1 failed=$((failed + 1))
fi fi
done done
local opt if [ $failed -gt 0 ]; then
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 [[ "$missing" != "" ]]; then if [[ "$missing" != "" ]]; then
echo "Missing arguments : ${missing}" echo "Missing arguments : ${missing}" >&2
fi fi
echo echo >&2
${cmdarg_helpers['usage']} >&2 ${cmdarg_helpers['usage']} >&2
exit 1 ${CMDARG_ERROR_BEHAVIOR} 1
fi fi
if [ ! -z "${cmdarg_cfg[cfgfile]}" ]; then if [ ! -z "${cmdarg_cfg[cfgfile]}" ]; then

View File

@@ -1,5 +1,26 @@
source $(dirname ${BASH_SOURCE})/../cmdarg.sh 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() function shunittest_array_undefined()
{ {
# Tests that cmdarg and cmdarg_parse return an error when an array # 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_cfg['boolean']}" == "true" ]] || return 1
[[ "${cmdarg_argv[0]}" == "something" ]] || 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
}

54
tests/test_validators.sh Normal file
View File

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