我希望使用我的 shell 脚本调用长短形式的命令行选项。
我知道可以使用 getopts
,但就像在 Perl 中一样,我无法对 shell 做同样的事情。
关于如何做到这一点的任何想法,以便我可以使用以下选项:
./shell.sh --copyfile abc.pl /tmp/
./shell.sh -c abc.pl /tmp/
在上面,这两个命令对我的 shell 意味着同样的事情,但是使用 getopts
,我无法实现这些?
getopt
和 getopts
是不同的野兽,人们似乎对他们的所作所为有点误解。 getopts
是 bash
的内置命令,用于循环处理命令行选项,并将找到的每个选项和值依次分配给内置变量,以便您进一步处理它们。但是,getopt
是一个外部实用程序,它实际上不会像 bash getopts
、Perl Getopt
模块或 Python {8 这样的方式为您处理选项 }/argparse
模块。 getopt
所做的只是规范化传入的选项——即将它们转换为更标准的形式,以便 shell 脚本更容易处理它们。例如,getopt
的应用程序可能会转换以下内容:
myscript -ab infile.txt -ooutfile.txt
进入这个:
myscript -a -b -o outfile.txt infile.txt
您必须自己进行实际处理。如果您对指定选项的方式进行各种限制,则根本不必使用 getopt
:
每个参数只放一个选项;
所有选项都在任何位置参数之前(即非选项参数);
对于带有值的选项(例如上面的 -o),该值必须作为单独的参数(在空格之后)。
为什么使用 getopt
而不是 getopts
?基本原因是只有 GNU getopt
为您提供对长名称命令行选项的支持。1(GNU getopt
是 Linux 上的默认设置。Mac OS X 和 FreeBSD 带有基本的并且不是很有用 getopt
,但可以安装 GNU 版本;见下文。)
例如,这是一个使用 GNU getopt
的示例,来自我的一个名为 javawrap
的脚本:
# NOTE: This requires GNU getopt. On Mac OS X and FreeBSD, you have to install this
# separately; see below.
TEMP=$(getopt -o vdm: --long verbose,debug,memory:,debugfile:,minheap:,maxheap: \
-n 'javawrap' -- "$@")
if [ $? != 0 ] ; then echo "Terminating..." >&2 ; exit 1 ; fi
# Note the quotes around '$TEMP': they are essential!
eval set -- "$TEMP"
VERBOSE=false
DEBUG=false
MEMORY=
DEBUGFILE=
JAVA_MISC_OPT=
while true; do
case "$1" in
-v | --verbose ) VERBOSE=true; shift ;;
-d | --debug ) DEBUG=true; shift ;;
-m | --memory ) MEMORY="$2"; shift 2 ;;
--debugfile ) DEBUGFILE="$2"; shift 2 ;;
--minheap )
JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MinHeapFreeRatio=$2"; shift 2 ;;
--maxheap )
JAVA_MISC_OPT="$JAVA_MISC_OPT -XX:MaxHeapFreeRatio=$2"; shift 2 ;;
-- ) shift; break ;;
* ) break ;;
esac
done
这使您可以指定像 --verbose -dm4096 --minh=20 --maxhe 40 --debugfi="/Users/John Johnson/debug.txt"
或类似的选项。调用 getopt
的效果是将选项规范化为 --verbose -d -m 4096 --minheap 20 --maxheap 40 --debugfile "/Users/John Johnson/debug.txt"
,以便您可以更轻松地处理它们。 "$1"
和 "$2"
周围的引用很重要,因为它可以确保正确处理带有空格的参数。
如果您删除前 9 行(从 eval set
行开始的所有内容),代码将仍然有效!但是,您的代码在接受什么样的选项时会更加挑剔:特别是,您必须以上述“规范”形式指定所有选项。但是,通过使用 getopt
,您可以对单字母选项进行分组,使用更短且不含歧义的长选项形式,使用 --file foo.txt
或 --file=foo.txt
样式,使用 -m 4096
或 {6 } 样式,以任何顺序混合选项和非选项等。如果发现无法识别或模棱两可的选项,getopt
也会输出错误消息。
注意:实际上有两个完全不同的版本的 getopt
,基本 getopt
和 GNU getopt
,具有不同的功能和不同的调用约定。2 Basic getopt
很糟糕:它不仅不能处理长选项,甚至不能处理参数内的嵌入空格或空参数,而 getopts
确实做到了这一点。上述代码在基本 getopt
中不起作用。 GNU getopt
默认安装在 Linux 上,但在 Mac OS X 和 FreeBSD 上需要单独安装。在 Mac OS X 上,安装 MacPorts (http://www.macports.org),然后执行 sudo port install getopt
以安装 GNU getopt
(通常安装到 /opt/local/bin
),并确保 /opt/local/bin
在您的 shell 路径中位于 /usr/bin
之前。在 FreeBSD 上,安装 misc/getopt
。
为您自己的程序修改示例代码的快速指南:在前几行中,除了调用 getopt
的行之外,所有的都是“样板”,应该保持不变。您应该在 -n
后更改程序名称,在 -o
后指定短选项,在 --long
后指定长选项。在取值的选项后面加一个冒号。
最后,如果您看到只有 set
而不是 eval set
的代码,那么它是为 BSD getopt
编写的。您应该将其更改为使用 eval set
样式,该样式适用于两个版本的 getopt
,而普通的 set
不适用于 GNU getopt
。
1实际上,ksh93
中的 getopts
支持长名称选项,但这个 shell 不像 bash
那样经常使用。在 zsh
中,使用 zparseopts
获得此功能。
2从技术上讲,“GNU getopt
”是用词不当;这个版本实际上是为 Linux 而不是 GNU 项目编写的。但是,它遵循所有 GNU 约定,并且通常使用术语“GNU getopt
”(例如在 FreeBSD 上)。
可以考虑三种实现方式:
Bash 内置 getopts。这不支持带有双破折号前缀的长选项名称。它仅支持单字符选项。
独立 getopt 命令的 BSD UNIX 实现(这是 MacOS 使用的)。这也不支持长选项。
独立 getopt 的 GNU 实现。 GNU getopt(3)(由 Linux 上的命令行 getopt(1) 使用)支持解析长选项。
其他一些答案显示了使用 bash 内置 getopts
模拟长选项的解决方案。该解决方案实际上是一个简短的选项,其字符为“-”。所以你得到“--”作为标志。然后后面的任何内容都变为 OPTARG,然后您使用嵌套的 case
测试 OPTARG。
这很聪明,但有一些警告:
getopts 不能强制执行 opt 规范。如果用户提供了一个无效的选项,它就不能返回错误。在解析 OPTARG 时,您必须自己进行错误检查。
OPTARG 用于长选项名称,当您的长选项本身有参数时,这会使使用变得复杂。您最终不得不自己编写代码作为附加案例。
因此,虽然可以编写更多代码来解决对长选项缺乏支持的问题,但这需要更多的工作,并且部分违背了使用 getopt 解析器来简化代码的目的。
Bash 内置的 getopts 函数可用于解析长选项,方法是在 optspec 中放置一个破折号,后跟一个冒号:
#!/usr/bin/env bash
optspec=":hv-:"
while getopts "$optspec" optchar; do
case "${optchar}" in
-)
case "${OPTARG}" in
loglevel)
val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
echo "Parsing option: '--${OPTARG}', value: '${val}'" >&2;
;;
loglevel=*)
val=${OPTARG#*=}
opt=${OPTARG%=$val}
echo "Parsing option: '--${opt}', value: '${val}'" >&2
;;
*)
if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
echo "Unknown option --${OPTARG}" >&2
fi
;;
esac;;
h)
echo "usage: $0 [-v] [--loglevel[=]<value>]" >&2
exit 2
;;
v)
echo "Parsing option: '-${optchar}'" >&2
;;
*)
if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
echo "Non-option argument: '-${OPTARG}'" >&2
fi
;;
esac
done
在 current working directory 中复制到可执行文件 name=getopts_test.sh
后,可以产生如下输出
$ ./getopts_test.sh
$ ./getopts_test.sh -f
Non-option argument: '-f'
$ ./getopts_test.sh -h
usage: code/getopts_test.sh [-v] [--loglevel[=]<value>]
$ ./getopts_test.sh --help
$ ./getopts_test.sh -v
Parsing option: '-v'
$ ./getopts_test.sh --very-bad
$ ./getopts_test.sh --loglevel
Parsing option: '--loglevel', value: ''
$ ./getopts_test.sh --loglevel 11
Parsing option: '--loglevel', value: '11'
$ ./getopts_test.sh --loglevel=11
Parsing option: '--loglevel', value: '11'
显然 getopts 既不执行 OPTERR
检查也不执行长选项的选项参数解析。上面的脚本片段显示了如何手动完成。基本原理也适用于 Debian Almquist shell(“dash”)。注意特殊情况:
getopts -- "-:" ## without the option terminator "-- " bash complains about "-:"
getopts "-:" ## this works in the Debian Almquist shell ("dash")
请注意,正如 http://mywiki.wooledge.org/BashFAQ 上的 GreyCat 指出的那样,这个技巧利用了 shell 的非标准行为,它允许将选项参数(即“-f 文件名”中的文件名)连接到选项(如在“-ffilename”中)。 POSIX 标准规定它们之间必须有一个空格,在“--longoption”的情况下,它将终止选项解析并将所有 longoptions 转换为非选项参数。
val="${!OPTIND}
中的 !
的语义是什么?
getopts
仅自动将 OPTIND
增加 1,但在我们的示例中,我们需要它增加 2,因此我们手动将其增加 1,然后让 getopts
再次为我们自动增加 1。
$
。 OPTIND=$(( $OPTIND + 1 ))
可以只是 OPTIND=$(( OPTIND + 1 ))
。更有趣的是,您甚至可以在算术表达式中分配和增加变量,因此可以将其进一步缩写为 : $(( ++OPTIND ))
,甚至 (( ++OPTIND ))
考虑到 ++OPTIND
将始终为正,因此它不会使用 -e
选项触发 shell 运行。 :-) gnu.org/software/bash/manual/html_node/Shell-Arithmetic.html
--very-bad
不发出警告?
内置的 getopts
命令仍然是 AFAIK,仅限于单字符选项。
有(或曾经有)一个外部程序 getopt
,它将重新组织一组选项,以便更容易解析。您也可以调整该设计以处理长选项。示例用法:
aflag=no
bflag=no
flist=""
set -- $(getopt abf: "$@")
while [ $# -gt 0 ]
do
case "$1" in
(-a) aflag=yes;;
(-b) bflag=yes;;
(-f) flist="$flist $2"; shift;;
(--) shift; break;;
(-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
(*) break;;
esac
shift
done
# Process remaining non-option arguments
...
您可以通过 getoptlong
命令使用类似的方案。
请注意,外部 getopt
程序的根本弱点是难以处理其中包含空格的参数,并且难以准确地保留这些空格。这就是内置 getopts
优越的原因,尽管受限于它仅处理单字母选项这一事实。
eval set
(请参阅下面的答案),以便它也可以与 GNU getopt(Linux 上的默认设置)一起正常工作并正确处理空格。
${1+"$@"}
的使用很奇怪,并且与现代 shell 中的必要功能,特别是与您在 Linux 上找到的任何 shell 不一致。请参阅 Using $1:+"$@"} in /bin/sh 以了解有关该符号的讨论。)
标准的内置 getopts
可以将长选项解析为 -
“选项”的“参数”
这是可移植的原生 POSIX shell——不需要外部程序或 bashism。
本指南将长选项实现为 -
选项的参数,因此 getopts
将 --alpha
视为具有参数 alpha
的 -
,而将 --bravo=foo
视为具有参数 bravo=foo
的 -
。真正的参数是通过 shell 参数扩展获取的,更新 $OPT
和 $OPTARG
。
在此示例中,-b
和 -c
(及其长形式 --bravo
和 --charlie
)具有强制参数。长选项的参数出现在等号之后,例如 --bravo=foo
(长选项的空格分隔符很难实现,见下文)。
因为它使用 getopts
builtin,所以此解决方案支持像 cmd --bravo=foo -ac FILE
之类的用法(它结合了选项 -a
和 -c
并将长选项与标准选项交错),而这里的大多数其他答案要么难以做到这一点,要么无法做到这一点。
die() { echo "$*" >&2; exit 2; } # complain to STDERR and exit with error
needs_arg() { if [ -z "$OPTARG" ]; then die "No arg for --$OPT option"; fi; }
while getopts ab:c:-: OPT; do
# support long options: https://stackoverflow.com/a/28466267/519360
if [ "$OPT" = "-" ]; then # long option: reformulate OPT and OPTARG
OPT="${OPTARG%%=*}" # extract long option name
OPTARG="${OPTARG#$OPT}" # extract long option argument (may be empty)
OPTARG="${OPTARG#=}" # if long option argument, remove assigning `=`
fi
case "$OPT" in
a | alpha ) alpha=true ;;
b | bravo ) needs_arg; bravo="$OPTARG" ;;
c | charlie ) needs_arg; charlie="$OPTARG" ;;
??* ) die "Illegal option --$OPT" ;; # bad long option
? ) exit 2 ;; # bad short option (error reported via getopts)
esac
done
shift $((OPTIND-1)) # remove parsed options and args from $@ list
When the option is a dash (-
), it is a long option. getopts
会将实际的长选项解析为 $OPTARG
,例如 --bravo=foo
最初设置 OPT='-'
和 OPTARG='bravo=foo'
。 if
节将 $OPT
设置为第一个等号之前的 $OPTARG
的内容(在我们的示例中为 bravo
),然后从 $OPTARG
的开头删除它(在此步骤中产生 =foo
,或如果没有 =
,则为空字符串)。最后,我们去掉参数的前导 =
。此时,$OPT
要么是短选项(一个字符),要么是长选项(2+ 个字符)。
case
然后匹配短选项或长选项。对于短选项,getopts
会自动抱怨选项和缺少参数,因此我们必须使用 needs_arg
函数手动复制这些选项,当 $OPTARG
为空时该函数会致命地退出。 ??*
条件将匹配任何剩余的长选项(?
匹配单个字符,*
匹配零个或多个,因此 ??*
匹配 2+ 个字符),允许我们在退出之前发出“非法选项”错误.
小错误:如果有人给出了一个无效的单字符长选项(而且它也不是一个短选项),这将退出并出现错误但没有消息(此实现假定它是一个短选项)。您可以在 case
之前的条件中使用一个额外的变量来跟踪它,然后在最终的情况下对其进行测试,但我认为这太过分了。
大写变量名: 通常,建议保留全大写变量以供系统使用。我将 $OPT
保持为全大写以使其与 $OPTARG
保持一致,但这确实违反了该约定。我认为它适合,因为这是系统应该做的事情,而且应该是安全的;我还没有听说过使用这个变量名的任何标准。
抱怨对长选项的意外参数:使用翻转测试模拟 needs_arg
以抱怨不期望的参数:
no_arg() { if [ -n "$OPTARG" ]; then die "No arg allowed for --$OPT option"; fi; }
要接受带有空格分隔参数的长选项: 您可以使用 eval "ARG_B=\"\$$OPTIND\""
(或使用 bash 的 indirect expansion、ARG_B="${!OPTIND}"
)拉入下一个参数,然后增加 $OPTIND
,如older version of this answer,但不可靠; getopts
可能会在参数超出其范围的假设下提前终止,并且某些实现不适合手动操作 $OPTIND
。
letter-c
不需要参数,那么使用letter-c)
还不够吗? *
似乎是多余的。
getopts
在第一个位置参数处停止,因为它不是为处理它们而设计的。这允许子命令具有自己的参数,例如 git diff --color
,因此我将 command --foo=moo bar --baz waz
解释为将 --foo
作为 command
的参数,将 --baz waz
作为参数(带选项)到 {7 } 子命令。这可以通过上面的代码来完成。我拒绝 --bravo -blah
,因为 --bravo
需要一个参数,并且不清楚 -blah
不是另一个选项。
eval
,所以它列在答案的其余部分下方。
这是一个实际使用带有长选项的 getopt 的示例:
aflag=no
bflag=no
cargument=none
# options may be followed by one colon to indicate they have a required argument
if ! options=$(getopt -o abc: -l along,blong,clong: -- "$@")
then
# something went wrong, getopt will put out an error message for us
exit 1
fi
set -- $options
while [ $# -gt 0 ]
do
case $1 in
-a|--along) aflag="yes" ;;
-b|--blong) bflag="yes" ;;
# for options with required arguments, an additional shift is required
-c|--clong) cargument="$2" ; shift;;
(--) shift; break;;
(-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
(*) break;;
esac
shift
done
eval set
(请参阅下面的答案),以便它也可以与 GNU getopt(Linux 上的默认设置)一起正常工作并正确处理空格。
getopt
而问题是关于 getopts
。
(--
、(-*
和 (*
是有效模式吗?它们与 --
、-*
和 *
有何不同?
(--)
与 case
节中的 --)
相同。看到可选前导括号的不均匀缩进和不一致使用很奇怪,但答案的当前代码对我来说似乎有效。
将 getopts 与短/长选项和参数一起使用
适用于所有组合,例如:
foobar -f --bar
foobar --foo -b
foobar -bf --bar --foobar
foobar -fbFBAshorty --bar -FB --arguments=longhorn
foobar -fA "文字矮" -B --arguments="文字长角"
bash foobar -F --barfoo
sh foobar -B --foobar - ...
bash ./foobar -F --bar
此示例的一些声明
Options=$@
Optnum=$#
sfoo='no '
sbar='no '
sfoobar='no '
sbarfoo='no '
sarguments='no '
sARG=empty
lfoo='no '
lbar='no '
lfoobar='no '
lbarfoo='no '
larguments='no '
lARG=empty
Usage 函数的外观
function _usage()
{
###### U S A G E : Help and ERROR ######
cat <<EOF
foobar $Options
$*
Usage: foobar <[options]>
Options:
-b --bar Set bar to yes ($foo)
-f --foo Set foo to yes ($bart)
-h --help Show this message
-A --arguments=... Set arguments to yes ($arguments) AND get ARGUMENT ($ARG)
-B --barfoo Set barfoo to yes ($barfoo)
-F --foobar Set foobar to yes ($foobar)
EOF
}
[ $# = 0 ] && _usage " >>>>>>>> no options given "
带有长/短标志以及长参数的 getop
while getopts ':bfh-A:BF' OPTION ; do
case "$OPTION" in
b ) sbar=yes ;;
f ) sfoo=yes ;;
h ) _usage ;;
A ) sarguments=yes;sARG="$OPTARG" ;;
B ) sbarfoo=yes ;;
F ) sfoobar=yes ;;
- ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
eval OPTION="\$$optind"
OPTARG=$(echo $OPTION | cut -d'=' -f2)
OPTION=$(echo $OPTION | cut -d'=' -f1)
case $OPTION in
--foo ) lfoo=yes ;;
--bar ) lbar=yes ;;
--foobar ) lfoobar=yes ;;
--barfoo ) lbarfoo=yes ;;
--help ) _usage ;;
--arguments ) larguments=yes;lARG="$OPTARG" ;;
* ) _usage " Long: >>>>>>>> invalid options (long) " ;;
esac
OPTIND=1
shift
;;
? ) _usage "Short: >>>>>>>> invalid options (short) " ;;
esac
done
输出
##################################################################
echo "----------------------------------------------------------"
echo "RESULT short-foo : $sfoo long-foo : $lfoo"
echo "RESULT short-bar : $sbar long-bar : $lbar"
echo "RESULT short-foobar : $sfoobar long-foobar : $lfoobar"
echo "RESULT short-barfoo : $sbarfoo long-barfoo : $lbarfoo"
echo "RESULT short-arguments: $sarguments with Argument = \"$sARG\" long-arguments: $larguments and $lARG"
将以上内容组合成一个有凝聚力的脚本
#!/bin/bash
# foobar: getopts with short and long options AND arguments
function _cleanup ()
{
unset -f _usage _cleanup ; return 0
}
## Clear out nested functions on exit
trap _cleanup INT EXIT RETURN
###### some declarations for this example ######
Options=$@
Optnum=$#
sfoo='no '
sbar='no '
sfoobar='no '
sbarfoo='no '
sarguments='no '
sARG=empty
lfoo='no '
lbar='no '
lfoobar='no '
lbarfoo='no '
larguments='no '
lARG=empty
function _usage()
{
###### U S A G E : Help and ERROR ######
cat <<EOF
foobar $Options
$*
Usage: foobar <[options]>
Options:
-b --bar Set bar to yes ($foo)
-f --foo Set foo to yes ($bart)
-h --help Show this message
-A --arguments=... Set arguments to yes ($arguments) AND get ARGUMENT ($ARG)
-B --barfoo Set barfoo to yes ($barfoo)
-F --foobar Set foobar to yes ($foobar)
EOF
}
[ $# = 0 ] && _usage " >>>>>>>> no options given "
##################################################################
####### "getopts" with: short options AND long options #######
####### AND short/long arguments #######
while getopts ':bfh-A:BF' OPTION ; do
case "$OPTION" in
b ) sbar=yes ;;
f ) sfoo=yes ;;
h ) _usage ;;
A ) sarguments=yes;sARG="$OPTARG" ;;
B ) sbarfoo=yes ;;
F ) sfoobar=yes ;;
- ) [ $OPTIND -ge 1 ] && optind=$(expr $OPTIND - 1 ) || optind=$OPTIND
eval OPTION="\$$optind"
OPTARG=$(echo $OPTION | cut -d'=' -f2)
OPTION=$(echo $OPTION | cut -d'=' -f1)
case $OPTION in
--foo ) lfoo=yes ;;
--bar ) lbar=yes ;;
--foobar ) lfoobar=yes ;;
--barfoo ) lbarfoo=yes ;;
--help ) _usage ;;
--arguments ) larguments=yes;lARG="$OPTARG" ;;
* ) _usage " Long: >>>>>>>> invalid options (long) " ;;
esac
OPTIND=1
shift
;;
? ) _usage "Short: >>>>>>>> invalid options (short) " ;;
esac
done
eval
方法来处理长选项的间隔参数,发现它在某些 shell 上不可靠(尽管我希望它可以与 bash 一起使用,在这种情况下你不必使用 {2 })。请参阅 my answer,了解如何使用 =
接受长选项参数以及我注意到的使用空间的尝试。我的解决方案不会拨打外部电话,而这个解决方案使用 cut
几次。
看看 shFlags,它是一个可移植的 shell 库(意思是:Linux、Solaris 等上的 sh、bash、dash、ksh、zsh)。
它使添加新标志就像在脚本中添加一行一样简单,并且它提供了自动生成的使用功能。
这是一个使用 shFlag 的简单 Hello, world!
:
#!/bin/sh
# source shflags from current directory
. ./shflags
# define a 'name' command-line string flag
DEFINE_string 'name' 'world' 'name to say hello to' 'n'
# parse the command-line
FLAGS "$@" || exit 1
eval set -- "${FLAGS_ARGV}"
# say hello
echo "Hello, ${FLAGS_name}!"
对于具有支持长选项的增强 getopt 的操作系统(例如 Linux),您可以执行以下操作:
$ ./hello_world.sh --name Kate
Hello, Kate!
对于其余部分,您必须使用 short 选项:
$ ./hello_world.sh -n Kate
Hello, Kate!
添加新标志就像添加新 DEFINE_ call
一样简单。
另一种方式...
# translate long options to short
for arg
do
delim=""
case "$arg" in
--help) args="${args}-h ";;
--verbose) args="${args}-v ";;
--config) args="${args}-c ";;
# pass through anything else
*) [[ "${arg:0:1}" == "-" ]] || delim="\""
args="${args}${delim}${arg}${delim} ";;
esac
done
# reset the translated args
eval set -- $args
# now we can process with getopt
while getopts ":hvc:" opt; do
case $opt in
h) usage ;;
v) VERBOSE=true ;;
c) source $OPTARG ;;
\?) usage ;;
:)
echo "option -$OPTARG requires an argument"
usage
;;
esac
done
$args
重新分配中不需要一个空格吗?这甚至可以在没有 bashism 的情况下完成,但是此代码将丢失选项和参数中的空格(我认为 $delim
技巧不会起作用)。如果您足够小心,仅在第一次迭代时将其清空,则可以改为在 for
循环中运行 set
inside。 Here is a safer version 没有 bashisms。
我是这样解决的:
# A string with command options
options=$@
# An array with all the arguments
arguments=($options)
# Loop index
index=0
for argument in $options
do
# Incrementing index
index=`expr $index + 1`
# The conditions
case $argument in
-a) echo "key $argument value ${arguments[index]}" ;;
-abc) echo "key $argument value ${arguments[index]}" ;;
esac
done
exit;
我是傻还是怎么的? getopt
和 getopts
太混乱了。
-ltr
或 -lt -r
以及 -l -t -r
)。它还提供了一些错误处理,以及在选项处理完成后将处理过的参数移走的简单方法。
如果您不想要 getopt
依赖项,您可以这样做:
while test $# -gt 0
do
case $1 in
# Normal option processing
-h | --help)
# usage and help
;;
-v | --version)
# version info
;;
# ...
# Special cases
--)
break
;;
--*)
# error unknown (long) option $1
;;
-?)
# error unknown (short) option $1
;;
# FUN STUFF HERE:
# Split apart combined short options
-*)
split=$1
shift
set -- $(echo "$split" | cut -c 2- | sed 's/./-& /g') "$@"
continue
;;
# Done with options
*)
break
;;
esac
# for testing purposes:
echo "$1"
shift
done
当然,你不能用一个破折号来使用长样式选项。如果您想添加缩短的版本(例如 --verbos 而不是 --verbose),那么您需要手动添加它们。
但是,如果您希望获得 getopts
功能以及长选项,这是一种简单的方法。
我还将此代码段放在 gist 中。
--)
中似乎缺少 shift ;
。目前 --
将保留为第一个非选项参数。
--
选项需要一个 shift
。我说这更好,因为替代方案是 getopt
或 getopts_long
的平台相关版本,或者您必须强制仅在命令开头使用短选项(即 - 您使用 getopts
然后处理 long之后的选项),而这给出了任何顺序和完全控制。
内置的 getopts
无法做到这一点。有一个外部 getopt(1) 程序可以执行此操作,但您只能在 Linux 上从 util-linux 包中获取它。它带有一个示例脚本getopt-parse.bash。
还有一个 getopts_long
写成一个 shell 函数。
getopt
于 1993 年包含在 FreeBSD 版本 1.0 中,从那时起一直是 FreeBSD 的一部分。因此,它从 FreeBSD 4.x 被采用,并包含在 Apple 的 Darwin 项目中。从 OS X 10.6.8 开始,Apple 包含的手册页与 FreeBSD 手册页完全相同。所以是的,它包含在 OS X 和除 Linux 之外的其他操作系统中。 -1 在这个错误信息的答案上。
#!/bin/bash
while getopts "abc:d:" flag
do
case $flag in
a) echo "[getopts:$OPTIND]==> -$flag";;
b) echo "[getopts:$OPTIND]==> -$flag";;
c) echo "[getopts:$OPTIND]==> -$flag $OPTARG";;
d) echo "[getopts:$OPTIND]==> -$flag $OPTARG";;
esac
done
shift $((OPTIND-1))
echo "[otheropts]==> $@"
exit
.
#!/bin/bash
until [ -z "$1" ]; do
case $1 in
"--dlong")
shift
if [ "${1:1:0}" != "-" ]
then
echo "==> dlong $1"
shift
fi;;
*) echo "==> other $1"; shift;;
esac
done
exit
"${1:0:1}"
,索引为 0 的子字符串,长度为 1。这不允许混合短选项和长选项。
在 ksh93
中,getopts
确实支持长名称...
while getopts "f(file):s(server):" flag
do
echo "$flag" $OPTIND $OPTARG
done
或者我发现的教程是这样说的。试试看。
发明另一个版本的轮子……
这个函数是 GNU getopt 的(希望是)与 POSIX 兼容的普通 bourne shell 替代品。它支持可以接受强制/可选/无参数的短/长选项,并且指定选项的方式几乎与 GNU getopt 相同,因此转换很简单。
当然,这仍然是要放入脚本的相当大的代码块,但它大约是众所周知的 getopt_long shell 函数的一半,并且在您只想替换现有的 GNU getopt 使用的情况下可能更可取。
这是相当新的代码,所以 YMMV(如果出于任何原因这实际上与 POSIX 不兼容,请务必告诉我——可移植性从一开始就是目的,但我没有有用的 POSIX 测试环境)。
代码和示例用法如下:
#!/bin/sh
# posix_getopt shell function
# Author: Phil S.
# Version: 1.0
# Created: 2016-07-05
# URL: http://stackoverflow.com/a/37087374/324105
# POSIX-compatible argument quoting and parameter save/restore
# http://www.etalabs.net/sh_tricks.html
# Usage:
# parameters=$(save "$@") # save the original parameters.
# eval "set -- ${parameters}" # restore the saved parameters.
save () {
local param
for param; do
printf %s\\n "$param" \
| sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/"
done
printf %s\\n " "
}
# Exit with status $1 after displaying error message $2.
exiterr () {
printf %s\\n "$2" >&2
exit $1
}
# POSIX-compatible command line option parsing.
# This function supports long options and optional arguments, and is
# a (largely-compatible) drop-in replacement for GNU getopt.
#
# Instead of:
# opts=$(getopt -o "$shortopts" -l "$longopts" -- "$@")
# eval set -- ${opts}
#
# We instead use:
# opts=$(posix_getopt "$shortopts" "$longopts" "$@")
# eval "set -- ${opts}"
posix_getopt () { # args: "$shortopts" "$longopts" "$@"
local shortopts longopts \
arg argtype getopt nonopt opt optchar optword suffix
shortopts="$1"
longopts="$2"
shift 2
getopt=
nonopt=
while [ $# -gt 0 ]; do
opt=
arg=
argtype=
case "$1" in
# '--' means don't parse the remaining options
( -- ) {
getopt="${getopt}$(save "$@")"
shift $#
break
};;
# process short option
( -[!-]* ) { # -x[foo]
suffix=${1#-?} # foo
opt=${1%$suffix} # -x
optchar=${opt#-} # x
case "${shortopts}" in
( *${optchar}::* ) { # optional argument
argtype=optional
arg="${suffix}"
shift
};;
( *${optchar}:* ) { # required argument
argtype=required
if [ -n "${suffix}" ]; then
arg="${suffix}"
shift
else
case "$2" in
( -* ) exiterr 1 "$1 requires an argument";;
( ?* ) arg="$2"; shift 2;;
( * ) exiterr 1 "$1 requires an argument";;
esac
fi
};;
( *${optchar}* ) { # no argument
argtype=none
arg=
shift
# Handle multiple no-argument parameters combined as
# -xyz instead of -x -y -z. If we have just shifted
# parameter -xyz, we now replace it with -yz (which
# will be processed in the next iteration).
if [ -n "${suffix}" ]; then
eval "set -- $(save "-${suffix}")$(save "$@")"
fi
};;
( * ) exiterr 1 "Unknown option $1";;
esac
};;
# process long option
( --?* ) { # --xarg[=foo]
suffix=${1#*=} # foo (unless there was no =)
if [ "${suffix}" = "$1" ]; then
suffix=
fi
opt=${1%=$suffix} # --xarg
optword=${opt#--} # xarg
case ",${longopts}," in
( *,${optword}::,* ) { # optional argument
argtype=optional
arg="${suffix}"
shift
};;
( *,${optword}:,* ) { # required argument
argtype=required
if [ -n "${suffix}" ]; then
arg="${suffix}"
shift
else
case "$2" in
( -* ) exiterr 1 \
"--${optword} requires an argument";;
( ?* ) arg="$2"; shift 2;;
( * ) exiterr 1 \
"--${optword} requires an argument";;
esac
fi
};;
( *,${optword},* ) { # no argument
if [ -n "${suffix}" ]; then
exiterr 1 "--${optword} does not take an argument"
fi
argtype=none
arg=
shift
};;
( * ) exiterr 1 "Unknown option $1";;
esac
};;
# any other parameters starting with -
( -* ) exiterr 1 "Unknown option $1";;
# remember non-option parameters
( * ) nonopt="${nonopt}$(save "$1")"; shift;;
esac
if [ -n "${opt}" ]; then
getopt="${getopt}$(save "$opt")"
case "${argtype}" in
( optional|required ) {
getopt="${getopt}$(save "$arg")"
};;
esac
fi
done
# Generate function output, suitable for:
# eval "set -- $(posix_getopt ...)"
printf %s "${getopt}"
if [ -n "${nonopt}" ]; then
printf %s "$(save "--")${nonopt}"
fi
}
示例用法:
# Process command line options
shortopts="hvd:c::s::L:D"
longopts="help,version,directory:,client::,server::,load:,delete"
#opts=$(getopt -o "$shortopts" -l "$longopts" -n "$(basename $0)" -- "$@")
opts=$(posix_getopt "$shortopts" "$longopts" "$@")
if [ $? -eq 0 ]; then
#eval set -- ${opts}
eval "set -- ${opts}"
while [ $# -gt 0 ]; do
case "$1" in
( -- ) shift; break;;
( -h|--help ) help=1; shift; break;;
( -v|--version ) version_help=1; shift; break;;
( -d|--directory ) dir=$2; shift 2;;
( -c|--client ) useclient=1; client=$2; shift 2;;
( -s|--server ) startserver=1; server_name=$2; shift 2;;
( -L|--load ) load=$2; shift 2;;
( -D|--delete ) delete=1; shift;;
esac
done
else
shorthelp=1 # getopt returned (and reported) an error.
fi
eval set
究竟做了什么。安全吗?
set -- ...
命令,dash 的手册页解释如下:“set 命令的第三个用途是将 shell 的位置参数的值设置为指定的 args。要更改位置参数不改变任何选项,使用“--”作为第一个参数来设置。” (POSIX shell 文档也这么说,但不太清楚。)
eval
是必需的,因为我们(GNU getopt 和 posix_getopt)都支持包含特殊字符的选项,因为它们会生成 shell-quoted values 字符串,一旦eval
使用,就会产生原始值再次(现在被处理成准备处理的标准化格式)。例如,--foo="hello, world."
中的空格否则会成为问题。 posix_getopt
的返回值必须以演示的方式通过 eval
才能工作。
echo "set -- ${opts}"
代替 eval "set -- ${opts}"
来查看 将 评估什么。
我只是不时编写 shell 脚本并且没有实践,因此感谢任何反馈。
使用@Arvid Requate 提出的策略,我们注意到一些用户错误。忘记包含值的用户会意外地将下一个选项的名称视为值:
./getopts_test.sh --loglevel= --toc=TRUE
将导致“loglevel”的值被视为“--toc=TRUE”。这是可以避免的。
我从 http://mwiki.wooledge.org/BashFAQ/035 关于手动解析的讨论中调整了一些关于检查 CLI 用户错误的想法。我在处理“-”和“--”参数时插入了错误检查。
然后我开始摆弄语法,所以这里的任何错误都是我的错,而不是原作者。
我的方法可以帮助喜欢在有或没有等号的情况下输入 long 的用户。也就是说,它对“--loglevel 9”的响应应该与“--loglevel = 9”相同。在 --/space 方法中,无法确定用户是否忘记了参数,因此需要进行一些猜测。
如果用户具有长/等号格式 (--opt=),则 = 后的空格会触发错误,因为未提供参数。如果用户有长/空格参数(--opt),如果没有参数(命令结束)或参数以破折号开头,则此脚本会导致失败)
如果您从这个开始,“--opt=value”和“--opt value”格式之间存在一个有趣的区别。使用等号,命令行参数被视为“opt=value”,处理字符串解析的工作是在“=”处分隔。相反,使用“--opt value”,参数的名称是“opt”,我们面临着获取命令行中提供的下一个值的挑战。这就是@Arvid Requate 使用间接引用 ${!OPTIND} 的地方。我仍然不明白这一点,好吧,BashFAQ 中的评论似乎警告这种风格(http://mywiki.wooledge.org/BashFAQ/006)。顺便说一句,我不认为之前发帖者关于 OPTIND=$(( $OPTIND + 1 )) 重要性的评论是正确的。我的意思是说,我认为忽略它并没有什么坏处。
在此脚本的最新版本中,标志 -v 表示 VERBOSE 打印输出。
将其保存在一个名为“cli-5.sh”的文件中,使其可执行,并且其中任何一个都可以正常工作,或者以所需的方式失败
./cli-5.sh -v --loglevel=44 --toc TRUE
./cli-5.sh -v --loglevel=44 --toc=TRUE
./cli-5.sh --loglevel 7
./cli-5.sh --loglevel=8
./cli-5.sh -l9
./cli-5.sh --toc FALSE --loglevel=77
./cli-5.sh --toc=FALSE --loglevel=77
./cli-5.sh -l99 -t yyy
./cli-5.sh -l 99 -t yyy
这是对用户 intpu 进行错误检查的示例输出
$ ./cli-5.sh --toc --loglevel=77
ERROR: toc value must not have dash at beginning
$ ./cli-5.sh --toc= --loglevel=77
ERROR: value for toc undefined
您应该考虑打开 -v,因为它会打印出 OPTIND 和 OPTARG 的内部信息
#/usr/bin/env bash
## Paul Johnson
## 20171016
##
## Combines ideas from
## https://stackoverflow.com/questions/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options
## by @Arvid Requate, and http://mwiki.wooledge.org/BashFAQ/035
# What I don't understand yet:
# In @Arvid REquate's answer, we have
# val="${!OPTIND}"; OPTIND=$(( $OPTIND + 1 ))
# this works, but I don't understand it!
die() {
printf '%s\n' "$1" >&2
exit 1
}
printparse(){
if [ ${VERBOSE} -gt 0 ]; then
printf 'Parse: %s%s%s\n' "$1" "$2" "$3" >&2;
fi
}
showme(){
if [ ${VERBOSE} -gt 0 ]; then
printf 'VERBOSE: %s\n' "$1" >&2;
fi
}
VERBOSE=0
loglevel=0
toc="TRUE"
optspec=":vhl:t:-:"
while getopts "$optspec" OPTCHAR; do
showme "OPTARG: ${OPTARG[*]}"
showme "OPTIND: ${OPTIND[*]}"
case "${OPTCHAR}" in
-)
case "${OPTARG}" in
loglevel) #argument has no equal sign
opt=${OPTARG}
val="${!OPTIND}"
## check value. If negative, assume user forgot value
showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
if [[ "$val" == -* ]]; then
die "ERROR: $opt value must not have dash at beginning"
fi
## OPTIND=$(( $OPTIND + 1 )) # CAUTION! no effect?
printparse "--${OPTARG}" " " "${val}"
loglevel="${val}"
shift
;;
loglevel=*) #argument has equal sign
opt=${OPTARG%=*}
val=${OPTARG#*=}
if [ "${OPTARG#*=}" ]; then
printparse "--${opt}" "=" "${val}"
loglevel="${val}"
## shift CAUTION don't shift this, fails othewise
else
die "ERROR: $opt value must be supplied"
fi
;;
toc) #argument has no equal sign
opt=${OPTARG}
val="${!OPTIND}"
## check value. If negative, assume user forgot value
showme "OPTIND is {$OPTIND} {!OPTIND} has value \"${!OPTIND}\""
if [[ "$val" == -* ]]; then
die "ERROR: $opt value must not have dash at beginning"
fi
## OPTIND=$(( $OPTIND + 1 )) #??
printparse "--${opt}" " " "${val}"
toc="${val}"
shift
;;
toc=*) #argument has equal sign
opt=${OPTARG%=*}
val=${OPTARG#*=}
if [ "${OPTARG#*=}" ]; then
toc=${val}
printparse "--$opt" " -> " "$toc"
##shift ## NO! dont shift this
else
die "ERROR: value for $opt undefined"
fi
;;
help)
echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2
exit 2
;;
*)
if [ "$OPTERR" = 1 ] && [ "${optspec:0:1}" != ":" ]; then
echo "Unknown option --${OPTARG}" >&2
fi
;;
esac;;
h|-\?|--help)
## must rewrite this for all of the arguments
echo "usage: $0 [-v] [--loglevel[=]<value>] [--toc[=]<TRUE,FALSE>]" >&2
exit 2
;;
l)
loglevel=${OPTARG}
printparse "-l" " " "${loglevel}"
;;
t)
toc=${OPTARG}
;;
v)
VERBOSE=1
;;
*)
if [ "$OPTERR" != 1 ] || [ "${optspec:0:1}" = ":" ]; then
echo "Non-option argument: '-${OPTARG}'" >&2
fi
;;
esac
done
echo "
After Parsing values
"
echo "loglevel $loglevel"
echo "toc $toc"
OPTIND=$(( $OPTIND + 1 ))
:每当您“吞噬” OPTIND 的参数时都需要它(例如:当使用 --toc value
时:值在参数编号 $OPTIND 中。一旦您检索到它的 toc 值,您应该告诉 getopts 下一个参数to parse 不是值,但是在它之后的那个(因此 : OPTIND=$(( $OPTIND + 1 ))
. 和你的脚本(以及你引用的脚本)在 done : shift $(( $OPTIND -1 ))
之后丢失了(因为 getopts 在解析参数 1 后退出)到 OPTIND-1,您需要将它们移出,因此 $@
现在是任何剩余的“非选项”参数
接受的答案很好地指出了 bash 内置 getopts
的所有缺点。答案以:
因此,虽然可以编写更多代码来解决对长选项缺乏支持的问题,但这需要更多的工作,并且部分违背了使用 getopt 解析器来简化代码的目的。
尽管我原则上同意该声明,但我觉得我们在各种脚本中实现此功能的次数证明了为创建“标准化”、经过良好测试的解决方案付出一些努力是合理的。
因此,我通过在纯 bash 中实现 getopts_long
来“升级”内置于 getopts
的 bash,没有外部依赖项。该函数的使用与内置的getopts
100% 兼容。
通过在脚本中包含 getopts_long
(即 hosted on GitHub),原始问题的答案可以简单地实现为:
source "${PATH_TO}/getopts_long.bash"
while getopts_long ':c: copyfile:' OPTKEY; do
case ${OPTKEY} in
'c'|'copyfile')
echo 'file supplied -- ${OPTARG}'
;;
'?')
echo "INVALID OPTION -- ${OPTARG}" >&2
exit 1
;;
':')
echo "MISSING ARGUMENT for option -- ${OPTARG}" >&2
exit 1
;;
*)
echo "Misconfigured OPTSPEC or uncaught option -- ${OPTKEY}" >&2
exit 1
;;
esac
done
shift $(( OPTIND - 1 ))
[[ "${1}" == "--" ]] && shift
我还没有足够的代表来评论或投票支持他的解决方案,但 sme's answer 对我来说非常有效。我遇到的唯一问题是参数最终用单引号括起来(所以我把它们去掉了)。
我还添加了一些示例用法和帮助文本。我将在此处包含我的稍微扩展的版本:
#!/bin/bash
# getopt example
# from: https://stackoverflow.com/questions/402377/using-getopts-in-bash-shell-script-to-get-long-and-short-command-line-options
HELP_TEXT=\
" USAGE:\n
Accepts - and -- flags, can specify options that require a value, and can be in any order. A double-hyphen (--) will stop processing options.\n\n
Accepts the following forms:\n\n
getopt-example.sh -a -b -c value-for-c some-arg\n
getopt-example.sh -c value-for-c -a -b some-arg\n
getopt-example.sh -abc some-arg\n
getopt-example.sh --along --blong --clong value-for-c -a -b -c some-arg\n
getopt-example.sh some-arg --clong value-for-c\n
getopt-example.sh
"
aflag=false
bflag=false
cargument=""
# options may be followed by one colon to indicate they have a required argument
if ! options=$(getopt -o abc:h\? -l along,blong,help,clong: -- "$@")
then
# something went wrong, getopt will put out an error message for us
exit 1
fi
set -- $options
while [ $# -gt 0 ]
do
case $1 in
-a|--along) aflag=true ;;
-b|--blong) bflag=true ;;
# for options with required arguments, an additional shift is required
-c|--clong) cargument="$2" ; shift;;
-h|--help|-\?) echo -e $HELP_TEXT; exit;;
(--) shift; break;;
(-*) echo "$0: error - unrecognized option $1" 1>&2; exit 1;;
(*) break;;
esac
shift
done
# to remove the single quotes around arguments, pipe the output into:
# | sed -e "s/^'\\|'$//g" (just leading/trailing) or | sed -e "s/'//g" (all)
echo aflag=${aflag}
echo bflag=${bflag}
echo cargument=${cargument}
while [ $# -gt 0 ]
do
echo arg=$1
shift
if [[ $aflag == true ]]; then
echo a is true
fi
done
在这里,您可以找到在 bash 中进行复杂选项解析的几种不同方法:http://mywiki.wooledge.org/ComplexOptionParsing
我确实创建了以下一个,我认为这是一个很好的,因为它是最少的代码,并且长短选项都有效。使用这种方法,长选项也可以有多个参数。
#!/bin/bash
# Uses bash extensions. Not portable as written.
declare -A longoptspec
longoptspec=( [loglevel]=1 ) #use associative array to declare how many arguments a long option expects, in this case we declare that loglevel expects/has one argument, long options that aren't listed i n this way will have zero arguments by default
optspec=":h-:"
while getopts "$optspec" opt; do
while true; do
case "${opt}" in
-) #OPTARG is name-of-long-option or name-of-long-option=value
if [[ "${OPTARG}" =~ .*=.* ]] #with this --key=value format only one argument is possible
then
opt=${OPTARG/=*/}
OPTARG=${OPTARG#*=}
((OPTIND--))
else #with this --key value1 value2 format multiple arguments are possible
opt="$OPTARG"
OPTARG=(${@:OPTIND:$((longoptspec[$opt]))})
fi
((OPTIND+=longoptspec[$opt]))
continue #now that opt/OPTARG are set we can process them as if getopts would've given us long options
;;
loglevel)
loglevel=$OPTARG
;;
h|help)
echo "usage: $0 [--loglevel[=]<value>]" >&2
exit 2
;;
esac
break; done
done
# End of file
改进的解决方案:
# translate long options to short
# Note: This enable long options but disable "--?*" in $OPTARG, or disable long options after "--" in option fields.
for ((i=1;$#;i++)) ; do
case "$1" in
--)
# [ ${args[$((i-1))]} == ... ] || EndOpt=1 ;;& # DIRTY: we still can handle some execptions...
EndOpt=1 ;;&
--version) ((EndOpt)) && args[$i]="$1" || args[$i]="-V";;
# default case : short option use the first char of the long option:
--?*) ((EndOpt)) && args[$i]="$1" || args[$i]="-${1:2:1}";;
# pass through anything else:
*) args[$i]="$1" ;;
esac
shift
done
# reset the translated args
set -- "${args[@]}"
function usage {
echo "Usage: $0 [options] files" >&2
exit $1
}
# now we can process with getopt
while getopts ":hvVc:" opt; do
case $opt in
h) usage ;;
v) VERBOSE=true ;;
V) echo $Version ; exit ;;
c) source $OPTARG ;;
\?) echo "unrecognized option: -$opt" ; usage -1 ;;
:)
echo "option -$OPTARG requires an argument"
usage -1
;;
esac
done
shift $((OPTIND-1))
[[ "$1" == "--" ]] && shift
也许使用 ksh 更简单,仅用于 getopts 部分,如果需要长命令行选项,因为在那里可以更容易地完成。
# Working Getopts Long => KSH
#! /bin/ksh
# Getopts Long
USAGE="s(showconfig)"
USAGE+="c:(createdb)"
USAGE+="l:(createlistener)"
USAGE+="g:(generatescripts)"
USAGE+="r:(removedb)"
USAGE+="x:(removelistener)"
USAGE+="t:(createtemplate)"
USAGE+="h(help)"
while getopts "$USAGE" optchar ; do
case $optchar in
s) echo "Displaying Configuration" ;;
c) echo "Creating Database $OPTARG" ;;
l) echo "Creating Listener LISTENER_$OPTARG" ;;
g) echo "Generating Scripts for Database $OPTARG" ;;
r) echo "Removing Database $OPTARG" ;;
x) echo "Removing Listener LISTENER_$OPTARG" ;;
t) echo "Creating Database Template" ;;
h) echo "Help" ;;
esac
done
我想要一些没有外部依赖的东西,具有严格的 bash 支持 (-u),我需要它甚至可以在较旧的 bash 版本上工作。这处理各种类型的参数:
短布尔 (-h)
短选项(-i "image.jpg")
长布尔 (--help)
等于选项(--file="filename.ext")
空间选项(--file "filename.ext")
连接布尔 (-hvm)
只需在脚本顶部插入以下内容:
# Check if a list of params contains a specific param
# usage: if _param_variant "h|?|help p|path f|file long-thing t|test-thing" "file" ; then ...
# the global variable $key is updated to the long notation (last entry in the pipe delineated list, if applicable)
_param_variant() {
for param in $1 ; do
local variants=${param//\|/ }
for variant in $variants ; do
if [[ "$variant" = "$2" ]] ; then
# Update the key to match the long version
local arr=(${param//\|/ })
let last=${#arr[@]}-1
key="${arr[$last]}"
return 0
fi
done
done
return 1
}
# Get input parameters in short or long notation, with no dependencies beyond bash
# usage:
# # First, set your defaults
# param_help=false
# param_path="."
# param_file=false
# param_image=false
# param_image_lossy=true
# # Define allowed parameters
# allowed_params="h|?|help p|path f|file i|image image-lossy"
# # Get parameters from the arguments provided
# _get_params $*
#
# Parameters will be converted into safe variable names like:
# param_help,
# param_path,
# param_file,
# param_image,
# param_image_lossy
#
# Parameters without a value like "-h" or "--help" will be treated as
# boolean, and will be set as param_help=true
#
# Parameters can accept values in the various typical ways:
# -i "path/goes/here"
# --image "path/goes/here"
# --image="path/goes/here"
# --image=path/goes/here
# These would all result in effectively the same thing:
# param_image="path/goes/here"
#
# Concatinated short parameters (boolean) are also supported
# -vhm is the same as -v -h -m
_get_params(){
local param_pair
local key
local value
local shift_count
while : ; do
# Ensure we have a valid param. Allows this to work even in -u mode.
if [[ $# == 0 || -z $1 ]] ; then
break
fi
# Split the argument if it contains "="
param_pair=(${1//=/ })
# Remove preceeding dashes
key="${param_pair[0]#--}"
# Check for concatinated boolean short parameters.
local nodash="${key#-}"
local breakout=false
if [[ "$nodash" != "$key" && ${#nodash} -gt 1 ]]; then
# Extrapolate multiple boolean keys in single dash notation. ie. "-vmh" should translate to: "-v -m -h"
local short_param_count=${#nodash}
let new_arg_count=$#+$short_param_count-1
local new_args=""
# $str_pos is the current position in the short param string $nodash
for (( str_pos=0; str_pos<new_arg_count; str_pos++ )); do
# The first character becomes the current key
if [ $str_pos -eq 0 ] ; then
key="${nodash:$str_pos:1}"
breakout=true
fi
# $arg_pos is the current position in the constructed arguments list
let arg_pos=$str_pos+1
if [ $arg_pos -gt $short_param_count ] ; then
# handle other arguments
let orignal_arg_number=$arg_pos-$short_param_count+1
local new_arg="${!orignal_arg_number}"
else
# break out our one argument into new ones
local new_arg="-${nodash:$str_pos:1}"
fi
new_args="$new_args \"$new_arg\""
done
# remove the preceding space and set the new arguments
eval set -- "${new_args# }"
fi
if ! $breakout ; then
key="$nodash"
fi
# By default we expect to shift one argument at a time
shift_count=1
if [ "${#param_pair[@]}" -gt "1" ] ; then
# This is a param with equals notation
value="${param_pair[1]}"
else
# This is either a boolean param and there is no value,
# or the value is the next command line argument
# Assume the value is a boolean true, unless the next argument is found to be a value.
value=true
if [[ $# -gt 1 && -n "$2" ]]; then
local nodash="${2#-}"
if [ "$nodash" = "$2" ]; then
# The next argument has NO preceding dash so it is a value
value="$2"
shift_count=2
fi
fi
fi
# Check that the param being passed is one of the allowed params
if _param_variant "$allowed_params" "$key" ; then
# --key-name will now become param_key_name
eval param_${key//-/_}="$value"
else
printf 'WARNING: Unknown option (ignored): %s\n' "$1" >&2
fi
shift $shift_count
done
}
并像这样使用它:
# Assign defaults for parameters
param_help=false
param_path=$(pwd)
param_file=false
param_image=true
param_image_lossy=true
param_image_lossy_quality=85
# Define the params we will allow
allowed_params="h|?|help p|path f|file i|image image-lossy image-lossy-quality"
# Get the params from arguments provided
_get_params $*
如果只是这就是您要调用脚本的方式
myscript.sh --input1 "ABC" --input2 "PQR" --input2 "XYZ"
那么你可以按照这个最简单的方法在 getopt 和 --longoptions 的帮助下实现它
试试这个,希望有用
# Read command line options
ARGUMENT_LIST=(
"input1"
"input2"
"input3"
)
# read arguments
opts=$(getopt \
--longoptions "$(printf "%s:," "${ARGUMENT_LIST[@]}")" \
--name "$(basename "$0")" \
--options "" \
-- "$@"
)
echo $opts
eval set --$opts
while true; do
case "$1" in
--input1)
shift
empId=$1
;;
--input2)
shift
fromDate=$1
;;
--input3)
shift
toDate=$1
;;
--)
shift
break
;;
esac
shift
done
getopts“可以用来”解析长选项,只要你不希望它们有参数......
方法如下:
$ cat > longopt
while getopts 'e:-:' OPT; do
case $OPT in
e) echo echo: $OPTARG;;
-) #long option
case $OPTARG in
long-option) echo long option;;
*) echo long option: $OPTARG;;
esac;;
esac
done
$ bash longopt -e asd --long-option --long1 --long2 -e test
echo: asd
long option
long option: long1
long option: long2
echo: test
如果您尝试使用 OPTIND 获取 long 选项的参数,getopts 会将其视为第一个非可选位置参数,并将停止解析任何其他参数。在这种情况下,您最好使用简单的 case 语句手动处理它。
这将“始终”有效:
$ cat >longopt2
while (($#)); do
OPT=$1
shift
case $OPT in
--*) case ${OPT:2} in
long1) echo long1 option;;
complex) echo comples with argument $1; shift;;
esac;;
-*) case ${OPT:1} in
a) echo short option a;;
b) echo short option b with parameter $1; shift;;
esac;;
esac
done
$ bash longopt2 --complex abc -a --long -b test
comples with argument abc
short option a
short option b with parameter test
尽管它不如 getopts 灵活,并且您必须在案例实例中自己执行大部分错误检查代码......
但这是一种选择。
为了保持跨平台兼容,并避免依赖外部可执行文件,我从另一种语言移植了一些代码。
我觉得它很容易使用,这里有一个例子:
ArgParser::addArg "[h]elp" false "This list"
ArgParser::addArg "[q]uiet" false "Supress output"
ArgParser::addArg "[s]leep" 1 "Seconds to sleep"
ArgParser::addArg "v" 1 "Verbose mode"
ArgParser::parse "$@"
ArgParser::isset help && ArgParser::showArgs
ArgParser::isset "quiet" \
&& echo "Quiet!" \
|| echo "Noisy!"
local __sleep
ArgParser::tryAndGetArg sleep into __sleep \
&& echo "Sleep for $__sleep seconds" \
|| echo "No value passed for sleep"
# This way is often more convienient, but is a little slower
echo "Sleep set to: $( ArgParser::getArg sleep )"
所需的 BASH 比它可能的要长一点,但我想避免依赖 BASH 4 的关联数组。您也可以直接从 http://nt4.com/bash/argparser.inc.sh 下载
#!/usr/bin/env bash
# Updates to this script may be found at
# http://nt4.com/bash/argparser.inc.sh
# Example of runtime usage:
# mnc.sh --nc -q Caprica.S0*mkv *.avi *.mp3 --more-options here --host centos8.host.com
# Example of use in script (see bottom)
# Just include this file in yours, or use
# source argparser.inc.sh
unset EXPLODED
declare -a EXPLODED
function explode
{
local c=$#
(( c < 2 )) &&
{
echo function "$0" is missing parameters
return 1
}
local delimiter="$1"
local string="$2"
local limit=${3-99}
local tmp_delim=$'\x07'
local delin=${string//$delimiter/$tmp_delim}
local oldifs="$IFS"
IFS="$tmp_delim"
EXPLODED=($delin)
IFS="$oldifs"
}
# See: http://fvue.nl/wiki/Bash:_Passing_variables_by_reference
# Usage: local "$1" && upvar $1 "value(s)"
upvar() {
if unset -v "$1"; then # Unset & validate varname
if (( $# == 2 )); then
eval $1=\"\$2\" # Return single value
else
eval $1=\(\"\${@:2}\"\) # Return array
fi
fi
}
function decho
{
:
}
function ArgParser::check
{
__args=${#__argparser__arglist[@]}
for (( i=0; i<__args; i++ ))
do
matched=0
explode "|" "${__argparser__arglist[$i]}"
if [ "${#1}" -eq 1 ]
then
if [ "${1}" == "${EXPLODED[0]}" ]
then
decho "Matched $1 with ${EXPLODED[0]}"
matched=1
break
fi
else
if [ "${1}" == "${EXPLODED[1]}" ]
then
decho "Matched $1 with ${EXPLODED[1]}"
matched=1
break
fi
fi
done
(( matched == 0 )) && return 2
# decho "Key $key has default argument of ${EXPLODED[3]}"
if [ "${EXPLODED[3]}" == "false" ]
then
return 0
else
return 1
fi
}
function ArgParser::set
{
key=$3
value="${1:-true}"
declare -g __argpassed__$key="$value"
}
function ArgParser::parse
{
unset __argparser__argv
__argparser__argv=()
# echo parsing: "$@"
while [ -n "$1" ]
do
# echo "Processing $1"
if [ "${1:0:2}" == '--' ]
then
key=${1:2}
value=$2
elif [ "${1:0:1}" == '-' ]
then
key=${1:1} # Strip off leading -
value=$2
else
decho "Not argument or option: '$1'" >& 2
__argparser__argv+=( "$1" )
shift
continue
fi
# parameter=${tmp%%=*} # Extract name.
# value=${tmp##*=} # Extract value.
decho "Key: '$key', value: '$value'"
# eval $parameter=$value
ArgParser::check $key
el=$?
# echo "Check returned $el for $key"
[ $el -eq 2 ] && decho "No match for option '$1'" >&2 # && __argparser__argv+=( "$1" )
[ $el -eq 0 ] && decho "Matched option '${EXPLODED[2]}' with no arguments" >&2 && ArgParser::set true "${EXPLODED[@]}"
[ $el -eq 1 ] && decho "Matched option '${EXPLODED[2]}' with an argument of '$2'" >&2 && ArgParser::set "$2" "${EXPLODED[@]}" && shift
shift
done
}
function ArgParser::isset
{
declare -p "__argpassed__$1" > /dev/null 2>&1 && return 0
return 1
}
function ArgParser::getArg
{
# This one would be a bit silly, since we can only return non-integer arguments ineffeciently
varname="__argpassed__$1"
echo "${!varname}"
}
##
# usage: tryAndGetArg <argname> into <varname>
# returns: 0 on success, 1 on failure
function ArgParser::tryAndGetArg
{
local __varname="__argpassed__$1"
local __value="${!__varname}"
test -z "$__value" && return 1
local "$3" && upvar $3 "$__value"
return 0
}
function ArgParser::__construct
{
unset __argparser__arglist
# declare -a __argparser__arglist
}
##
# @brief add command line argument
# @param 1 short and/or long, eg: [s]hort
# @param 2 default value
# @param 3 description
##
function ArgParser::addArg
{
# check for short arg within long arg
if [[ "$1" =~ \[(.)\] ]]
then
short=${BASH_REMATCH[1]}
long=${1/\[$short\]/$short}
else
long=$1
fi
if [ "${#long}" -eq 1 ]
then
short=$long
long=''
fi
decho short: "$short"
decho long: "$long"
__argparser__arglist+=("$short|$long|$1|$2|$3")
}
##
# @brief show available command line arguments
##
function ArgParser::showArgs
{
# declare -p | grep argparser
printf "Usage: %s [OPTION...]\n\n" "$( basename "${BASH_SOURCE[0]}" )"
printf "Defaults for the options are specified in brackets.\n\n";
__args=${#__argparser__arglist[@]}
for (( i=0; i<__args; i++ ))
do
local shortname=
local fullname=
local default=
local description=
local comma=
explode "|" "${__argparser__arglist[$i]}"
shortname="${EXPLODED[0]:+-${EXPLODED[0]}}" # String Substitution Guide:
fullname="${EXPLODED[1]:+--${EXPLODED[1]}}" # http://tldp.org/LDP/abs/html/parameter-substitution.html
test -n "$shortname" \
&& test -n "$fullname" \
&& comma=","
default="${EXPLODED[3]}"
case $default in
false )
default=
;;
"" )
default=
;;
* )
default="[$default]"
esac
description="${EXPLODED[4]}"
printf " %2s%1s %-19s %s %s\n" "$shortname" "$comma" "$fullname" "$description" "$default"
done
}
function ArgParser::test
{
# Arguments with a default of 'false' do not take paramaters (note: default
# values are not applied in this release)
ArgParser::addArg "[h]elp" false "This list"
ArgParser::addArg "[q]uiet" false "Supress output"
ArgParser::addArg "[s]leep" 1 "Seconds to sleep"
ArgParser::addArg "v" 1 "Verbose mode"
ArgParser::parse "$@"
ArgParser::isset help && ArgParser::showArgs
ArgParser::isset "quiet" \
&& echo "Quiet!" \
|| echo "Noisy!"
local __sleep
ArgParser::tryAndGetArg sleep into __sleep \
&& echo "Sleep for $__sleep seconds" \
|| echo "No value passed for sleep"
# This way is often more convienient, but is a little slower
echo "Sleep set to: $( ArgParser::getArg sleep )"
echo "Remaining command line: ${__argparser__argv[@]}"
}
if [ "$( basename "$0" )" == "argparser.inc.sh" ]
then
ArgParser::test "$@"
fi
如果您的所有长选项都具有唯一且匹配的第一个字符作为短选项,例如
./slamm --chaos 23 --plenty test -quiet
是相同的
./slamm -c 23 -p test -q
您可以在 getopts 重写 $args 之前使用它:
# change long options to short options
for arg; do
[[ "${arg:0:1}" == "-" ]] && delim="" || delim="\""
if [ "${arg:0:2}" == "--" ];
then args="${args} -${arg:2:1}"
else args="${args} ${delim}${arg}${delim}"
fi
done
# reset the incoming args
eval set -- $args
# proceed as usual
while getopts ":b:la:h" OPTION; do
.....
感谢 mtvee 的启发 ;-)
内置getopts
仅解析短选项(ksh93 除外),但您仍然可以添加几行脚本以使 getopts 处理长选项。
这是在 http://www.uxora.com/unix/shell-script/22-handle-long-options-with-getopts 中找到的部分代码
#== set short options ==#
SCRIPT_OPTS=':fbF:B:-:h'
#== set long options associated with short one ==#
typeset -A ARRAY_OPTS
ARRAY_OPTS=(
[foo]=f
[bar]=b
[foobar]=F
[barfoo]=B
[help]=h
[man]=h
)
#== parse options ==#
while getopts ${SCRIPT_OPTS} OPTION ; do
#== translate long options to short ==#
if [[ "x$OPTION" == "x-" ]]; then
LONG_OPTION=$OPTARG
LONG_OPTARG=$(echo $LONG_OPTION | grep "=" | cut -d'=' -f2)
LONG_OPTIND=-1
[[ "x$LONG_OPTARG" = "x" ]] && LONG_OPTIND=$OPTIND || LONG_OPTION=$(echo $OPTARG | cut -d'=' -f1)
[[ $LONG_OPTIND -ne -1 ]] && eval LONG_OPTARG="\$$LONG_OPTIND"
OPTION=${ARRAY_OPTS[$LONG_OPTION]}
[[ "x$OPTION" = "x" ]] && OPTION="?" OPTARG="-$LONG_OPTION"
if [[ $( echo "${SCRIPT_OPTS}" | grep -c "${OPTION}:" ) -eq 1 ]]; then
if [[ "x${LONG_OPTARG}" = "x" ]] || [[ "${LONG_OPTARG}" = -* ]]; then
OPTION=":" OPTARG="-$LONG_OPTION"
else
OPTARG="$LONG_OPTARG";
if [[ $LONG_OPTIND -ne -1 ]]; then
[[ $OPTIND -le $Optnum ]] && OPTIND=$(( $OPTIND+1 ))
shift $OPTIND
OPTIND=1
fi
fi
fi
fi
#== options follow by another option instead of argument ==#
if [[ "x${OPTION}" != "x:" ]] && [[ "x${OPTION}" != "x?" ]] && [[ "${OPTARG}" = -* ]]; then
OPTARG="$OPTION" OPTION=":"
fi
#== manage options ==#
case "$OPTION" in
f ) foo=1 bar=0 ;;
b ) foo=0 bar=1 ;;
B ) barfoo=${OPTARG} ;;
F ) foobar=1 && foobar_name=${OPTARG} ;;
h ) usagefull && exit 0 ;;
: ) echo "${SCRIPT_NAME}: -$OPTARG: option requires an argument" >&2 && usage >&2 && exit 99 ;;
? ) echo "${SCRIPT_NAME}: -$OPTARG: unknown option" >&2 && usage >&2 && exit 99 ;;
esac
done
shift $((${OPTIND} - 1))
这是一个测试:
# Short options test
$ ./foobar_any_getopts.sh -bF "Hello world" -B 6 file1 file2
foo=0 bar=1
barfoo=6
foobar=1 foobar_name=Hello world
files=file1 file2
# Long and short options test
$ ./foobar_any_getopts.sh --bar -F Hello --barfoo 6 file1 file2
foo=0 bar=1
barfoo=6
foobar=1 foobar_name=Hello
files=file1 file2
否则在最近的 Korn Shell ksh93 中,getopts
可以自然地解析长选项,甚至显示类似的手册页。 (见http://www.uxora.com/unix/shell-script/20-getopts-with-man-page-and-long-options)
内置 OS X (BSD) getopt 不支持长选项,但 GNU 版本支持:brew install gnu-getopt
。然后,类似于:cp /usr/local/Cellar/gnu-getopt/1.1.6/bin/getopt /usr/local/bin/gnu-getopt
。
一个简单的 DIY,只获取长名称的 args:
利用:
$ ./test-args.sh --a1 a1 --a2 "a 2" --a3 --a4= --a5=a5 --a6="a 6"
a1 = "a1"
a2 = "a 2"
a3 = "TRUE"
a4 = ""
a5 = "a5"
a6 = "a 6"
a7 = ""
脚本:
#!/bin/bash
function main() {
ARGS=`getArgs "$@"`
a1=`echo "$ARGS" | getNamedArg a1`
a2=`echo "$ARGS" | getNamedArg a2`
a3=`echo "$ARGS" | getNamedArg a3`
a4=`echo "$ARGS" | getNamedArg a4`
a5=`echo "$ARGS" | getNamedArg a5`
a6=`echo "$ARGS" | getNamedArg a6`
a7=`echo "$ARGS" | getNamedArg a7`
echo "a1 = \"$a1\""
echo "a2 = \"$a2\""
echo "a3 = \"$a3\""
echo "a4 = \"$a4\""
echo "a5 = \"$a5\""
echo "a6 = \"$a6\""
echo "a7 = \"$a7\""
exit 0
}
function getArgs() {
for arg in "$@"; do
echo "$arg"
done
}
function getNamedArg() {
ARG_NAME=$1
sed --regexp-extended --quiet --expression="
s/^--$ARG_NAME=(.*)\$/\1/p # Get arguments in format '--arg=value': [s]ubstitute '--arg=value' by 'value', and [p]rint
/^--$ARG_NAME\$/ { # Get arguments in format '--arg value' ou '--arg'
n # - [n]ext, because in this format, if value exists, it will be the next argument
/^--/! p # - If next doesn't starts with '--', it is the value of the actual argument
/^--/ { # - If next do starts with '--', it is the next argument and the actual argument is a boolean one
# Then just repla[c]ed by TRUE
c TRUE
}
}
"
}
main "$@"
getopt
不是 GNU 实用程序,传统的getopt
最初并非来自 BSD,而是来自 AT&T Unix。 ksh93 的getopts
(也来自 AT&T)支持 GNU 风格的长选项。POSIXLY_CORRECT
),而“Linux-enhanced getopt”错误地暗示这个版本仅存在于 Linux 上。getopt
可以很容易地移植到其他 Unices,但util-linux
中的许多其他软件是特定于 Linux 的) .所有使用 GNU getopt(3) 的非 GNU 程序都理解$POSIX_CORRECT
。例如,您不会仅仅基于这些理由就说aplay
是 GNU。我怀疑当 FreeBSD 提到 GNU getopt 时,他们的意思是 GNU getopt(3) C API。getopt
解析后,不会有分号。在set --
之后,破折号和引号将被视为参数。我已经测试了一些组合。但是我仍然找不到eval set -- $TEMP
的行为与eval set -- "$TEMP"
不同的示例。感谢您的出色回答。