Все решения, представленные до сих пор, помещают код в case ... in ... esac
, но, на мой взгляд, было бы гораздо более естественным иметь модифицированную команду getopts
, поэтому я написал эту функцию:
ИЗМЕНИТЬ:
Теперь вы можете указать тип необязательного аргумента (см. информацию об использовании).
Кроме того, вместо того, чтобы проверять, выглядит ли $nextArg
как параметр(ы) arg, функция теперь проверяет, содержит ли $nextArg
букву из $optstring
. Таким образом, буква параметра, не содержащаяся в $optstring
, может использоваться как необязательный аргумент, как и в случае обязательных аргументов getopts
.
Последние изменения:
- Фиксированная проверка, если
$nextArg
является параметром arg:
Проверяется, начинается ли $nextArg
с дефиса.
Без этой проверки необязательные аргументы, содержащие букву из $optstring
, не распознаются как таковые.
- Добавлен спецификатор типа регулярного выражения (см. информацию об использовании).
Использование:
Вызов: getopts-plus optstring name "$@"
optstring
: Аналогично обычным getopts, но вы можете указать параметры с необязательным аргументом, добавив :: к букве параметра.
Однако, если ваш сценарий поддерживает вызов с параметром с необязательным аргументом в качестве единственного аргумента параметра, за которым следует аргумент без параметра, аргумент без параметра будет считаться аргументом для параметра.
Если вам повезло и ожидается, что необязательный аргумент будет целым числом, а необязательный аргумент — строкой или наоборот, вы можете указать тип, добавив :::i для целого числа. или :::s для строки, чтобы решить эту проблему.
Если это неприменимо, вы можете указать регулярное выражение для необязательного аргумента, добавив ::/.../ к букве параметра.
Если после аргумента, не являющегося параметром, вариант с необязательным аргументом, он будет считаться необязательным аргументом только в том случае, если он соответствует регулярному выражению.
Для ясности: ::/.../ не предназначен для проверки аргумента но исключительно для того, чтобы различать аргументы для опций с необязательными аргументами и аргументами без опций.
#!/bin/bash
# Invocation: getopts-plus optstring name "$@"\
# \
# optstring: Like normal getopts, but you may specify options with optional argument
# by appending :: to the option letter.\
# \
# However, if your script supports an invocation with an option with optional
# argument as the only option argument, followed by a non-option argument,
# the non-option argument will be considered to be the argument for the option.\
# \
# If you're lucky and the optional argument is expected to be an integer, whereas
# the non-option argument is a string or vice versa, you may specify the type by
# appending :::i for an integer or :::s for a string to solve that issue.\
# \
# If that doesn't apply, you may specify a regexp for the optional arg by appending
# ::/.../ to the option letter.\
# If there is a non-option argument after the option with optional argument, it will
# be considered to be the optional argument only if it matches the regexp.\
# To be clear: ::/.../ is not meant for argument validation but solely to discriminate
# between arguments for options with optional argument and non-option arguments.
function getopts-plus
{
local optstring=$1
local -n name=$2
shift 2
local optionalArgSuffixRE='::(:[si]|/.*/)?'
local optionalArgTypeCaptureRE=':::([si])|::(/.*/)'
# If we pass 'opt' for 'name' (as I always do when using getopts) and there is
# also a local variable 'opt', the "outer" 'opt' will always be empty.
# I don't understand why a local variable interferes with caller's variable with
# same name in this case; however, we can easily circumvent this.
local opt_
# Extract options with optional arg
local -A isOptWithOptionalArg
while read opt_; do
# Using an associative array as set
isOptWithOptionalArg[$opt_]=1
done <<<$(grep -Eo "[a-zA-Z]$optionalArgSuffixRE" <<<$optstring | sed -E "s#$optionalArgSuffixRE##")
# Extract all option letters (used to weed out possible optional args that are option args)
local optLetters=$(sed -E "s#:|$optionalArgSuffixRE##g" <<<$optstring | grep -o '[A-Za-z]')
# Save original optstring, then remove our suffix(es)
local optstringOrg=$optstring
optstring=$(sed -E "s#$optionalArgSuffixRE##g" <<<$optstring)
getopts $optstring name "$@" || return # Return value is getopts' exit value.
# If current option is an option with optional arg and if an arg has been provided,
# check if that arg is not an option and if it isn't, check if that arg matches(*)
# the specified type, if any, and if it does or no type has been specified,
# assign it to OPTARG and inc OPTIND.
#
# (*) We detect an int because it's easy, but we assume a string if it's not an int
# because detecting a string would be complicated.
# So it sounds strange to call it a match if we know that the optional arg is specified
# to be a string, but merely that the provided arg is not an int, but in this context,
# "not an int" is equivalent to "string". At least I think so, but I might be wrong.
if ((isOptWithOptionalArg[$name])) && [[ ${!OPTIND} ]]; then
local nextArg=${!OPTIND} foundOpt=0
# Test if $nextArg is an option arg
if [[ $nextArg == -* ]]; then
# Check if $nextArg contains a letter from $optLetters.
# This way, an option not contained in $optstring can be
# used as optional arg, as with getopts' mandatory args.
local i
# Start at char 1 to skip the leading dash
for ((i = 1; i < ${#nextArg}; i++)); do
while read opt_; do
[[ ${nextArg:i:1} == $opt_ ]] && foundOpt=1 && break 2
done <<<$optLetters
done
((foundOpt)) && return
fi
# Extract type of optional arg if specified
# N.B.: We use -n option and p modifier to get an output
# only if the substitution has actually been performed.
local optArgType=$(sed -En "s#.*$name($optionalArgTypeCaptureRE).*#\2\3#p" <<<$optstringOrg)
local nextArgIsOptArg=0
case $optArgType in
/*/) # Check if $nextArg matches regexp
local optArgRE=$(sed -E "s#/(.*)/#\1#" <<<$optArgType)
[[ $nextArg =~ $optArgRE ]] && nextArgIsOptArg=1
;;
[si]) # Check if $nextArg is an int
# Use i attribute to check if $nextArg is an integer.
# N.B.: - Assigning something that is not an int
# to a variable that has the i attribute set
# has the same effect as assigning 0.
# - Trying to assign a string that contains a space,
# a \t or a \n (no matter if literally or decoded)
# to a variable that has the i attribute set fails.
local -i nextArgIsInt
# First, test if $nextArg contains a space, a \t or a \n (literally or decoded).
if [[ $nextArg =~ .*[\ \t\n$'\t'$'\n'].* ]]; then # => Not a number
nextArgIsInt=0
else
nextArgIsInt=$nextArg
fi
# Test if specified type and arg type match (see (*) above).
# N.B.: We need command groups since && and || between commands have same precedence.
{ [[ $optArgType == i ]] && ((nextArgIsInt)) || { [[ $optArgType == s ]] && ((! nextArgIsInt)); }; } && nextArgIsOptArg=1
;;
'') # No type or regexp specified => Assume $nextArg is optional arg.
nextArgIsOptArg=1
;;
esac
if ((nextArgIsOptArg)); then
OPTARG=$nextArg && ((OPTIND++))
fi
fi
}
Та же функция без комментариев, если они вам не нужны:
#!/bin/bash
# Invocation: getopts-plus optstring name "$@"\
# \
# optstring: Like normal getopts, but you may specify options with optional argument
# by appending :: to the option letter.\
# \
# However, if your script supports an invocation with an option with optional
# argument as the only option argument, followed by a non-option argument,
# the non-option argument will be considered to be the argument for the option.\
# \
# If you're lucky and the optional argument is expected to be an integer, whereas
# the non-option argument is a string or vice versa, you may specify the type by
# appending :::i for an integer or :::s for a string to solve that issue.\
# \
# If that doesn't apply, you may specify a regexp for the optional arg by appending
# ::/.../ to the option letter.\
# If there is a non-option argument after the option with optional argument, it will
# be considered to be the optional argument only if it matches the regexp.\
# To be clear: ::/.../ is not meant for argument validation but solely to discriminate
# between arguments for options with optional argument and non-option arguments.
function getopts-plus
{
local optstring=$1
local -n name=$2
shift 2
local optionalArgSuffixRE='::(:[si]|/.*/)?'
local optionalArgTypeCaptureRE=':::([si])|::(/.*/)'
local opt_
local -A isOptWithOptionalArg
while read opt_; do
isOptWithOptionalArg[$opt_]=1
done <<<$(grep -Eo "[a-zA-Z]$optionalArgSuffixRE" <<<$optstring | sed -E "s#$optionalArgSuffixRE##")
local optLetters=$(sed -E "s#:|$optionalArgSuffixRE##g" <<<$optstring | grep -o '[A-Za-z]')
local optstringOrg=$optstring
optstring=$(sed -E "s#$optionalArgSuffixRE##g" <<<$optstring)
getopts $optstring name "$@" || return
if ((isOptWithOptionalArg[$name])) && [[ ${!OPTIND} ]]; then
local nextArg=${!OPTIND} foundOpt=0
if [[ $nextArg == -* ]]; then
local i
for ((i = 1; i < ${#nextArg}; i++)); do
while read opt_; do
[[ ${nextArg:i:1} == $opt_ ]] && foundOpt=1 && break 2
done <<<$optLetters
done
((foundOpt)) && return
fi
local optArgType=$(sed -En "s#.*$name($optionalArgTypeCaptureRE).*#\2\3#p" <<<$optstringOrg)
local nextArgIsOptArg=0
case $optArgType in
/*/)
local optArgRE=$(sed -E "s#/(.*)/#\1#" <<<$optArgType)
[[ $nextArg =~ $optArgRE ]] && nextArgIsOptArg=1
;;
[si])
local -i nextArgIsInt
if [[ $nextArg =~ .*[\ \t\n$'\t'$'\n'].* ]]; then
nextArgIsInt=0
else
nextArgIsInt=$nextArg
fi
{ [[ $optArgType == i ]] && ((nextArgIsInt)) || { [[ $optArgType == s ]] && ((! nextArgIsInt)); }; } && nextArgIsOptArg=1
;;
'')
nextArgIsOptArg=1
;;
esac
if ((nextArgIsOptArg)); then
OPTARG=$nextArg && ((OPTIND++))
fi
fi
}
Некоторые тесты:
Необязательный тип аргумента -g
, указанный как целое число, целое число не передается, но за которым следует неопциональная строка arg.
$ . ./getopts-plus.sh
$ while getopts-plus 'b:c::de::f::g:::ia' opt -ab 99 -c 11 -def 55 -g "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == '' <-- Empty because "hello you" is not an int
Как и выше, но с int arg.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ia' opt -ab 99 -c 11 -def 55 -g 7 "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == '7' <-- The passed int
Добавлен необязательный параметр -h
с регулярным выражением ^(a|b|ab|ba)$
, аргумент не передается.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/a' opt -ab 99 -c 11 -def 55 -gh "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == ''
-------------------------
opt == 'h'
OPTARG == '' <-- Empty because "hello you" does not match the regexp
Как и выше, но с аргументом, соответствующим регулярному выражению.
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/a' opt -ab 99 -c 11 -def 55 -gh ab "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
opt == 'a'
OPTARG == ''
-------------------------
opt == 'b'
OPTARG == '99'
-------------------------
opt == 'c'
OPTARG == '11'
-------------------------
opt == 'd'
OPTARG == ''
-------------------------
opt == 'e'
OPTARG == ''
-------------------------
opt == 'f'
OPTARG == '55'
-------------------------
opt == 'g'
OPTARG == ''
-------------------------
opt == 'h'
OPTARG == 'ab' <-- The arg that matches the regexp
person
Christoph
schedule
06.07.2021