lib: Update option parser to better support short options

This commit is contained in:
Ethan P 2020-10-29 01:24:32 -07:00
parent bb94b33353
commit 65a2f7561e
No known key found for this signature in database
GPG Key ID: 6963FD04F6CF35EA
3 changed files with 347 additions and 51 deletions

View File

@ -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
# Pop array.
((_ARGV_INDEX++))
# Handle short options.
if [[ "$OPT" =~ ^-[^-]{2,} ]]; then
case "$SHIFTOPT_SHORT_OPTIONS" in
# PASS mode: "-abc=0" -> ("-abc=0")
PASS) _shiftopt_next ;;
# 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,14 +149,14 @@ 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
fi
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]}"
((_ARGV_INDEX++))
_shiftopt_next
fi
# Error if no value is provided.
if [[ "$OPT_VAL" =~ -.* ]]; then

View File

@ -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" \

View File

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