From 77698f430cc1f210fe9e4f59a3d7086c0dd1eb29 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Wed, 13 Nov 2013 11:14:09 -0800 Subject: [PATCH] Added long opt parsing, now understands -- and parses non-option elements into cmdarg_argv. Docs updated. --- README.md | 54 +++++++++++++++++++++++++++++++++++++++--- cmdarg.sh | 41 +++++++++++++++++++++++++++----- tests/test_dashdash.sh | 37 +++++++++++++++++++++++++++++ tests/test_longopt.sh | 31 ++++++++++++++++++++++++ tests/test_types.sh | 2 +- 5 files changed, 155 insertions(+), 10 deletions(-) create mode 100644 tests/test_dashdash.sh create mode 100644 tests/test_longopt.sh diff --git a/README.md b/README.md index aa04b33..0e1536f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,14 @@ This function is used to tell the library what command line arguments you accept cmdarg 'u:' 'source_ldap_username' 'Source (old) LDAP Username' cmdarg 'c:' 'groupmap' 'A CSV file mapping usernames to groups that they should belong to post-conversion' '' 'test -e $OPTARG' -All arguments are OPTIONAL by default. An argument that has ':' on the end of its single character option, and does not specify a default value (empty string is considered "not specified"), is REQUIRED. +All arguments are OPTIONAL by default. An argument that has ':' on the end of its single character option, and does not specify a default value (empty string is considered "not specified"), is REQUIRED. The arguments can be set on the command line either via '-X' or '--Y', where X is the short option and Y is the long option. Example: + + cmdarg 'r:' 'required-thing' 'Some thing I require' + + # your_script.sh -r some_thingy + # your_script.sh --required-thing some_thingy + +Because cmdarg does key off of the short options, you are limited to as many unique single characters are in your character set (likely 61 - 26 lower & upper alpha, +9 numerics). cmdarg_info =========== @@ -46,7 +53,7 @@ This command does what you expect, parsing your command line arguments. However ... Beware that "$@" will change depending on your context. So if you have a main() function called in your script, you need to make sure that you pass "$@" from the toplevel script in to it, otherwise the options will be blank when you pass them to cmdarg_parse. -Any argument parsed that has a validator assigned, and whose validator returns nonzero, is considered a failure. Any REQUIRED argument that is not specified is considered a failure. +Any argument parsed that has a validator assigned, and whose validator returns nonzero, is considered a failure. Any REQUIRED argument that is not specified is considered a failure. However, it is worth noting that if a required argument has a default value, and you provide an empty value to it, we won't know any better and that will be accepted (how do we know you didn't actually *mean* to do that?). For every argument, a global associative array "cmdarg_cfg" is populated with the long version of the option. E.g., in the example above, '-c' would become ${cmdarg_cfg['groupmap']}, for friendlier access during scripting. @@ -153,4 +160,45 @@ Given some code like this: cmdarg_cfg[source_ldap_basedn]="1" cmdarg_cfg[source_ldap_ou_users]="users" cmdarg_cfg[source_ldap]="1" - cmdarg_cfg[dest_ldap]="1" \ No newline at end of file + cmdarg_cfg[dest_ldap]="1" + +Setting arrays and hashes +========================= + +You can use the cmdarg function to accept arrays and hashes from the command line as well. Consider: + + declare -a array + declare -A hash + cmdarg 'a:[]' 'array' 'Some array you can set indexes in' + cmdarg 'H:{}' 'hash' 'Some hash you can set keys in' + + + your_script -a 32 --array something -H key=value --hash other_key=value + + + echo ${array[0]} + echo ${array[1]} + echo ${hash['key']} + echo ${hash['other_key']} + +The long option names in this form must equal the name of a previously declared array or hash, appropriately. Cmdarg populates that variable directly with options for these arguments. + +Positional arguments and -- +=========================== + +Like any good option parsing framework, cmdarg understands '--' and positional arguments that are meant to be provided without any kind of option parsing applied to them. So if you have: + + myscript.sh -x 0 --longopt thingy file1 file2 + +... It would seem reasonable to assume that -x and --longopt would be parsed as expected; with arguments of 0 and thingy. But what to do with file1 and file2? cmdarg puts those into a bash indexed array called cmdarg_argv. + +Similarly, cmdarg understands '--' which means "stop processing arguments, the rest of this stuff is just to be passed to the program directly". So in this case: + + myscript.sh -x 0 --longopt thingy -- --some-thing-with-dashes + +... 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. + +Tests +===== + +cmdarg is testable by the shunit bash unit testing tool. See the tests/ directory. diff --git a/cmdarg.sh b/cmdarg.sh index d44a1ad..dd08685 100644 --- a/cmdarg.sh +++ b/cmdarg.sh @@ -161,7 +161,7 @@ function cmdarg_set_opt arg="$2" case ${CMDARG_TYPES[$key]} in $CMDARG_TYPE_STRING) - cmdarg_cfg[$key]=$OPTARG + cmdarg_cfg[$key]=$arg ;; $CMDARG_TYPE_BOOLEAN) cmdarg_cfg[$key]=true @@ -225,19 +225,46 @@ 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 + local OPTIND parsing fullopt opt optarg longopt + + parsing=0 + while [[ "$@" != "" ]]; do + optarg="" + opt="" + longopt="" + fullopt=$1 + shift + 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]} + else + echo "Malformed argument: ${fullopt}" >&2 + echo "While parsing: $@" >&2 + cmdarg_usage + exit 1 + fi + + if [[ ${CMDARG_FLAGS[$opt]} -eq $CMDARG_FLAG_WITHARG ]]; then + optarg=$1 + shift + fi - while getopts "$CMDARG_GETOPTLIST" opt "$@"; do if [ "$opt" == "h" ]; then cmdarg_usage exit 1 elif [ ${CMDARG["${opt}"]+abc} ]; then - cmdarg_set_opt "${CMDARG[$opt]}" "$OPTARG" + cmdarg_set_opt "${CMDARG[$opt]}" "$optarg" else + echo "Unknown argument or invalid value : -${opt} | --${longopt}" >&2 cmdarg_usage exit 1 fi - OPTARG="" done # --- Don't exit early during validation, tell the user @@ -254,7 +281,7 @@ function cmdarg_parse done local opt - local optarg + local OPTARG for opt in "${!cmdarg_cfg[@]}" do shortopt=${CMDARG_REV[$opt]} @@ -351,4 +378,6 @@ declare -xA CMDARG_INFO declare -xA CMDARG_FLAGS # Map of (short arg) -> type (string, array, hash) declare -xA CMDARG_TYPES +# Array of all elements found after -- +declare -xa cmdarg_argv CMDARG_GETOPTLIST="h" diff --git a/tests/test_dashdash.sh b/tests/test_dashdash.sh new file mode 100644 index 0000000..0f027bc --- /dev/null +++ b/tests/test_dashdash.sh @@ -0,0 +1,37 @@ +#!/usr/bin/bash4 + +source $(dirname ${BASH_SOURCE[0]})/../cmdarg.sh + +function shunittest_dashdash +{ + set -x + cmdarg_purge + cmdarg_parse -- lolzors something + [[ "${cmdarg_argv[0]}" == "lolzors" ]] || return 1 + [[ "${cmdarg_argv[1]}" == "something" ]] || return 1 +} + +function shunittest_missing_dashdash +{ + set -x + cmdarg_purge + ( cmdarg_parse --lolzors ) || return 0 + return 1 +} + +function shunittest_withbool_missing_dashdash +{ + set -x + cmdarg_purge + cmdarg 'x' 'xray' 'thingy for xray' + ( cmdarg_parse -x lolzors ) || return 0 + cmdarg_parse -x -- lolzors +} + +function shunittest_withopt_with_dashdash +{ + set -x + cmdarg_purge + cmdarg 'x:' 'xray' 'thingy for xray' + ( cmdarg_parse -x -- lolzors ) || return 0 +} \ No newline at end of file diff --git a/tests/test_longopt.sh b/tests/test_longopt.sh new file mode 100644 index 0000000..9cc66fe --- /dev/null +++ b/tests/test_longopt.sh @@ -0,0 +1,31 @@ +#!/usr/bin/bash4 + +source $(dirname ${BASH_SOURCE[0]})/../cmdarg.sh + +function shunittest_longopt +{ + cmdarg_purge + cmdarg 'l:' 'long-required-opt' 'Some long opt that requires a value' + cmdarg 'o' 'long-boolean-opt' 'Some long option that is boolean' + cmdarg 'L:' 'long-required-default-opt' 'Some long opt that requires a value but has a default' '(nil)' + + cmdarg_parse --long-required-opt hooha --long-boolean-opt + + [[ "${cmdarg_cfg['long-required-opt']}" == "hooha" ]] || return 1 + [[ "${cmdarg_cfg['long-boolean-opt']}" == "true" ]] || return 1 + [[ "${cmdarg_cfg['long-required-default-opt']}" == "(nil)" ]] || return 1 +} + +function shunittest_longopt_shortopts_still_work +{ + cmdarg_purge + cmdarg 'l:' 'long-required-opt' 'Some long opt that requires a value' + cmdarg 'o' 'long-boolean-opt' 'Some long option that is boolean' + cmdarg 'L:' 'long-required-default-opt' 'Some long opt that requires a value but has a default' '(nil)' + + cmdarg_parse -l hooha -o + + [[ "${cmdarg_cfg['long-required-opt']}" == "hooha" ]] || return 1 + [[ "${cmdarg_cfg['long-boolean-opt']}" == "true" ]] || return 1 + [[ "${cmdarg_cfg['long-required-default-opt']}" == "(nil)" ]] || return 1 +} \ No newline at end of file diff --git a/tests/test_types.sh b/tests/test_types.sh index 0cd0cba..9907966 100644 --- a/tests/test_types.sh +++ b/tests/test_types.sh @@ -64,7 +64,7 @@ function shunittest_hash_values do cmp="$cmp ${k}=${hash[$k]}" done - cmp=$(echo "$cmp" | sed s/'^\s*'//) + cmp=$(echo "$cmp" | sed s/'^ *'//) if [[ "$cmp" != "$base" ]]; then echo "Hash does not contain expected arguments ($cmp vs $base)" cmdarg_dump >&2