From 65a2f7561e383819020aa83d2e15f78d28f9d423 Mon Sep 17 00:00:00 2001 From: Ethan P Date: Thu, 29 Oct 2020 01:24:32 -0700 Subject: [PATCH] lib: Update option parser to better support short options --- lib/opt.sh | 88 ++++++++++-- src/batgrep.sh | 8 +- test/suite/lib_opt.sh | 302 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 347 insertions(+), 51 deletions(-) diff --git a/lib/opt.sh b/lib/opt.sh index 3d7198a..3042760 100644 --- a/lib/opt.sh +++ b/lib/opt.sh @@ -13,12 +13,38 @@ source "${LIB}/constants.sh" # option will be transparently skipped instead of handled. SHIFTOPT_HOOKS=() +# A setting to change how `shiftopt` will interpret short options that consist +# of more than one character. +# +# Values: +# +# SPLIT -- Splits the option into multiple single-character short options. +# "-abc" -> ("-a" "-b" "-c") +# +# VALUE -- Uses the remaining characters as the value for the short option. +# "-abc" -> ("-a=bc") +# +# CONV -- Converts the argument to a long option. +# "-abc" -> ("--abc") +# +# PASS -- Pass the argument along as-is. +# "-abc" -> ("-abc") +# +SHIFTOPT_SHORT_OPTIONS="VALUE" + # Sets the internal _ARGV, _ARGV_INDEX, and _ARGV_LAST variables used when # parsing options with the shiftopt and shiftval functions. +# +# Arguments: +# ... -- The program arguments. +# +# Example: +# setargs "--long=3" "file.txt" setargs() { _ARGV=("$@") _ARGV_LAST="$((${#_ARGV[@]} - 1))" _ARGV_INDEX=0 + _ARGV_SUBINDEX=1 } # Resets the internal _ARGV* variables to the original script arguments. @@ -27,6 +53,14 @@ resetargs() { setargs "${_ARGV_ORIGINAL[@]}" } +# INTERNAL. +# +# Increments the argv index pointer used by `shiftopt`. +_shiftopt_next() { + _ARGV_SUBINDEX=1 + ((_ARGV_INDEX++)) || true +} + # Gets the next option passed to the script. # # Variables: @@ -47,13 +81,47 @@ shiftopt() { OPT="${_ARGV[$_ARGV_INDEX]}" unset OPT_VAL - if [[ "$OPT" =~ ^--[a-zA-Z0-9_-]+=.* ]]; then + if [[ "$OPT" =~ ^-[a-zA-Z0-9_-]+=.* ]]; then OPT_VAL="${OPT#*=}" OPT="${OPT%%=*}" fi + + # Handle short options. + if [[ "$OPT" =~ ^-[^-]{2,} ]]; then + case "$SHIFTOPT_SHORT_OPTIONS" in + # PASS mode: "-abc=0" -> ("-abc=0") + PASS) _shiftopt_next ;; - # Pop array. - ((_ARGV_INDEX++)) + # CONV mode: "-abc=0" -> ("--abc=0") + CONV) OPT="-${OPT}"; _shiftopt_next ;; + + # VALUE mode: "-abc=0" -> ("-a=bc=0") + VALUE) { + OPT="${_ARGV[$_ARGV_INDEX]}" + OPT_VAL="${OPT:2}" + OPT="${OPT:0:2}" + _shiftopt_next + } ;; + + # SPLIT mode: "-abc=0" -> ("-a=0" "-b=0" "-c=0") + SPLIT) { + OPT="-${OPT:$_ARGV_SUBINDEX:1}" + ((_ARGV_SUBINDEX++)) || true + if [[ "$_ARGV_SUBINDEX" -gt "${#OPT}" ]]; then + _shiftopt_next + fi + } ;; + + # ????? mode: Treat it as pass. + *) + printf "shiftopt: unknown SHIFTOPT_SHORT_OPTIONS mode '%s'" \ + "$SHIFTOPT_SHORT_OPTIONS" 1>&2 + _shiftopt_next + ;; + esac + else + _shiftopt_next + fi # Handle hooks. local hook @@ -81,15 +149,15 @@ shiftval() { return 0 fi - # If it's a short flag followed by a number, use the number. - if [[ "$OPT" =~ ^-[[:alpha:]][[:digit:]]{1,}$ ]]; then - OPT_VAL="${OPT:2}" - return + if [[ "$_ARGV_SUBINDEX" -gt 1 && "$SHIFTOPT_SHORT_OPTIONS" = "SPLIT" ]]; then + # If it's a short group argument in SPLIT mode, we grab the next argument. + OPT_VAL="${_ARGV[$((_ARGV_INDEX+1))]}" + else + # Otherwise, we can handle it normally. + OPT_VAL="${_ARGV[$_ARGV_INDEX]}" + _shiftopt_next fi - OPT_VAL="${_ARGV[$_ARGV_INDEX]}" - ((_ARGV_INDEX++)) - # Error if no value is provided. if [[ "$OPT_VAL" =~ -.* ]]; then printc "%{RED}%s: '%s' requires a value%{CLEAR}\n" "$PROGRAM" "$ARG" diff --git a/src/batgrep.sh b/src/batgrep.sh index 2d4c9a3..64bd703 100755 --- a/src/batgrep.sh +++ b/src/batgrep.sh @@ -94,9 +94,9 @@ while shiftopt; do -s | --case-sensitive) OPT_CASE_SENSITIVITY="--case-sensitive" ;; -S | --smart-case) OPT_CASE_SENSITIVITY="--smart-case" ;; - -A* | --after-context) shiftval; OPT_CONTEXT_AFTER="$OPT_VAL" ;; - -B* | --before-context) shiftval; OPT_CONTEXT_BEFORE="$OPT_VAL" ;; - -C* | --context) + -A | --after-context) shiftval; OPT_CONTEXT_AFTER="$OPT_VAL" ;; + -B | --before-context) shiftval; OPT_CONTEXT_BEFORE="$OPT_VAL" ;; + -C | --context) shiftval OPT_CONTEXT_BEFORE="$OPT_VAL" OPT_CONTEXT_AFTER="$OPT_VAL" @@ -249,7 +249,7 @@ main() { --with-filename \ --vimgrep \ "${RG_ARGS[@]}" \ - --context=0 \ + --context 0 \ --no-context-separator \ --sort path \ "$PATTERN" \ diff --git a/test/suite/lib_opt.sh b/test/suite/lib_opt.sh index 6d04240..f0a0eb4 100644 --- a/test/suite/lib_opt.sh +++ b/test/suite/lib_opt.sh @@ -1,14 +1,13 @@ setup() { - set - pos1 \ - --val1 for_val1 \ - --val2=for_val2 \ - pos2 \ - --flag1 \ - -v3=for_val3 \ - -v4 \ - -v55 \ - -vn for_val4 \ - --flag2 + set - --long-implicit implicit_value \ + --long-explicit=explicit_value \ + -i implicit \ + -x=explicit \ + -I1 group_implicit \ + -X2=group_explicit \ + positional_1 \ + positional_2 \ + --after-positional source "${LIB}/print.sh" source "${LIB}/opt.sh" @@ -31,7 +30,7 @@ test:long() { description "Parse long options." while shiftopt; do - if [[ "$OPT" = "--flag1" ]]; then + if [[ "$OPT" = "--long-implicit" ]]; then assert_opt_valueless return fi @@ -40,13 +39,13 @@ test:long() { fail 'Failed to find option.' } -test:long_value_implicit() { +test:long_implicit() { description "Parse long options in '--long value' syntax." while shiftopt; do - if [[ "$OPT" = "--val1" ]]; then + if [[ "$OPT" = "--long-implicit" ]]; then shiftval - assert_opt_value 'for_val1' + assert_opt_value 'implicit_value' return fi done @@ -54,13 +53,14 @@ test:long_value_implicit() { fail 'Failed to find option.' } -test:long_value_explicit() { +test:long_explicit() { description "Parse long options in '--long=value' syntax." while shiftopt; do - if [[ "$OPT" = "--val2" ]]; then + if [[ "$OPT" = "--long-explicit" ]]; then + assert_opt_value 'explicit_value' shiftval - assert_opt_value 'for_val2' + assert_opt_value 'explicit_value' return fi done @@ -68,13 +68,12 @@ test:long_value_explicit() { fail 'Failed to find option.' } -test:short_value_implicit_number() { - description "Parse short options in '-k0' syntax." +test:short_default() { + description "Parse short options in '-k' syntax." while shiftopt; do - if [[ "$OPT" = "-v4" ]]; then - shiftval - assert_opt_value '4' + if [[ "$OPT" = "-i" ]]; then + assert_opt_valueless return fi done @@ -82,14 +81,13 @@ test:short_value_implicit_number() { fail 'Failed to find option.' } - -test:short_value_implicit_number2() { - description "Parse short options in '-k0' syntax." +test:short() { + description "Parse short options in '-k val' syntax." while shiftopt; do - if [[ "$OPT" = "-v55" ]]; then - shiftval - assert_opt_value '55' + if [[ "$OPT" = "-i" ]]; then + shiftopt + assert_opt_valueless return fi done @@ -97,13 +95,13 @@ test:short_value_implicit_number2() { fail 'Failed to find option.' } -test:short_value_implicit() { +test:short_implicit() { description "Parse short options in '-k value' syntax." while shiftopt; do - if [[ "$OPT" = "-vn" ]]; then + if [[ "$OPT" = "-i" ]]; then shiftval - assert_opt_value 'for_val4' + assert_opt_value 'implicit' return fi done @@ -111,12 +109,14 @@ test:short_value_implicit() { fail 'Failed to find option.' } -test:short_value_explicit() { +test:short_explicit() { description "Parse short options in '-k=value' syntax." while shiftopt; do - if [[ "$OPT" =~ ^-v3 ]]; then - assert_equal "$OPT" "-v3=for_val3" + if [[ "$OPT" = "-x" ]]; then + assert_opt_value 'explicit' + shiftval + assert_opt_value 'explicit' return fi done @@ -124,6 +124,234 @@ test:short_value_explicit() { fail 'Failed to find option.' } +test:short_default_mode() { + description "Ensure the default mode for '-abc' is VALUE." + assert_equal "$SHIFTOPT_SHORT_OPTIONS" "VALUE" +} + +test:short_split_none() { + description "Parse short options in '-abc' syntax with SPLIT mode." + SHIFTOPT_SHORT_OPTIONS="SPLIT" + + local found=0 + while shiftopt; do + case "$OPT" in + "-I"|"-1") + assert_opt_valueless + ((found++)) || true + ;; + esac + done + + assert_equal "$found" 2 +} + +test:short_split_implicit() { + description "Parse short options in '-abc val' syntax with SPLIT mode." + SHIFTOPT_SHORT_OPTIONS="SPLIT" + + local found=0 + while shiftopt; do + case "$OPT" in + "-I"|"-1") + assert_opt_valueless + shiftval + assert_opt_value "group_implicit" + ((found++)) || true + ;; + esac + done + + assert_equal "$found" 2 +} + +test:short_split_explicit() { + description "Parse short options in '-abc=val' syntax with SPLIT mode." + SHIFTOPT_SHORT_OPTIONS="SPLIT" + + local found=0 + while shiftopt; do + case "$OPT" in + "-X"|"-2") + assert_opt_value "group_explicit" + ((found++)) || true + ;; + esac + done + + assert_equal "$found" 2 +} + +test:short_pass_none() { + description "Parse short options in '-abc' syntax with PASS mode." + SHIFTOPT_SHORT_OPTIONS="PASS" + + local found=0 + while shiftopt; do + case "$OPT" in + "-I"|"-1") fail "Short group -I1 was split." ;; + "-I1") + assert_opt_valueless + ((found++)) || true + ;; + esac + done + + assert_equal "$found" 1 +} + +test:short_pass_implicit() { + description "Parse short options in '-abc val' syntax with PASS mode." + SHIFTOPT_SHORT_OPTIONS="PASS" + + local found=0 + while shiftopt; do + case "$OPT" in + "-I"|"-1") fail "Short group -I1 was split." ;; + "-I1") + assert_opt_valueless + shiftval + assert_opt_value "group_implicit" + ((found++)) || true + ;; + esac + done + + assert_equal "$found" 1 +} + +test:short_pass_explicit() { + description "Parse short options in '-abc=val' syntax with PASS mode." + SHIFTOPT_SHORT_OPTIONS="PASS" + + local found=0 + while shiftopt; do + case "$OPT" in + "-X"|"-2") fail "Short group -X2 was split." ;; + "-X2") + assert_opt_value "group_explicit" + ((found++)) || true + ;; + esac + done + + assert_equal "$found" 1 +} + +test:short_conv_none() { + description "Parse short options in '-abc' syntax with CONV mode." + SHIFTOPT_SHORT_OPTIONS="CONV" + + local found=0 + while shiftopt; do + case "$OPT" in + "-I"|"-1") fail "Short group -I1 was split." ;; + "-I1") fail "Short group -I1 was not converted." ;; + "--I1") + assert_opt_valueless + ((found++)) || true + ;; + esac + done + + assert_equal "$found" 1 +} + +test:short_conv_implicit() { + description "Parse short options in '-abc val' syntax with CONV mode." + SHIFTOPT_SHORT_OPTIONS="CONV" + + local found=0 + while shiftopt; do + case "$OPT" in + "-I"|"-1") fail "Short group -I1 was split." ;; + "-I1") fail "Short group -I1 was not converted." ;; + "--I1") + assert_opt_valueless + shiftval + assert_opt_value "group_implicit" + ((found++)) || true + ;; + esac + done + + assert_equal "$found" 1 +} + +test:short_conv_explicit() { + description "Parse short options in '-abc=val' syntax with CONV mode." + SHIFTOPT_SHORT_OPTIONS="CONV" + + local found=0 + while shiftopt; do + case "$OPT" in + "-X"|"-2") fail "Short group -X2 was split." ;; + "-X2") fail "Short group -X2 was not converted." ;; + "--X2") + assert_opt_value "group_explicit" + ((found++)) || true + ;; + esac + done + + assert_equal "$found" 1 +} + +test:short_value_none() { + description "Parse short options in '-abc' syntax with VALUE mode." + SHIFTOPT_SHORT_OPTIONS="VALUE" + + local found=0 + while shiftopt; do + case "$OPT" in + "-I1") fail "Short group -I1 was not truncated." ;; + "-I") + assert_opt_value "1" + ((found++)) || true + ;; + esac + done + + assert_equal "$found" 1 +} + +test:short_value_implicit() { + description "Parse short options in '-abc val' syntax with VALUE mode." + SHIFTOPT_SHORT_OPTIONS="VALUE" + + local found=0 + while shiftopt; do + case "$OPT" in + "-I1") fail "Short group -I1 was not truncated." ;; + "-I") + shiftval + assert_opt_value "1" + ((found++)) || true + ;; + esac + done + + assert_equal "$found" 1 +} + +test:short_value_explicit() { + description "Parse short options in '-abc=val' syntax with VALUE mode." + SHIFTOPT_SHORT_OPTIONS="VALUE" + + local found=0 + while shiftopt; do + case "$OPT" in + "-X2") fail "Short group -X2 was not truncated." ;; + "-X") + assert_opt_value "2=group_explicit" + ((found++)) || true + ;; + esac + done + + assert_equal "$found" 1 +} + test:hook() { description "Option hooks." @@ -131,7 +359,7 @@ test:hook() { found=false example_hook() { - if [[ "$OPT" = "pos1" ]]; then + if [[ "$OPT" = "--long-implicit" ]]; then found=true return 0 fi @@ -139,7 +367,7 @@ test:hook() { } while shiftopt; do - if [[ "$OPT" = "pos1" ]]; then + if [[ "$OPT" = "--long-implicit" ]]; then fail "Option was not filtered by hook." fi done @@ -167,5 +395,5 @@ test:fn_resetargs() { resetargs shiftopt || true - assert_opt_name "pos1" + assert_opt_name "--long-implicit" }