使用getopts处理长、短命令行选项

我希望使用shell脚本调用长形式和短形式的命令行选项。

我知道可以使用getopts,但像在Perl中一样,我不能用shell做同样的事情。

关于如何做到这一点的任何想法,这样我就可以使用如下选项:

./shell.sh --copyfile abc.pl /tmp/
./shell.sh -c abc.pl /tmp/

在上面,这两个命令对我的shell意味着同样的事情,但使用getopts,我还没有能够实现这些?

524699 次浏览

可以考虑以下三种实现方式:

  • Bash内置getopts。这不支持带有双破折号前缀的长选项名。它只支持单字符选项。

  • BSD UNIX实现的独立getopt命令(这是MacOS使用的)。这也不支持长选项。

  • 独立getopt的GNU实现。GNU getopt(3) (Linux上由命令行getopt(1)使用)支持解析长选项。


其他一些答案显示了使用bash内置getopts来模拟长选项的解决方案。该解决方案实际上生成了一个字符为“-”的短选项。所以你得到“——”作为标志。然后后面的任何东西都变成OPTARG,然后你用嵌套的case测试OPTARG。

这很聪明,但也需要注意:

  • getopts不能强制opt规范。如果用户提供了无效的选项,它不能返回错误。在解析OPTARG时,您必须自己进行错误检查。
  • OPTARG用于长选项名称,当长选项本身有参数时,这会使使用复杂化。你最终不得不自己编写代码作为一个额外的案例。

因此,虽然可以编写更多的代码来解决长选项支持不足的问题,但工作量要大得多,并且在一定程度上违背了使用getopt解析器来简化代码的目的。

内置的getopts不能这样做。有一个外部的getopt(1)程序可以做到这一点,但你只能在Linux上从util-linux包中获得它。它附带一个示例脚本getopt-parse.bash

还有一个getopts_long被写成shell函数。

内置的getopts命令仍然仅限于单字符选项。

现在(或过去)有一个外部程序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更优越的原因,尽管它只处理单字母选项。

看看shFlags,它是一个可移植的shell库(意思是:sh, bash, dash, ksh, zsh在Linux, Solaris等上)。

它使得添加新标志就像在脚本中添加一行一样简单,并且它提供了一个自动生成的用法函数。

下面是一个使用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!

对于其他情况,您必须使用短选项:

$ ./hello_world.sh -n Kate
Hello, Kate!

添加一个新标志就像添加一个新的DEFINE_ call一样简单。

#!/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

ksh93中,getopts不支持长名称…

while getopts "f(file):s(server):" flag
do
echo "$flag" $OPTIND $OPTARG
done

至少我找到的教程是这么说的。试试看。

下面是一个实际使用长选项的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

另一种方式…

# 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
我研究那个课题已经有很长时间了……并制作了我自己的库,你将需要在你的主脚本的来源。 有关示例,请参见libopt4shellcd2mpc

改进的解决方案:

# 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

我是这样解决的:

# 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;

我是不是太笨了?getoptgetopts太混乱了。

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

复制到当前工作目录中名为=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(“破折号”)。注意特殊情况:

getopts -- "-:"  ## without the option terminator "-- " bash complains about "-:"
getopts "-:"     ## this works in the Debian Almquist shell ("dash")

请注意,正如GreyCat在http://mywiki.wooledge.org/BashFAQ中指出的那样,这个技巧利用了shell的非标准行为,允许选项参数(即“-ffilename”中的文件名)连接到选项(如“-ffilename”)。POSIX标准规定它们之间必须有一个空格,在"——longoption"的情况下,空格将终止选项解析并将所有长选项转换为非选项参数。

getoptgetopts是不同的野兽,人们似乎对它们的作用有一些误解。getoptsbash的内置命令,用于循环处理命令行选项,并将找到的每个选项和值依次分配给内置变量,以便进一步处理它们。然而,getopt是一个外部实用程序,它getopts1的方式就像bash getopts、Perl Getopt模块或Python optparse/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-m4096样式,以任意顺序混合选项和非选项,等等。如果发现无法识别或有歧义的选项,getopt也会输出错误消息。

getopt3:实际上有两个getopt4版本的getopt,基本的getopt和GNU的getopt,具有不同的特性和不同的调用约定。getopt5基本的getopt相当糟糕:它不仅不能处理长选项,甚至不能处理参数内的嵌入空格或空参数,而getopts在这方面做得很好。上面的代码将不能在基本的getopt中工作。GNU getopt默认安装在Linux上,但在Mac OS X和FreeBSD上需要单独安装。在Mac OS X上,安装MacPorts (getopt6),然后执行sudo port install getopt来安装GNU getopt(通常在/opt/local/bin中),并确保/opt/local/bin位于getopt1之前的shell路径中。在FreeBSD上,安装getopt2。

