From b7f85914e74190cb5fb4f2cf288bdca7e66f83ca Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Tue, 20 May 2014 08:49:46 -0700 Subject: [PATCH] Close #13 : cmdarg_helpers[] introduced, README documentation updated --- README.md | 36 ++++++++++++ cmdarg.sh | 60 ++++++++++++++----- tests/test_helpers.sh | 134 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 16 deletions(-) create mode 100644 tests/test_helpers.sh diff --git a/README.md b/README.md index a13592b..7ede370 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,8 @@ cmdarg takes the pain out of creating your --help messages. For example, conside -b,--boolean-thing : Boolean. Some boolean thing -a,--myarray v[, ...] : Array. Some array of stuff. Pass this argument multiple times for multiple values. +You can change the formatting of help messages with helper functions. (see Helpers, below). + Setting arrays and hashes ========================= @@ -203,6 +205,40 @@ Similarly, cmdarg understands '--' which means "stop processing arguments, the r ... Cmdarg would parse -x and --longopt as expected, and then ${cmdarg_argv[0]} would hold "--some-thing-with-dashes", for your program to do with what it will. +Helpers +======= + +cmdarg is meant to be extensible by default, so there are some places where you can hook into it to change cmdarg's behavior. By changing the members of the cmdarg_helpers hash, like this: + + # Change the way arguments are described in --help + cmdarg_helpers['describe']=my_description_function + # Completely replace cmdarg's builtin --help message generator with your own + cmdarg_helpers['usage']=my_usage_function + +## Description Helper + +The description helper is used when you are happy with the overall structure of how cmdarg prints your usage message (header, required, optional, footer), but you want to change the way that individual arguments are described. You can do this by setting cmdarg_helpers['describe'] to the name of a bash function which accepts the following parameters (in order): + +* $1 : long option to be described +* $2 : short option to be described +* $3 : argument type being described (will be one of ${CMDARG_TYPE_STRING}, ${CMDARG_TYPE_BOOLEAN}, ${CMDARG_TYPE_ARRAY} or ${CMDARG_TYPE_HASH}) +* $4 : any default value that is set for the option being described +* $5 : The description for the option being described (as provided to 'cmdarg' previously) +* $6 : Flags for the option being described (a logically OR'ed bitmask of ${CMDARG_FLAG_NOARG}, ${CMDARG_FLAG_REQARG}, or ${CMDARG_FLAG_OPTARG} - although we specify this as a bitmask and advise you to treat it as such, in practice, this is usually an assignment of one of those 3 values) +* $7 : The name of any validator (if any) set for the option being described + +This is every piece of information cmdarg keeps related to an argument (aside from its value). You can use these to describe the argument however you please. Your function must print the text description to stdout. The return value of your function is ignored. + +For examples of this behavior, please see ./tests/test_helpers.sh + +## Usage Helper + +The usage helper is used when you want to completely override cmdarg's built in --help handler. Note that, when you override the usage helper, you will no longer benefit from the description helper, since that is called from inside of the default usage handler. If you override the usage helper, you will have to implement 100% of --help functionality on your own. + +The short options for all specified arguments in cmdarg are kept in a hash ${CMDARG} which maps short arguments (-x) to long arguments (--long-version-of-x). However, it is not recommended that you iterate over this hash directly, as the order of hash key iteration is not guaranteed, so your --help message will change every time. To help with this, cmdarg populates two one-dimensional arrays, CMDARG_OPTIONAL and CMDARG_REQUIRED with the short options of all optional and require arguments, respectively. It is recommended that you iterate over these arrays instead of CMDARG to ensure an ordered output. It is further recommended that you still utilize cmdarg_describe to describe each individual argument, since this abstracts away the logic of how to get the flags, the type, etc of the argument, and lets you continue to provide a standard interface for your API developer(s). + +For examples of this behavior, please see ./tests/test_helpers.sh, the "shunittest_test_describe_and_usage_helper" function. + getopt vs getopts ================= diff --git a/cmdarg.sh b/cmdarg.sh index 7b0a03c..1ea6877 100644 --- a/cmdarg.sh +++ b/cmdarg.sh @@ -7,8 +7,8 @@ fi CMDARG_FLAG_NOARG=0 -CMDARG_FLAG_REQARG=1 -CMDARG_FLAG_OPTARG=2 +CMDARG_FLAG_REQARG=2 +CMDARG_FLAG_OPTARG=4 CMDARG_TYPE_ARRAY=1 CMDARG_TYPE_HASH=2 @@ -93,7 +93,7 @@ function cmdarg function cmdarg_info { - # cmdarg + # cmdarg_info # # Sets various flags about your script that are printed during cmdarg_usage # @@ -109,30 +109,53 @@ function cmdarg_info function cmdarg_describe { - local key default + local longopt opt argtype default description flags validator longopt=${CMDARG[$1]} opt=$1 - if [ "${CMDARG_DEFAULT[$opt]}" != "" ]; then - default="(Default \"${CMDARG_DEFAULT[$opt]}\")" + argtype=${CMDARG_TYPES[$longopt]} + default=${CMDARG_DEFAULT[$opt]} + description=${CMDARG_DESC[$opt]} + flags="${CMDARG_FLAGS[$opt]}" + validator="${CMDARG_VALIDATORS[$opt]}" + + ${cmdarg_helpers['describe']} $longopt $opt $argtype "${default}" "${description}" "${flags}" "${validator}" +} + +function cmdarg_describe_default +{ + set -u + local longopt opt argtype default description flags validator + longopt=$1 + opt=$2 + argtype=$3 + default="$4" + description="$5" + flags="$6" + validator="${7:-}" + set +u + + if [ "${default}" != "" ]; then + default="(Default \"${default}\")" fi - case ${CMDARG_TYPES[$longopt]} in + case ${argtype} in $CMDARG_TYPE_STRING) - echo "-${opt},--${longopt} v : String. ${CMDARG_DESC[$opt]} $default" + echo "-${opt},--${longopt} v : String. ${description} ${default}" ;; $CMDARG_TYPE_BOOLEAN) - echo "-${opt},--${longopt} : Boolean. ${CMDARG_DESC[$opt]} $default" + echo "-${opt},--${longopt} : Boolean. ${description} ${default}" ;; $CMDARG_TYPE_ARRAY) - echo "-${opt},--${longopt} v[, ...] : Array. ${CMDARG_DESC[$opt]}. 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) - echo "-${opt},--${longopt} k=v{, ..} : Hash. ${CMDARG_DESC[$opt]}. 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}" ;; *) - echo "Unable to return string description for ${key}; unknown type ${CMDARG_TYPES[$opt]}" >&2 + echo "Unable to return string description for ${opt}; unknown type ${argtype}" >&2 exit 1 ;; esac + } function cmdarg_usage @@ -264,12 +287,12 @@ function cmdarg_parse else echo "Malformed argument: ${fullopt}" >&2 echo "While parsing: $@" >&2 - cmdarg_usage >&2 + ${cmdarg_helpers['usage']} >&2 exit 1 fi if [[ "$opt" == "h" ]] || [[ "$longopt" == "help" ]]; then - cmdarg_usage >&2 + ${cmdarg_helpers['usage']} >&2 exit 1 fi @@ -285,7 +308,7 @@ function cmdarg_parse cmdarg_set_opt "${CMDARG[$opt]}" "$optarg" else echo "Unknown argument or invalid value : -${opt} | --${longopt}" >&2 - cmdarg_usage >&2 + ${cmdarg_helpers['usage']} >&2 exit 1 fi done @@ -322,7 +345,7 @@ function cmdarg_parse echo "Missing arguments : ${missing}" fi echo - cmdarg_usage >&2 + ${cmdarg_helpers['usage']} >&2 exit 1 fi @@ -403,4 +426,9 @@ declare -xA CMDARG_FLAGS declare -xA CMDARG_TYPES # Array of all elements found after -- declare -xa cmdarg_argv +# Hash of functions that are used for user-extensible functionality +declare -xA cmdarg_helpers +cmdarg_helpers['describe']=cmdarg_describe_default +cmdarg_helpers['usage']=cmdarg_usage + CMDARG_GETOPTLIST="h" diff --git a/tests/test_helpers.sh b/tests/test_helpers.sh new file mode 100644 index 0000000..d9f6bb6 --- /dev/null +++ b/tests/test_helpers.sh @@ -0,0 +1,134 @@ +#!/usr/bin/bash4 + +source $(dirname ${BASH_SOURCE[0]})/../cmdarg.sh + +function shunittest_test_usage_helper +{ + function usage_helper + { + echo "LOL I AM A HELPER" + return 0 + } + function parser { + cmdarg_purge + cmdarg_helpers['usage']=usage_helper + cmdarg_parse --help + } + [[ "$(parser 2>&1)" == "LOL I AM A HELPER" ]] || return 1 +} + +function shunittest_test_describe_helper +{ + function always_succeed + { + return 0 + } + + function describe + { + set -u + local longopt opt argtype default description + longopt=$1 + opt=$2 + argtype=$3 + default="$4" + description="$5" + flags="$6" + validator="$7" + set +u + + echo "${opt}:${longopt}:${argtype}:${description}:${default}:${flags}:${validator}" + } + cmdarg_helpers['describe']=describe + function parser + { + declare -a array + declare -A hash + cmdarg_purge + cmdarg 's:' 'string' 'some string' '12345' always_succeed + cmdarg 'b' 'boolean' 'some boolean' + cmdarg 'a?[]' 'array' 'some array' + cmdarg 'H?{}' 'hash' 'some hash' + set -x + [[ "$(cmdarg_describe s)" == "s:string:${CMDARG_TYPE_STRING}:some string:12345:${CMDARG_FLAG_REQARG}:always_succeed" ]] || return 1 + [[ "$(cmdarg_describe b)" == "b:boolean:${CMDARG_TYPE_BOOLEAN}:some boolean::${CMDARG_FLAG_NOARG}:" ]] || return 1 + [[ "$(cmdarg_describe a)" == "a:array:${CMDARG_TYPE_ARRAY}:some array::${CMDARG_FLAG_OPTARG}:" ]] || return 1 + [[ "$(cmdarg_describe H)" == "H:hash:${CMDARG_TYPE_HASH}:some hash::${CMDARG_FLAG_OPTARG}:" ]] || return 1 + set +x + } + parser +} + +# This test adds no value to the test suite, it simply serves as an example of how to override +# both the describe AND usage helpers +function shunittest_test_describe_and_usage_helper +{ + function always_succeed + { + return 0 + } + + function describe + { + set -u + local longopt opt argtype default description + longopt=$1 + opt=$2 + argtype=$3 + default="$4" + description="$5" + flags="$6" + validator="$7" + set +u + + echo "${opt}:${longopt}:${argtype}:${description}:${default}:${flags}:${validator}" + } + + function usage + { + echo "I ignore the default header and footer, and substitute my own." + echo "I do not indent my arguments or separate optional and required." + + # cmdarg helpfully separates options into OPTIONAL or REQUIRED arrays + # so that you don't have to sort the keys for uniform --help message output + # and so you can easily break arguments out into required/optional blocks + # in the usage message ... our helper doesn't care, it just prints them all + # together, but it still uses the sorted lists. + + for shortopt in ${CMDARG_OPTIONAL[@]} ${CMDARG_REQUIRED[@]} + do + cmdarg_describe $shortopt + done + } + + cmdarg_helpers['describe']=describe + cmdarg_helpers['usage']=usage + + function parser + { + declare -a array + declare -A hash + cmdarg_purge + cmdarg 's:' 'string' 'some string' '12345' always_succeed + cmdarg 'b' 'boolean' 'some boolean' + cmdarg 'a?[]' 'array' 'some array' + cmdarg 'H?{}' 'hash' 'some hash' + cmdarg_parse --help + } + output="I ignore the default header and footer, and substitute my own. +I do not indent my arguments or separate optional and required. +s:string:${CMDARG_TYPE_STRING}:some string:12345:${CMDARG_FLAG_REQARG}:always_succeed +b:boolean:${CMDARG_TYPE_BOOLEAN}:some boolean::${CMDARG_FLAG_NOARG}: +a:array:${CMDARG_TYPE_ARRAY}:some array::${CMDARG_FLAG_OPTARG}: +H:hash:${CMDARG_TYPE_HASH}:some hash::${CMDARG_FLAG_OPTARG}:" + + set +e + capture="$(parser 2>&1)" + if [[ "${capture}" != "$output" ]]; then + echo "${capture}" > /tmp/$$.parser 2>&1 + echo "${output}" > /tmp/$$.output + diff -y /tmp/$$.output /tmp/$$.parser + return 1 + fi + set -e +}