到目前为止提供的所有解决方案都将代码放入case ... in ... esac
,但在我看来,修改命令会更自然getopts
,因此我编写了这个函数:
编辑:
现在,您可以指定可选参数的类型(请参阅使用信息)。
此外,$nextArg
该函数现在不再测试是否“看起来像”一个选项 arg,而是检查是否$nextArg
包含来自$optstring
. 这样,不包含在其中的选项字母$optstring
可以用作可选参数,与getopts
' 强制参数一样。
最新变化:
- 固定测试 if
$nextArg
是一个选项 arg:
测试是否$nextArg
以破折号开头。如果没有此测试,则无法识别
包含 from 字母的可选参数。$optstring
- 添加了正则表达式类型说明符(请参阅使用信息)。
- 固定:0 未被识别为指定为 int 的可选 arg。
- 简化测试 if
$nextArg
是一个 int。
- 类型说明符
::/.../:
用于perl
测试是否$nextArg
匹配正则表达式。
这样,您就可以从(几乎 (*))Perl 正则表达式的全部功能中受益。
(*):见最后一段使用信息。
- 已修复:不适用于一个以上的正则表达式类型说明符:
使用perl
而不是grep
/sed
构造,因为需要非贪婪匹配。
用法:
调用:getopts-plus optstring name "$@"
optstring
: 和普通的一样,但是你可以通过在选项字母后面附加::getopts
来指定带有可选参数的选项。
但是,如果您的脚本支持使用带有可选参数作为唯一选项参数的选项的调用,然后是非选项参数,则非选项参数将被视为该选项的参数。
如果你很幸运并且可选参数应该是一个整数,而非选项参数是一个字符串,反之亦然,你可以通过附加:::i为整数或:::s来指定类型字符串来解决这个问题。
如果这不适用,您可以通过将::/.../附加到选项字母来为可选参数指定 Perl 正则表达式。
有关 Perl 正则表达式的介绍,请参见此处:https ://perldoc.perl.org/perlretut
请注意:ATM,只会/.../
在 之后被识别为正则表达式::
,即不能使用其他分隔符,也不能使用修饰符,因此 egm#...#a
不会认可。
如果在带有可选参数的选项之后有一个非选项参数,那么只有匹配正则表达式时才会被认为是可选参数。
需要明确的是:::/.../
并非用于参数验证,而仅用于区分具有可选参数和非选项参数的选项的参数。
#!/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 Perl regexp for the optional arg by appending
# ::/.../ to the option letter.\
# See here for an introduction to Perl regexps: https://perldoc.perl.org/perlretut
# Please note: ATM, only /.../ will be recognised as a regexp after ::,\
# i. e. neither other delimiters, nor modifiers may be used, so e. g. m#...#a will
# not be recognised.\
# 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 <<<$(perlGetCaptures "$optstring" "([a-zA-Z])$optionalArgSuffixRE")
# Extract all option letters (used to weed out possible optional args that are option args)
local optLetters=$(perlGetCaptures "$optstring" "([a-zA-Z])(?:$optionalArgSuffixRE|:)?")
# Save original optstring, then remove our suffix(es)
local optstringOrg=$optstring
optstring=$(perl -pe "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
local optArgType=$(perlGetCaptures "$optstringOrg" "$name(?:$optionalArgTypeCaptureRE)" '$1$2')
local nextArgIsOptArg=0
case $optArgType in
/*/) # Check if $nextArg matches regexp
perlMatch "$nextArg" "$optArgType" && nextArgIsOptArg=1
;;
[si]) # Check if $nextArg is an int
local nextArgIsInt=0
[[ $nextArg =~ ^[0-9]+$ ]] && nextArgIsInt=1
# 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
}
# Uses perl to match \<string\> against \<regexp\>.\
# Returns with code 0 on a match and 1 otherwise.
function perlMatch # Args: <string> <regexp>
{
perl -e 'q('"$1"') =~ '"$2"' and exit 0; exit 1;'
}
# Uses perl to match \<string\> against \<regexp\>
# and prints each capture on a separate line.\
# If \<regexp\> contains more than one capture group,
# you must specify the \<line format\> which is an
# arbitrary Perl string containing your desired backrefs.\
# By default, merely $1 will be printed.
function perlGetCaptures # Args: <string> <regexp> [<line format>]
{
local lineFmt=${3:-\$1}
# Matching repeatedly with g option gives one set of captures at a time.
perl -e 'while (q('"$1"') =~ m#'"$2"'#g) { print(qq('"$lineFmt"') . "\n"); }'
}
如果您不需要它们,则函数体内没有注释的相同脚本:
#!/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 Perl regexp for the optional arg by appending
# ::/.../ to the option letter.\
# See here for an introduction to Perl regexps: https://perldoc.perl.org/perlretut
# Please note: ATM, only /.../ will be recognised as a regexp after ::,\
# i. e. neither other delimiters, nor modifiers may be used, so e. g. m#...#a will
# not be recognised.\
# 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 <<<$(perlGetCaptures "$optstring" "([a-zA-Z])$optionalArgSuffixRE")
local optLetters=$(perlGetCaptures "$optstring" "([a-zA-Z])(?:$optionalArgSuffixRE|:)?")
local optstringOrg=$optstring
optstring=$(perl -pe "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=$(perlGetCaptures "$optstringOrg" "$name(?:$optionalArgTypeCaptureRE)" '$1$2')
local nextArgIsOptArg=0
case $optArgType in
/*/)
perlMatch "$nextArg" "$optArgType" && nextArgIsOptArg=1
;;
[si])
local nextArgIsInt=0
[[ $nextArg =~ ^[0-9]+$ ]] && nextArgIsInt=1
{ [[ $optArgType == i ]] && ((nextArgIsInt)) || { [[ $optArgType == s ]] && ((! nextArgIsInt)); }; } && nextArgIsOptArg=1
;;
'')
nextArgIsOptArg=1
;;
esac
if ((nextArgIsOptArg)); then
OPTARG=$nextArg && ((OPTIND++))
fi
fi
}
# Uses perl to match \<string\> against \<regexp\>.\
# Returns with code 0 on a match and 1 otherwise.
function perlMatch # Args: <string> <regexp>
{
perl -e 'q('"$1"') =~ '"$2"' and exit 0; exit 1;'
}
# Uses perl to match \<string\> against \<regexp\>
# and prints each capture on a separate line.\
# If \<regexp\> contains more than one capture group,
# you must specify the \<line format\> which is an
# arbitrary Perl string containing your desired backrefs.\
# By default, merely $1 will be printed.
function perlGetCaptures # Args: <string> <regexp> [<line format>]
{
local lineFmt=${3:-\$1}
perl -e 'while (q('"$1"') =~ m#'"$2"'#g) { print(qq('"$lineFmt"') . "\n"); }'
}
使用最新版本的一些测试:
指定为整数的可选 arg 类型-g
,不传递 int 但后跟非选项字符串 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
添加了带有 regexp的可选选项/^(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
像上面一样,但 arg 匹配正则表达式。
$ 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
-i
添加了另一个带有正则表达式的正则表达式类型的可选选项/^\w+$/
(使用 Perl 标记\w
,表示字母数字或下划线),没有传递参数。
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/ai::/^\w+$/' opt -ab 99 -c 11 -def 55 -gh ab -i "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
[23:10:49]
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'
-------------------------
opt == 'i'
OPTARG == '' <-- Empty because "hello you" contains a space.
像上面一样,但 arg 匹配正则表达式。
$ OPTIND=1
$ while getopts-plus 'b:c::de::f::g:::ih::/^(a|b|ab|ba)$/ai::/^\w+$/' opt -ab 99 -c 11 -def 55 -gh ab -i foo_Bar_1 "hello you"; do e opt OPTARG; echo; printf "%.0s-" $(seq 1 25); echo -e "\n"; done
[23:15:23]
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'
-------------------------
opt == 'i'
OPTARG == 'foo_Bar_1' <-- Matched because it contains only alphanumeric chars and underscores.