为您自己的程序修改示例代码的快速指南:在前几行中,所有代码都是“boilerplate”;,除了调用getopt的行。你应该在-n之后更改程序名称,在-o之后指定短选项,在--long之后指定长选项。在带值的选项后面加冒号。

最后,如果你看到的代码只有set而不是eval set,那么它是为BSD getopt编写的。你应该将其更改为使用eval set样式,这适用于两个版本的getopt,而普通的set不适用于GNU getopt

__abc5实际上,ksh93中的getopts支持长名称选项,但这个shell不像bash那样经常使用。在zsh中,使用zparseopts来获得这个功能。

__abc2技术上,"GNU getopt"是用词不当;这个版本实际上是为Linux而不是GNU项目编写的。然而,它遵循所有的GNU约定,术语“GNU getopt"是常用的(例如在FreeBSD上)。

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来获取长选项的参数,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那么灵活,而且你必须在用例实例中自己做很多错误检查代码……

但这是一种选择。

如果你不想要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功能和长选项,这是一个简单的方法。

我还把这个代码片段放在要点中。

我还没有足够的代表来评论或投票他的解决方案,但中小企业的回答对我来说非常有效。我遇到的唯一问题是参数以单引号结尾(所以我把它们去掉了)。

我还添加了一些示例用法和帮助文本。我将在这里附上我略微扩展的版本:

#!/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

对于getopts部分,如果需要较长的命令行选项,使用ksh可能更简单,因为在ksh中更容易完成。

# 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

例如,如果所有长选项都有唯一且匹配的首字符作为短选项

./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


适用于所有组合,例如:

  • Foobar -f——bar
  • Foobar——foo -b
  • Foobar -bf -bar - Foobar
  • foobar -fbFBAshorty——bar -FB——arguments=longhorn
  • foobar -fA "text shorty" -B——arguments="text longhorn"
  • 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 "

getops带有长/短标志以及长参数

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

为了保持跨平台兼容性,避免依赖外部可执行文件,我从另一种语言移植了一些代码。

我发现它很容易使用,这里有一个例子:

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
下面你可以在bash中找到一些不同的复杂选项解析方法: http://mywiki.wooledge.org/ComplexOptionParsing < / p > 我确实创建了下面的一个,我认为这是一个很好的,因为它是最小的代码 多头和空头选择都有效。用这种方法,长选项也可以有多个参数
#!/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

长选项可以被标准的内置getopts作为- " option "的"参数"解析。

这是可移植的本地POSIX shell -不需要外部程序或bashisms。

本指南将长选项实现为-选项的参数,因此--alphagetopts视为带有参数alpha-,而--bravo=foo被视为带有参数bravo=foo-。true实参通过shell形参展开获取,更新$OPT$OPTARG

在本例中,-b-c(以及它们的长形式--bravo--charlie)具有强制参数。长选项的参数出现在等号之后,例如--bravo=foo(长选项的空格分隔符很难实现,见下文)。

因为它使用了getopts装入的,所以这个解决方案支持像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

当选项是破折号(-)时,它是一个长选项。getopts将实际的长选项解析为$OPTARG,例如--bravo=foo最初设置OPT='-'OPTARG='bravo=foo'if节将$OPT设置为$OPTARG的第一个等号之前的内容(在我们的例子中是bravo),然后从$OPTARG的开头删除它(在这一步中产生getopts1,如果没有getopts2则产生空字符串)。最后,去掉参数的前导getopts2。此时,$OPT要么是一个短选项(一个字符),要么是一个长选项(2+个字符)。

case然后匹配短选项或长选项。对于较短的选项,getopts会自动报错选项和缺少参数,因此我们必须使用needs_arg函数手动复制这些选项,当$OPTARG为空时,该函数将致命地退出。??*条件将匹配任何剩余的长选项(?匹配单个字符,*匹配0个或多个字符,因此??*匹配2个+字符),允许我们发出“非法选项”;退出前的错误。

与gnu风格的长选项一样,提供--将停止解析,因此-a -- --bravo=4将把$alpha设置为true,但$bravo将保持不变,而$1将为--bravo=4。我不能说我建议用前导破折号来命名文件,但这是表示它们不是选项的方法。


小虫:如果有人给出了一个无效的单字符长选项(它也不是一个短选项),它将退出并报错但没有消息(此实现假设它是一个短选项)。你可以在case之前的条件中使用一个额外的变量来跟踪它,然后在最后的case条件中测试它,但我认为这是一个太麻烦的角落情况。

