如何parsingBash中的命令行参数?
说,我有一个脚本,调用这一行:
./myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile
或者这个:
./myscript -v -f -d -o /fizz/someOtherFile ./foo/bar/someFile
什么是可接受的parsing方式,使得在每种情况下(或两者的组合) $v
, $f
和$d
都将被设置为true
, $outFile
将等于/fizz/someOtherFile
?
首选方法:使用没有getopt的直接bash [s]
我原本是在OP问的时候回答这个问题的。 这个Q / A得到了很多关注,所以我也应该提供非魔术的方式来做到这一点。 我打算扩大guneysus的答案来解决讨厌的sed,包括Tobias Kienzler的build议 。
传递键值对参数的两种最常用的方法是:
直击的空间分离
用法./myscript.sh -e conf -s /etc -l /usr/lib /etc/hosts
#!/bin/bash POSITIONAL=() while [[ $# -gt 0 ]] do key="$1" case $key in -e|--extension) EXTENSION="$2" shift # past argument shift # past value ;; -s|--searchpath) SEARCHPATH="$2" shift # past argument shift # past value ;; -l|--lib) LIBPATH="$2" shift # past argument shift # past value ;; --default) DEFAULT=YES shift # past argument ;; *) # unknown option POSITIONAL+=("$1") # save it in an array for later shift # past argument ;; esac done set -- "${POSITIONAL[@]}" # restore positional parameters echo FILE EXTENSION = "${EXTENSION}" echo SEARCH PATH = "${SEARCHPATH}" echo LIBRARY PATH = "${LIBPATH}" echo DEFAULT = "${DEFAULT}" echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l) if [[ -n $1 ]]; then echo "Last line of file specified as non-opt/last argument:" tail -1 "$1" fi
直击是等分的
用法./myscript.sh -e=conf -s=/etc -l=/usr/lib /etc/hosts
#!/bin/bash for i in "$@" do case $i in -e=*|--extension=*) EXTENSION="${i#*=}" shift # past argument=value ;; -s=*|--searchpath=*) SEARCHPATH="${i#*=}" shift # past argument=value ;; -l=*|--lib=*) LIBPATH="${i#*=}" shift # past argument=value ;; --default) DEFAULT=YES shift # past argument with no value ;; *) # unknown option ;; esac done echo "FILE EXTENSION = ${EXTENSION}" echo "SEARCH PATH = ${SEARCHPATH}" echo "LIBRARY PATH = ${LIBPATH}" echo "Number files in SEARCH PATH with EXTENSION:" $(ls -1 "${SEARCHPATH}"/*."${EXTENSION}" | wc -l) if [[ -n $1 ]]; then echo "Last line of file specified as non-opt/last argument:" tail -1 $1 fi
为了更好地理解本指南中的 ${i#*=}
search“子串删除”。 它在function上等同于`sed 's/[^=]*=//' <<< "$i"`
,它调用了一个不必要的subprocess或`echo "$i" | sed 's/[^=]*=//'`
`echo "$i" | sed 's/[^=]*=//'`
调用两个不必要的subprocess。
使用getopt [s]
来自: http : //mywiki.wooledge.org/BashFAQ/035#getopts
切勿使用getopt(1)。 getopt
不能处理空的参数string或embedded空白的参数。 请忘记它曾经存在过。
POSIX shell(和其他)提供了可以安全使用的getopts
。 以下是一个简单的getopts
示例:
#!/bin/sh # A POSIX variable OPTIND=1 # Reset in case getopts has been used previously in the shell. # Initialize our own variables: output_file="" verbose=0 while getopts "h?vf:" opt; do case "$opt" in h|\?) show_help exit 0 ;; v) verbose=1 ;; f) output_file=$OPTARG ;; esac done shift $((OPTIND-1)) [ "$1" = "--" ] && shift echo "verbose=$verbose, output_file='$output_file', Leftovers: $@" # End of file
getopts
的优点是:
- 它是可移植的,并将工作在例如短跑。
- 它可以自动地以预期的Unix方式处理
-vf filename
。
getopts
的缺点是它只能处理简短的选项( -h
,而不是--help
)而没有欺骗。
有一个getopts教程解释了所有的语法和variables的含义。 在bash中,也有help getopts
,这可能是信息。
没有答案提到增强getopt 。 最 -vfd
答案是误导性的:它忽略-vfd
要求的-vfd
风格的短期选项,位置参数之后的选项(OP也要求),它忽略了parsing错误。 代替:
- 从util-linux或以前的GNU glibc使用增强的
getopt
。 1 - 它与
getopt_long()
GNU glibc的C函数一起工作。 - 具有所有有用的区别特征(其他人没有):
- 处理空格,引用字符,甚至二进制参数2
- 它可以在最后处理选项:
script.sh -o outFile file1 file2 -v
- 允许
=
-style长选项:script.sh --outfile=fileOut --infile fileIn
- 已经这么老了3 ,没有GNU系统缺less这个(例如任何Linux有它)。
- 你可以用下面的方法来testing它的存在:
getopt --test
→返回值4。 - 其他
getopt
或shell-getopts
的使用是有限的。
以下电话
myscript -vfd ./foo/bar/someFile -o /fizz/someOtherFile myscript -v -f -d -o/fizz/someOtherFile -- ./foo/bar/someFile myscript --verbose --force --debug ./foo/bar/someFile -o/fizz/someOtherFile myscript --output=/fizz/someOtherFile ./foo/bar/someFile -vfd myscript ./foo/bar/someFile -df -v --output /fizz/someOtherFile
所有的回报
verbose: y, force: y, debug: y, in: ./foo/bar/someFile, out: /fizz/someOtherFile
用下面的myscript
#!/bin/bash getopt --test > /dev/null if [[ $? -ne 4 ]]; then echo "I'm sorry, `getopt --test` failed in this environment." exit 1 fi OPTIONS=dfo:v LONGOPTIONS=debug,force,output:,verbose # -temporarily store output to be able to check for errors # -eg use “--options” parameter by name to activate quoting/enhanced mode # -pass arguments only via -- "$@" to separate them correctly PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTIONS --name "$0" -- "$@") if [[ $? -ne 0 ]]; then # eg $? == 1 # then getopt has complained about wrong arguments to stdout exit 2 fi # read getopt's output this way to handle the quoting right: eval set -- "$PARSED" # now enjoy the options in order and nicely split until we see -- while true; do case "$1" in -d|--debug) d=y shift ;; -f|--force) f=y shift ;; -v|--verbose) v=y shift ;; -o|--output) outFile="$2" shift 2 ;; --) shift break ;; *) echo "Programming error" exit 3 ;; esac done # handle non-option arguments if [[ $# -ne 1 ]]; then echo "$0: A single input file is required." exit 4 fi echo "verbose: $v, force: $f, debug: $d, in: $1, out: $outFile"
在大多数的“bash系统”上都有1个增强的getopt,包括Cygwin; 在OS X上尝试brew install gnu-getopt
2 POSIX exec()
约定没有可靠的方法在命令行参数中传递二进制NULL; 那些字节过早地结束了参数
3 1997年或之前发布的第一个版本(我只追溯到1997年)
getopt()
/ getopts()
是一个不错的select。 从这里偷来的 :
在这个小脚本中显示了“getopt”的简单使用:
#!/bin/bash echo "Before getopt" for i do echo $i done args=`getopt abc:d $*` set -- $args echo "After getopt" for i do echo "-->$i" done
我们所说的是,-a,-b,-c或-d都是允许的,但是-c之后是一个参数(“c:”表示)。
如果我们称之为“g”并尝试一下:
bash-2.05a$ ./g -abc foo Before getopt -abc foo After getopt -->-a -->-b -->-c -->foo -->--
我们从两个参数开始,“getopt”将选项分开,并将每个选项放在自己的参数中。 它还加了“ – ”。
来自: digitalpeer.com ,稍作修改
用法myscript.sh -p=my_prefix -s=dirname -l=libname
#!/bin/bash for i in "$@" do case $i in -p=*|--prefix=*) PREFIX="${i#*=}" ;; -s=*|--searchpath=*) SEARCHPATH="${i#*=}" ;; -l=*|--lib=*) DIR="${i#*=}" ;; --default) DEFAULT=YES ;; *) # unknown option ;; esac done echo PREFIX = ${PREFIX} echo SEARCH PATH = ${SEARCHPATH} echo DIRS = ${DIR} echo DEFAULT = ${DEFAULT}
为了更好地理解本指南中的 ${i#*=}
search“子串删除”。 它在function上等同于`sed 's/[^=]*=//' <<< "$i"`
,它调用了一个不必要的subprocess或`echo "$i" | sed 's/[^=]*=//'`
`echo "$i" | sed 's/[^=]*=//'`
调用两个不必要的subprocess。
冒着添加另一个例子来忽略,这是我的计划。
- 处理
-n arg
和--name=arg
- 允许在最后的论点
- 如果有任何拼写错误,会显示正常的错误
- 兼容,不使用bashisms
- 可读,不需要在一个循环中维护状态
希望对某人有用。
while [ "$#" -gt 0 ]; do case "$1" in -n) name="$2"; shift 2;; -p) pidfile="$2"; shift 2;; -l) logfile="$2"; shift 2;; --name=*) name="${1#*=}"; shift 1;; --pidfile=*) pidfile="${1#*=}"; shift 1;; --logfile=*) logfile="${1#*=}"; shift 1;; --name|--pidfile|--logfile) echo "$1 requires an argument" >&2; exit 1;; -*) echo "unknown option: $1" >&2; exit 1;; *) handle_argument "$1"; shift 1;; esac done
我对这个问题迟了大约4年,但是想要回报。 我用较早的答案作为整理我旧的adhoc paramparsing的一个起点。 然后我重构了下面的模板代码。 它处理长和短的参数,使用=或空格分隔的参数,以及多个短参数组合在一起。 最后它将任何非参数参数重新插入到$ 1,$ 2 ..variables中。 我希望这是有用的。
#!/usr/bin/env bash # NOTICE: Uncomment if your script depends on bashisms. #if [ -z "$BASH_VERSION" ]; then bash $0 $@ ; exit $? ; fi echo "Before" for i ; do echo - $i ; done # Code template for parsing command line parameters using only portable shell # code, while handling both long and short params, handling '-f file' and # '-f=file' style param data and also capturing non-parameters to be inserted # back into the shell positional parameters. while [ -n "$1" ]; do # Copy so we can modify it (can't modify $1) OPT="$1" # Detect argument termination if [ x"$OPT" = x"--" ]; then shift for OPT ; do REMAINS="$REMAINS \"$OPT\"" done break fi # Parse current opt while [ x"$OPT" != x"-" ] ; do case "$OPT" in # Handle --flag=value opts like this -c=* | --config=* ) CONFIGFILE="${OPT#*=}" shift ;; # and --flag value opts like this -c* | --config ) CONFIGFILE="$2" shift ;; -f* | --force ) FORCE=true ;; -r* | --retry ) RETRY=true ;; # Anything unknown is recorded for later * ) REMAINS="$REMAINS \"$OPT\"" break ;; esac # Check for multiple short options # NOTICE: be sure to update this pattern to match valid options NEXTOPT="${OPT#-[cfr]}" # try removing single short opt if [ x"$OPT" != x"$NEXTOPT" ] ; then OPT="-$NEXTOPT" # multiple short opts, keep going else break # long form, exit inner loop fi done # Done with that param. move to next shift done # Set the non-parameters back into the positional parameters ($1 $2 ..) eval set -- $REMAINS echo -e "After: \n configfile='$CONFIGFILE' \n force='$FORCE' \n retry='$RETRY' \n remains='$REMAINS'" for i ; do echo - $i ; done
更简单的方法是:
while [[ "$#" > 1 ]]; do case $1 in --deploy) deploy="$2";; --uglify) uglify="$2";; *) break;; esac; shift; shift done echo "Should deploy? $deploy" echo "Should uglify? $uglify"
我的答案很大程度上是基于Bruno Bronosky的回答 ,但我把他的两个纯粹的bash实现混合到了一个我经常使用的实现中。
# As long as there is at least one more argument, keep looping while [[ $# -gt 0 ]]; do key="$1" case "$key" in # This is a flag type option. Will catch either -f or --foo -f|--foo) FOO=1 ;; # Also a flag type option. Will catch either -b or --bar -b|--bar) BAR=1 ;; # This is an arg value type option. Will catch -o value or --output-file value -o|--output-file) shift # past the key and to the value OUTPUTFILE="$1" ;; # This is an arg=value type option. Will catch -o=value or --output-file=value -o=*|--output-file=*) # No need to shift here since the value is part of the same string OUTPUTFILE="${key#*=}" ;; *) # Do whatever you want with extra options echo "Unknown option '$key'" ;; esac # Shift after checking all the cases to get the next option shift done
这允许你有两个空间分开的选项/值,以及相同的定义值。
所以你可以运行你的脚本使用:
./myscript --foo -b -o /fizz/file.txt
以及:
./myscript -f --bar -o=/fizz/file.txt
两者应该有相同的最终结果。
优点:
-
允许-arg = value和-arg值
-
与可以在bash中使用的任何参数名称一起使用
- 意思是 – 一个或者–arg或者–arg或者-arg或者其他的
-
纯粹的打击。 不需要学习/使用getopt或getopts
缺点:
-
不能组合参数
- 含义没有-abc。 你必须做-a -b -c
这些是我能想到的唯一的优点/缺点
我觉得这个很简单,可以使用:
#!/bin/bash # readopt='getopts $opts opt;rc=$?;[ $rc$opt == 0? ]&&exit 1;[ $rc == 0 ]||{ shift $[OPTIND-1];false; }' opts=vfdo: # Enumerating options while eval $readopt do echo OPT:$opt ${OPTARG+OPTARG:$OPTARG} done # Enumerating arguments for arg do echo ARG:$arg done
调用示例:
./myscript -v -do /fizz/someOtherFile -f ./foo/bar/someFile OPT:v OPT:d OPT:o OPTARG:/fizz/someOtherFile OPT:f ARG:./foo/bar/someFile
通过@guneysus扩展出色的答案,这是一个调整,让用户使用任何他们喜欢的语法,例如
command -x=myfilename.ext --another_switch
VS
command -x myfilename.ext --another_switch
也就是说可以用空格来代替等号。
这个“模糊的解释”可能不是你所喜欢的,但是如果你正在制作可与其他工具互换的脚本(就像我的工作必须与ffmpeg一样),灵活性是有用的。
STD_IN=0 prefix="" key="" value="" for keyValue in "$@" do case "${prefix}${keyValue}" in -i=*|--input_filename=*) key="-i"; value="${keyValue#*=}";; -ss=*|--seek_from=*) key="-ss"; value="${keyValue#*=}";; -t=*|--play_seconds=*) key="-t"; value="${keyValue#*=}";; -|--stdin) key="-"; value=1;; *) value=$keyValue;; esac case $key in -i) MOVIE=$(resolveMovie "${value}"); prefix=""; key="";; -ss) SEEK_FROM="${value}"; prefix=""; key="";; -t) PLAY_SECONDS="${value}"; prefix=""; key="";; -) STD_IN=${value}; prefix=""; key="";; *) prefix="${keyValue}=";; esac done
我发现这个问题写在脚本中的便携式parsing令人沮丧,我已经写了Argbash – 一个FOSS代码生成器,可以生成参数 – 为您的脚本parsing代码加上它有一些很好的function:
这是我如何在一个函数中做的,以避免打破getopts同时在栈中更高的地方运行:
function waitForWeb () { local OPTIND=1 OPTARG OPTION local host=localhost port=8080 proto=http while getopts "h:p:r:" OPTION; do case "$OPTION" in h) host="$OPTARG" ;; p) port="$OPTARG" ;; r) proto="$OPTARG" ;; esac done ... }
如果#1你已经安装了getopts,#2你打算在同一个平台上运行它。 OSX和Linux(例如)在这方面performance不同。
这里是一个(非getopts)解决scheme,它支持equals,non-equals和boolean标志。 例如,你可以用这种方式运行你的脚本:
./script --arg1=value1 --arg2 value2 --shouldClean # parse the arguments. COUNTER=0 ARGS=("$@") while [ $COUNTER -lt $# ] do arg=${ARGS[$COUNTER]} let COUNTER=COUNTER+1 nextArg=${ARGS[$COUNTER]} if [[ $skipNext -eq 1 ]]; then echo "Skipping" skipNext=0 continue fi argKey="" argVal="" if [[ "$arg" =~ ^\- ]]; then # if the format is: -key=value if [[ "$argKey" =~ \= ]]; then argVal=$(echo "$argKey" | cut -d'=' -f2) argKey=$(echo "$argKey" | cut -d'=' -f1) skipNext=0 # if the format is: -key value elif [[ ! "$nextArg" =~ ^\- ]]; then argKey="$arg" argVal="$nextArg" skipNext=1 # if the format is: -key (a boolean flag) elif [[ "$nextArg" =~ ^\- ]] || [[ -z "$nextArg" ]]; then argKey="$arg" argVal="" skipNext=0 fi # if the format has not flag, just a value. else argKey="" argVal="$arg" skipNext=0 fi case "$argKey" in --source-scmurl) SOURCE_URL="$argVal" ;; --dest-scmurl) DEST_URL="$argVal" ;; --version-num) VERSION_NUM="$argVal" ;; -c|--clean) CLEAN_BEFORE_START="1" ;; -h|--help|-help|--h) showUsage exit ;; esac done
我给你的函数parse_params
将parsing参数:
- 不污染全球范围。
- 毫不费力地返回给你准备好使用variables,以便你可以build立更多的逻辑
- params之前的破折号数量并不重要(
--all
等于-all
等于all=all
)
下面的脚本是一个复制粘贴的工作演示。 请参阅show_use
函数以了解如何使用parse_params
。
限制:
- 不支持空格分隔的参数(
-d 1
) - 参数名称会丢失破折号,所以
--any-param
和-anyparam
是等价的 -
eval $(parse_params "$@")
必须在bash 函数中使用 (它不会在全局范围内工作)
#!/bin/bash # Universal Bash parameter parsing # Parse equals separated params into named local variables # Standalone named parameter value will equal param name (--force creates variable $force=="force") # Parses multi-valued named params into array (--path=path1 --path=path2 creates ${path[*]} array) # Parses un-named params into ${ARGV[*]} array # @author Oleksii Chekulaiev # @version v1.2 (Aug-24-2017) parse_params () { local existing_named local ARGV=() echo "local ARGV=(); " while [[ "$1" != "" ]]; do # If equals delimited named parameter if [[ "$1" =~ ^..*=..* ]]; then # key is part before first = local _key=$(echo "$1" | cut -d = -f 1) # val is everything after key and = (protect from param==value error) local _val="${1/$_key=}" # remove dashes from key name _key=${_key//\-} # search for existing parameter name if (echo "$existing_named" | grep "\b$_key\b" >/dev/null); then # if name already exists then it's a multi-value named parameter # re-declare it as an array if needed if ! (declare -p _key 2> /dev/null | grep -q 'declare \-a'); then echo "$_key=(\"\$$_key\");" fi # append new value echo "$_key+=('$_val');" else # single-value named parameter echo "local $_key=\"$_val\";" existing_named=" $_key" fi # If standalone named parameter elif [[ "$1" =~ ^\-. ]]; then # remove dashes local _key=${1//\-} echo "local $_key=\"$_key\";" # non-named parameter else # escape asterisk to prevent bash asterisk expansion _escaped=${1/\*/\'\"*\"\'} echo "ARGV+=('$_escaped');" fi shift done } #--------------------------- DEMO OF THE USAGE ------------------------------- show_use () { eval $(parse_params "$@") # -- echo "${ARGV[0]}" echo "${ARGV[1]}" echo "$anyparam" echo "$k" echo "${multivalue[0]}" echo "${multivalue[1]}" [[ "$force" == "force" ]] && echo "\$force is set so let the force be with you" } show_use "param 1" --anyparam="my value" param2 k=5 --force --multi-value=test1 --multi-value=test2
我想提供我的版本的选项parsing,允许以下:
-s p1 --stage p1 -w somefolder --workfolder somefolder -sw p1 somefolder -e=hello
也允许这个(可能是不需要的):
-s--workfolder p1 somefolder -se=hello p1 -swe=hello p1 somefolder
您必须在使用前决定是否在选项上使用=。 这是为了保持代码清洁(ISH)。
while [[ $# > 0 ]] do key="$1" while [[ ${key+x} ]] do case $key in -s*|--stage) STAGE="$2" shift # option has parameter ;; -w*|--workfolder) workfolder="$2" shift # option has parameter ;; -e=*) EXAMPLE="${key#*=}" break # option has been fully handled ;; *) # unknown option echo Unknown option: $key #1>&2 exit 10 # either this: my preferred way to handle unknown options break # or this: do this to signal the option has been handled (if exit isn't used) ;; esac # prepare for next option in this key, if any [[ "$key" = -? || "$key" == --* ]] && unset key || key="${key/#-?/-}" done shift # option(s) fully processed, proceed to next input argument done
EasyOptions不需要任何parsing:
## Options: ## --verbose, -v Verbose mode ## --output=FILE Output filename source easyoptions || exit if test -n "${verbose}"; then echo "output file is ${output}" echo "${arguments[@]}" fi
混合位置和基于标志的参数
–param = arg(等于分隔)
自由混合位置参数之间的标志:
./script.sh dumbo 127.0.0.1 --environment=production -q -d ./script.sh dumbo --environment=production 127.0.0.1 --quiet -d
可以用一个相当简洁的方法来完成:
# process flags pointer=1 while [[ $pointer -le $# ]]; do param=${!pointer} if [[ $param != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer else case $param in # paramter-flags with arguments -e=*|--environment=*) environment="${param#*=}";; --another=*) another="${param#*=}";; # binary flags -q|--quiet) quiet=true;; -d) debug=true;; esac # splice out pointer frame from positional list [[ $pointer -gt 1 ]] \ && set -- ${@:1:((pointer - 1))} ${@:((pointer + 1)):$#} \ || set -- ${@:((pointer + 1)):$#}; fi done # positional remain node_name=$1 ip_address=$2
– 参数arg(空格分隔)
通常情况下,不要混合使用 – --flag=value
和 – --flag value
样式。
./script.sh dumbo 127.0.0.1 --environment production -q -d
这是有点冒险的阅读,但仍然有效
./script.sh dumbo --environment production 127.0.0.1 --quiet -d
资源
# process flags pointer=1 while [[ $pointer -le $# ]]; do if [[ ${!pointer} != "-"* ]]; then ((pointer++)) # not a parameter flag so advance pointer else param=${!pointer} ((pointer_plus = pointer + 1)) slice_len=1 case $param in # paramter-flags with arguments -e|--environment) environment=${!pointer_plus}; ((slice_len++));; --another) another=${!pointer_plus}; ((slice_len++));; # binary flags -q|--quiet) quiet=true;; -d) debug=true;; esac # splice out pointer frame from positional list [[ $pointer -gt 1 ]] \ && set -- ${@:1:((pointer - 1))} ${@:((pointer + $slice_len)):$#} \ || set -- ${@:((pointer + $slice_len)):$#}; fi done # positional remain node_name=$1 ip_address=$2
请注意, getopt(1)
是AT&T短暂的错误。
getopt是在1984年创build的,但是在1986年已经被埋没了,因为它不是真正可用的。
getopt(1)
手册页仍然提到"$*"
而不是"$@"
,1986年,Bourne Shell和getopts(1)
shell内置getopts(1)
一起被添加到Bourne Shell中为了处理里面的空间的争论。
顺便说一句:如果您有兴趣parsingshell脚本中的长选项,可能有兴趣知道libc(Solaris)和ksh93
的getopt(3)
实现都添加了一个统一的长选项实现,该实现支持长选项作为别名选项。 这会导致ksh93
和Bourne Shell
通过getopts
实现长期选项的统一接口。
Bourne Shell手册页中的长选项示例:
getopts "f:(file)(input-file)o:(output-file)" OPTX "$@"
显示在Bourne Shell和ksh93中可以使用多长时间的选项别名。
查看最近的Bourne Shell的手册页:
http://schillix.sourceforge.net/man/man1/bosh.1.html
以及来自OpenSolaris的getopt(3)手册页:
http://schillix.sourceforge.net/man/man3c/getopt.3c.html
最后,getopt(1)手册页来validation过期的$ *:
这里是我使用variables数组的解决schemeBruno Bronosky的答案。
它可以让你混合参数的位置,并给你一个参数数组保存顺序没有选项
#!/bin/bash echo $@ PARAMS=() SOFT=0 SKIP=() for i in "$@" do case $i in -n=*|--skip=*) SKIP+=("${i#*=}") shift # past argument=value ;; -s|--soft) SOFT=1 shift # past argument=value ;; *) # unknown option PARAMS+=("$i") ;; esac done echo "SKIP = ${SKIP[@]}" echo "SOFT = $SOFT" echo "Parameters:" echo ${PARAMS[@]}
会输出例如:
$ ./test.sh parameter -s somefile --skip=.c --skip=.obj parameter -s somefile --skip=.c --skip=.obj SKIP = .c .obj SOFT = 1 Parameters: parameter somefile
解决scheme,保留未处理的参数。 包括演示。
这是我的解决scheme。 这是非常灵活的,不像其他人,不应该要求外部包和处理剩余的参数干净。
用法是: ./myscript -flag flagvariable -otherflag flagvar2
你所要做的就是编辑validflags行。 它前缀连字符并search所有参数。 然后它将下一个参数定义为标志名称,例如
./myscript -flag flagvariable -otherflag flagvar2 echo $flag $otherflag flagvariable flagvar2
主要代码(简短的版本,详细的例子进一步下来,也是一个错误的版本):
#!/usr/bin/env bash #shebang.io validflags="rate time number" count=1 for arg in $@ do match=0 argval=$1 for flag in $validflags do sflag="-"$flag if [ "$argval" == "$sflag" ] then declare $flag=$2 match=1 fi done if [ "$match" == "1" ] then shift 2 else leftovers=$(echo $leftovers $argval) shift fi count=$(($count+1)) done #Cleanup then restore the leftovers shift $# set -- $leftovers
内置回声演示的详细版本:
#!/usr/bin/env bash #shebang.io rate=30 time=30 number=30 echo "all args $@" validflags="rate time number" count=1 for arg in $@ do match=0 argval=$1 # argval=$(echo $@ | cut -d ' ' -f$count) for flag in $validflags do sflag="-"$flag if [ "$argval" == "$sflag" ] then declare $flag=$2 match=1 fi done if [ "$match" == "1" ] then shift 2 else leftovers=$(echo $leftovers $argval) shift fi count=$(($count+1)) done #Cleanup then restore the leftovers echo "pre final clear args: $@" shift $# echo "post final clear args: $@" set -- $leftovers echo "all post set args: $@" echo arg1: $1 arg2: $2 echo leftovers: $leftovers echo rate $rate time $time number $number
最后一个,如果一个无效的参数通过,那么这个错误就会出错。
#!/usr/bin/env bash #shebang.io rate=30 time=30 number=30 validflags="rate time number" count=1 for arg in $@ do argval=$1 match=0 if [ "${argval:0:1}" == "-" ] then for flag in $validflags do sflag="-"$flag if [ "$argval" == "$sflag" ] then declare $flag=$2 match=1 fi done if [ "$match" == "0" ] then echo "Bad argument: $argval" exit 1 fi shift 2 else leftovers=$(echo $leftovers $argval) shift fi count=$(($count+1)) done #Cleanup then restore the leftovers shift $# set -- $leftovers echo rate $rate time $time number $number echo leftovers: $leftovers
优点:它做什么,它处理得很好。 它保留了许多其他的解决scheme没有使用的参数。 它还允许调用variables,而不用在脚本中手动定义。 It also allows prepopulation of variables if no corresponding argument is given. (See verbose example).
Cons: Can't parse a single complex arg string eg -xcvf would process as a single argument. You could somewhat easily write additional code into mine that adds this functionality though.
The top answer to this question seemed a bit buggy when I tried it — here's my solution which I've found to be more robust:
boolean_arg="" arg_with_value="" while [[ $# -gt 0 ]] do key="$1" case $key in -b|--boolean-arg) boolean_arg=true shift ;; -a|--arg-with-value) arg_with_value="$2" shift shift ;; -*) echo "Unknown option: $1" exit 1 ;; *) arg_num=$(( $arg_num + 1 )) case $arg_num in 1) first_normal_arg="$1" shift ;; 2) second_normal_arg="$1" shift ;; *) bad_args=TRUE esac ;; esac done # Handy to have this here when adding arguments to # see if they're working. Just edit the '0' to be '1'. if [[ 0 == 1 ]]; then echo "first_normal_arg: $first_normal_arg" echo "second_normal_arg: $second_normal_arg" echo "boolean_arg: $boolean_arg" echo "arg_with_value: $arg_with_value" exit 0 fi if [[ $bad_args == TRUE || $arg_num < 2 ]]; then echo "Usage: $(basename "$0") <first-normal-arg> <second-normal-arg> [--boolean-arg] [--arg-with-value VALUE]" exit 1 fi
I have write a bash helper to write a nice bash tool
project home: https://gitlab.mbedsys.org/mbedsys/bashopts
例:
#!/bin/bash -ei # load the library . bashopts.sh # Enable backtrace dusplay on error trap 'bashopts_exit_handle' ERR # Initialize the library bashopts_setup -n "$0" -d "This is myapp tool description displayed on help message" -s "$HOME/.config/myapprc" # Declare the options bashopts_declare -n first_name -l first -of -d "First name" -t string -i -s -r bashopts_declare -n last_name -l last -ol -d "Last name" -t string -i -s -r bashopts_declare -n display_name -l display-name -t string -d "Display name" -e "\$first_name \$last_name" bashopts_declare -n age -l number -d "Age" -t number bashopts_declare -n email_list -t string -m add -l email -d "Email adress" # Parse arguments bashopts_parse_args "$@" # Process argument bashopts_process_args
will give help:
NAME: ./example.sh - This is myapp tool description displayed on help message USAGE: [options and commands] [-- [extra args]] OPTIONS: -h,--help Display this help -n,--non-interactive true Non interactive mode - [$bashopts_non_interactive] (type:boolean, default:false) -f,--first "John" First name - [$first_name] (type:string, default:"") -l,--last "Smith" Last name - [$last_name] (type:string, default:"") --display-name "John Smith" Display name - [$display_name] (type:string, default:"$first_name $last_name") --number 0 Age - [$age] (type:number, default:0) --email Email adress - [$email_list] (type:string, default:"")
enjoy 🙂
Here is my approach – using regexp.
- no getopts
- it handles block of short parameters
-qwerty
- it handles short parameters
-q -w -e
- it handles long options
--qwerty
- you can pass attribute to short or long option (if you are using block of short options, attribute is attached to the last option)
- you can use spaces or
=
to provide attributes, but attribute matches until encountering hyphen+space "delimiter", so in--q=qwe ty
qwe ty
is one attribute - it handles mix of all above so
-oa -op attr ibute --option=att ribu te --op-tion attribute --option att-ribute
is valid
脚本:
#!/usr/bin/env sh help_menu() { echo "Usage: ${0##*/} [-h][-l FILENAME][-d] Options: -h, --help display this help and exit -l, --logfile=FILENAME filename -d, --debug enable debug " } parse_options() { case $opt in h|help) help_menu exit ;; l|logfile) logfile=${attr} ;; d|debug) debug=true ;; *) echo "Unknown option: ${opt}\nRun ${0##*/} -h for help.">&2 exit 1 esac } options=$@ until [ "$options" = "" ]; do if [[ $options =~ (^ *(--([a-zA-Z0-9-]+)|-([a-zA-Z0-9-]+))(( |=)(([\_\.\?\/\\a-zA-Z0-9]?[ -]?[\_\.\?a-zA-Z0-9]+)+))?(.*)|(.+)) ]]; then if [[ ${BASH_REMATCH[3]} ]]; then # for --option[=][attribute] or --option[=][attribute] opt=${BASH_REMATCH[3]} attr=${BASH_REMATCH[7]} options=${BASH_REMATCH[9]} elif [[ ${BASH_REMATCH[4]} ]]; then # for block options -qwert[=][attribute] or single short option -a[=][attribute] pile=${BASH_REMATCH[4]} while (( ${#pile} > 1 )); do opt=${pile:0:1} attr="" pile=${pile/${pile:0:1}/} parse_options done opt=$pile attr=${BASH_REMATCH[7]} options=${BASH_REMATCH[9]} else # leftovers that don't match opt=${BASH_REMATCH[10]} options="" fi parse_options fi done
Use module "arguments" from bash-modules
例:
#!/bin/bash . import.sh log arguments NAME="world" parse_arguments "-n|--name)NAME;S" -- "$@" || { error "Cannot parse command line." exit 1 } info "Hello, $NAME!"
This also might be useful to know, you can set a value and if someone provides input, override the default with that value..
myscript.sh -f ./serverlist.txt or just ./myscript.sh (and it takes defaults)
#!/bin/bash # --- set the value, if there is inputs, override the defaults. HOME_FOLDER="${HOME}/owned_id_checker" SERVER_FILE_LIST="${HOME_FOLDER}/server_list.txt" while [[ $# > 1 ]] do key="$1" shift case $key in -i|--inputlist) SERVER_FILE_LIST="$1" shift ;; esac done echo "SERVER LIST = ${SERVER_FILE_LIST}"
Another solution without getopt[s], POSIX, old Unix style
Similar to the solution Bruno Bronosky posted this here is one without the usage of getopt(s)
.
Main differentiating feature of my solution is that it allows to have options concatenated together just like tar -xzf foo.tar.gz
is equal to tar -x -z -f foo.tar.gz
. And just like in tar
, ps
etc. the leading hyphen is optional for a block of short options (but this can be changed easily). Long options are supported as well (but when a block starts with one then two leading hyphens are required).
Code with example options
#!/bin/sh echo echo "POSIX-compliant getopt(s)-free old-style-supporting option parser from phk@[se.unix]" echo print_usage() { echo "Usage: $0 {a|b|c} [ARG...] Options: --aaa-0-args -a Option without arguments. --bbb-1-args ARG -b ARG Option with one argument. --ccc-2-args ARG1 ARG2 -c ARG1 ARG2 Option with two arguments. " >&2 } if [ $# -le 0 ]; then print_usage exit 1 fi opt= while :; do if [ $# -le 0 ]; then # no parameters remaining -> end option parsing break elif [ ! "$opt" ]; then # we are at the beginning of a fresh block # remove optional leading hyphen and strip trailing whitespaces opt=$(echo "$1" | sed 's/^-\?\([a-zA-Z0-9\?-]*\)/\1/') fi # get the first character -> check whether long option first_chr=$(echo "$opt" | awk '{print substr($1, 1, 1)}') [ "$first_chr" = - ] && long_option=T || long_option=F # note to write the options here with a leading hyphen less # also do not forget to end short options with a star case $opt in -) # end of options shift break ;; a*|-aaa-0-args) echo "Option AAA activated!" ;; b*|-bbb-1-args) if [ "$2" ]; then echo "Option BBB with argument '$2' activated!" shift else echo "BBB parameters incomplete!" >&2 print_usage exit 1 fi ;; c*|-ccc-2-args) if [ "$2" ] && [ "$3" ]; then echo "Option CCC with arguments '$2' and '$3' activated!" shift 2 else echo "CCC parameters incomplete!" >&2 print_usage exit 1 fi ;; h*|\?*|-help) print_usage exit 0 ;; *) if [ "$long_option" = T ]; then opt=$(echo "$opt" | awk '{print substr($1, 2)}') else opt=$first_chr fi printf 'Error: Unknown option: "%s"\n' "$opt" >&2 print_usage exit 1 ;; esac if [ "$long_option" = T ]; then # if we had a long option then we are going to get a new block next shift opt= else # if we had a short option then just move to the next character opt=$(echo "$opt" | awk '{print substr($1, 2)}') # if block is now empty then shift to the next one [ "$opt" ] || shift fi done echo "Doing something..." exit 0
For the example usage please see the examples further below.
Position of options with arguments
For what its worth there the options with arguments don't be the last (only long options need to be). So while eg in tar
(at least in some implementations) the f
options needs to be last because the file name follows ( tar xzf bar.tar.gz
works but tar xfz bar.tar.gz
does not) this is not the case here (see the later examples).
Multiple options with arguments
As another bonus the option parameters are consumed in the order of the options by the parameters with required options. Just look at the output of my script here with the command line abc XYZ
(or -abc XYZ
):
Option AAA activated! Option BBB with argument 'X' activated! Option CCC with arguments 'Y' and 'Z' activated!
Long options concatenated as well
Also you can also have long options in option block given that they occur last in the block. So the following command lines are all equivalent (including the order in which the options and its arguments are being processed):
-
-cba ZYX
-
cba ZYX
-
-cb-aaa-0-args ZYX
-
-c-bbb-1-args ZYX -a
-
--ccc-2-args ZY -ba X
-
c ZY b X a
-
-c ZY -b X -a
-
--ccc-2-args ZY --bbb-1-args X --aaa-0-args
All of these lead to:
Option CCC with arguments 'Z' and 'Y' activated! Option BBB with argument 'X' activated! Option AAA activated! Doing something...
Not in this solution
Optional arguments
Options with optional arguments should be possible with a bit of work, eg by looking forward whether there is a block without a hyphen; the user would then need to put a hyphen in front of every block following a block with a parameter having an optional parameter. Maybe this is too complicated to communicate to the user so better just require a leading hyphen altogether in this case.
Things get even more complicated with multiple possible parameters. I would advise against making the options trying to be smart by determining whether the an argument might be for it or not (eg with an option just takes a number as an optional argument) because this might break in the future.
I personally favor additional options instead of optional arguments.
Option arguments introduced with an equal sign
Just like with optional arguments I am not a fan of this (BTW, is there a thread for discussing the pros/cons of different parameter styles?) but if you want this you could probably implement it yourself just like done at http://mywiki.wooledge.org/BashFAQ/035#Manual_loop with a --long-with-arg=?*
case statement and then stripping the equal sign (this is BTW the site that says that making parameter concatenation is possible with some effort but "left [it] as an exercise for the reader" which made me take them at their word but I started from scratch).
Other notes
POSIX-compliant, works even on ancient Busybox setups I had to deal with (with eg cut
, head
and getopts
missing).
Assume we create a shell script named test_args.sh
as follow
#!/bin/sh until [ $# -eq 0 ] do name=${1:1}; shift; if [[ -z "$1" || $1 == -* ]] ; then eval "export $name=true"; else eval "export $name=$1"; shift; fi done echo "year=$year month=$month day=$day flag=$flag"
After we run the following command:
sh test_args.sh -year 2017 -flag -month 12 -day 22
The output would be:
year=2017 month=12 day=22 flag=true
This example shows how to use getopt
and eval
and HEREDOC
and shift
to handle short and long parameters with and without a required value that follows. Also the switch/case statement is concise and easy to follow.
#!/usr/bin/env bash # usage function function usage() { cat << HEREDOC Usage: $progname [--num NUM] [--time TIME_STR] [--verbose] [--dry-run] optional arguments: -h, --help show this help message and exit -n, --num NUM pass in a number -t, --time TIME_STR pass in a time string -v, --verbose increase the verbosity of the bash script --dry-run do a dry run, don't change any files HEREDOC } # initialize variables progname=$(basename $0) verbose=0 dryrun=0 num_str= time_str= # use getopt and store the output into $OPTS # note the use of -o for the short options, --long for the long name options # and a : for any option that takes a parameter OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@") if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; usage; exit 1 ; fi eval set -- "$OPTS" while true; do # uncomment the next line to see how shift is working # echo "\$1:\"$1\" \$2:\"$2\"" case "$1" in -h | --help ) usage; exit; ;; -n | --num ) num_str="$2"; shift 2 ;; -t | --time ) time_str="$2"; shift 2 ;; --dry-run ) dryrun=1; shift ;; -v | --verbose ) verbose=$((verbose + 1)); shift ;; -- ) shift; break ;; * ) break ;; esac done if (( $verbose > 0 )); then # print out all the parameters we read in cat <<-EOM num=$num_str time=$time_str verbose=$verbose dryrun=$dryrun EOM fi # The rest of your script below
The most significant lines of the script above are these:
OPTS=$(getopt -o "hn:t:v" --long "help,num:,time:,verbose,dry-run" -n "$progname" -- "$@") if [ $? != 0 ] ; then echo "Error in command line arguments." >&2 ; exit 1 ; fi eval set -- "$OPTS" while true; do case "$1" in -h | --help ) usage; exit; ;; -n | --num ) num_str="$2"; shift 2 ;; -t | --time ) time_str="$2"; shift 2 ;; --dry-run ) dryrun=1; shift ;; -v | --verbose ) verbose=$((verbose + 1)); shift ;; -- ) shift; break ;; * ) break ;; esac done
Short, to the point, readable, and handles just about everything (IMHO).
希望能帮助别人。