大写变量名:通常,建议为系统使用保留全大写变量。我将$OPT保留为全大写,以与$OPTARG保持一致,但这确实打破了该约定。我认为这很合适,因为这是系统应该做的事情,它应该是安全的;我还没有听说过任何使用这个变量名的标准。

抱怨多头期权的意外参数:用翻转测试模拟needs_arg,在不期望参数时抱怨参数:

no_arg() { if [ -n "$OPTARG" ]; then die "No arg allowed for --$OPT option"; fi; }

接受带有空格分隔参数的长选项:你可以用eval "ARG_B=\"\$$OPTIND\""(或bash的间接的扩张ARG_B="${!OPTIND}")拉入下一个参数,然后像这个答案的旧版本中指出的那样增加$OPTIND,但这是不可靠的;getopts可以在参数超出其作用域的假设下过早终止,并且一些实现不太适合手动操作$OPTIND

内装式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

否则,在最近的光辉壳牌 ksh93, getopts可以自然地解析长选项,甚至显示类似的手册页。(见http://www.uxora.com/unix/shell-script/20-getopts-with-man-page-and-long-options)

嗯。

对纯bash选项不太满意。为什么不使用perl来得到你想要的。直接解析$*数组,并自动命名您的选项。

简单的帮助脚本:

#!/usr/bin/perl
use Getopt::Long;


my $optstring = shift;


my @opts = split(m#,#, $optstring);


my %opt;
GetOptions(\%opt, @opts);


print "set -- " . join(' ', map("'$_'", @ARGV)) . ";";
my $xx;


my $key;
foreach $key (keys(%opt))
{
print "export $key='$opt{$key}'; ";
}

然后你可以在你的脚本中使用一行,例如:

#!/bin/bash


eval `getopts.pl reuse:s,long_opt:s,hello $*`;


echo "HELLO: $hello"
echo "LONG_OPT: $long_opt"
echo "REUSE: $reuse"


echo $*

/tmp/script.sh hello——reuse me——long_opt whatever_you_want_except_spaces——hello 1 2 3

< p >你好:1 LONG_OPT: whatever_you_want_except空格 重用:我< / p >

1 2 3

这里唯一需要注意的是空格不适用。但它避免了bash相当复杂的循环语法,适用于长参数,自动将它们命名为变量并自动调整$*的大小,因此99%的时间都是有效的。

我想要一个没有外部依赖关系、有严格bash支持(-u)的东西,而且我需要它能在较旧的bash版本上工作。这个函数处理各种类型的参数:

  • 短bool (-h)
  • 短选项(-i "image.jpg")
  • 长boos(——救命)
  • = options(——file="filename.ext")
  • 空格选项(——file "filename.ext")
  • 连接的bool (-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 $*

发明了另一个版本的轮子……

这个函数(希望)是一个posix兼容的普通bourne shell替换GNU getopt。它支持短/长选项,可以接受强制/可选/无参数,并且指定选项的方式几乎与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

内置的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

我只是偶尔写一些shell脚本,但没有实践经验,所以任何反馈都很感激。

使用@Arvid Requate提出的策略,我们注意到一些用户错误。忘记包含值的用户会意外地将下一个选项的名称视为值:

./getopts_test.sh --loglevel= --toc=TRUE

将导致"loglevel"的值被视为"——toc=TRUE"。这可以 被避免。< / p >

我从手动解析的http://mwiki.wooledge.org/BashFAQ/035讨论中改编了一些关于检查CLI用户错误的想法。我在处理"-"和"——"参数时插入了错误检查。

然后我开始摆弄语法,所以这里的任何错误都是我的错,而不是原始作者的错。

我的方法可以帮助那些喜欢输入带等号或不带等号的长字符的用户。也就是说,它对“——loglevel 9”的响应应该与“——loglevel=9”的响应相同。在——/space方法中,不可能确定用户是否忘记了一个参数,因此需要进行一些猜测。

  1. 如果用户使用长/等号格式(——opt=),则=后的空格会触发错误,因为没有提供参数。
  2. 如果user有长/空格参数(——opt),如果后面没有参数(命令结束)或参数以破折号开头,该脚本将导致失败。

如果你刚开始使用这个,“——opt=value”和“——opt value”格式之间有一个有趣的区别。使用等号,命令行参数被视为“opt=value”,处理它的工作是字符串解析,在“=”处分离。相比之下,使用"——opt value",参数的名称是"opt",我们面临的挑战是在命令行中获取下一个值。这就是@Arvid Requate使用${!OPTIND},间接引用。我仍然不明白,好吧,在所有BashFAQ中的评论似乎警告反对这种风格(http://mywiki.wooledge.org/BashFAQ/006)。顺便说一句,我不认为之前的帖子关于OPTIND=$(($OPTIND + 1))的重要性的评论是正确的。我的意思是说,我看不提也无妨。

在该脚本的最新版本中,flag -v表示详细打印输出。

将其保存在一个名为“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"

接受的答案很好地指出了bash内置getopts的所有缺点。答案以:

因此,虽然可以编写更多的代码来解决长选项支持不足的问题,但工作量要大得多,并且在一定程度上违背了使用getopt解析器来简化代码的目的。

尽管我原则上同意这种说法,但我觉得我们在各种脚本中实现这个特性的次数证明,我们应该花一些精力来创建一个“标准化”的、经过良好测试的解决方案。

因此,我已经“升级”了在getopts中构建的bash,在纯bash中实现了getopts_long,没有外部依赖。该函数的使用与内置的getopts完全兼容。

通过在脚本中包含getopts_long(即托管在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

一个简单的DIY,只获得长名称的参数:

使用:

$ ./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 "$@"

如果这就是您想要调用脚本的方式

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

这需要一些时间,但我想要全部:

  • 短选项
  • 长选项
  • 不管有没有论点
  • 非选项参数(没有"-"或“——“)
  • 顺序不重要(script.sh /file -Vscript.sh -V /file)
  • 抓住错误用法
  • 在不同的脚本中使用它作为模块,而不需要更改多行代码

最后,我想出了下面的解决方案,它使用getopt来捕捉错误,并将非选项移动到列表的末尾,然后是getopts,用于解析短选项和长选项。

所有的选项都会自动解析,将它们的长选项名作为变量名(请看例子):

# create string of short options
opt_short=$(printf "%s" "${!options[@]}")


# create string of long options
opt_long="$(printf ",%s" "${options[@]}")"


# catch wrong options and move non-options to the end of the string
args=$(getopt -l "$opt_long" "$opt_short" "$@" 2> >(sed -e 's/^/stderr/g')) || echo -n "Error: " && echo "$args" | grep -oP "(?<=^stderr).*" && exit 1


# create new array of options
mapfile -t args < <(xargs -n1 <<< "$(echo "$args" | sed -E "s/(--[^ ]+) /\1=/g")" )


# overwrite $@ (options)
set -- "${args[@]}"


# parse options ([h]=help sets the variable "$opt_help" and [V]="" sets the variable "$opt_V")
while getopts "$opt_short-:" opt; do


# long option
if [[ "$opt" == "-" ]]; then


# extract long option name
opt="${OPTARG%%=*}"


# extract long option argument (may be empty)
OPTARG="${OPTARG#"$opt"}"


# remove "=" from long option argument
OPTARG="${OPTARG#=}"


# set variable name
opt=opt_$opt


# short option without argument uses long option name as variable name
elif [[ "${options[$opt]+x}" ]] && [[ "${options[$opt]}" ]]; then
opt=opt_${options[$opt]}


# short option with argument uses long option name as variable name
elif [[ "${options[$opt:]+x}" ]] && [[ "${options[$opt:]}" ]]; then
opt=opt_${options[$opt:]}


# short option without long option name uses short option name as variable name
else
opt=opt_$opt
fi


# remove double colon
opt="${opt%:}"


# options without arguments are set to 1
[[ ! $OPTARG ]] && OPTARG=1


# set variable variables
printf -v "$opt" '%s' "$OPTARG"


done


# remove all parsed options from $@
shift $((OPTIND-1))

现在,我只需要定义所需的选项名称和脚本source:

# import options module
declare -A options=( [h]=help [f:]=file: [V]=verbose [0]=long_only: [s]="" )
source "/usr/local/bin/inc/options.sh";


# display help text
if [[ $opt_help ]]; then
echo "help text"
exit
fi


# output
echo "help:$opt_help"
echo "file:$opt_file"
echo "verbose:$opt_verbose"
echo "long_only:$opt_long_only"
echo "short_only:$opt_s"
echo "path:$1"
echo "mail:$2"

在调用脚本时,可以完全随机地传递所有选项和非选项:

#             $opt_file     $1        $2          $opt_... $opt_... $opt_...
# /demo.sh --file=file.txt /dir info@example.com -V --long_only=yes -s
help:1
file:file.txt
verbose:1
long_only:yes
short_only:1
path=/dir
mail:info@example.com

笔记

  • options数组中,在选项名后添加:以启用参数。
  • 如果没有给出较长的选项名,则变量名将是$opt_X,其中X是较短的选项名。
  • 如果你想使用一个较长的选项名而不定义一个较短的选项名,那么将数组索引设置为一个数字,就像上面用[0]=long_only做的例子一样。当然,每个数组下标必须是唯一的。

使用的技术