一、概述
在编写shell脚本时经常会遇到一个问题,即传递命令行参数。如果只考虑最简单的应用场景,即所有参数都按顺序,显示传递进去,shell脚本倒是非常方便,终端直接传,脚本里面对应顺序的参数用 $1,$2.. 等表示即可。这种用法对应Python中的sys.argv参数传递模式。不过当参数数量较多时,我们往往希望某些参数能提供默认值,从而只需传递少量参数即可运行。熟悉Python的朋友应该都使用过Argparse模块。该模块提供了常用的命令行参数解析功能,包括设置参数类型,默认值,选择范围等,当然还有便捷的输入提示(不要小看了这个功能,没有合理的参数提示,会对他人的使用造成很大不便,而且过一段时间自己也不记得运行时的细节...)。
1.1 问题分析
总结一下,个人认为命令行解析可分成三个不同level的问题:
I. 简单参数传递,无解析。这种模式可以在写脚本一时获取廉价的快感,毕竟用python写个argparse也得好几行,如果用sys.argv或者shell传递几乎没有工作量。
II. 参数设置默认值。该问题稍复杂但是有时候是必须面对的。当然对于这个问题,如果还想偷懒也不是不可以。比如在一些简单脚本中我就经常用下面的写法来😁 (注意python脚本的脚本名占 了第一个参数~)
if [[ $# < 2 ]] # Shell
then
var=default_value
else
var=$2
fi
if len(sys.argv) < 3: # Python
var = default_value
else:
var = sys.argv[2]
然而这种根据参数数目来过滤的写法,很难在多个参数时针对性的设置默认值。这个级别的参数传递问题由于也比较常用,Python的话用argparse设置default即可,但是shell脚本就比较麻烦,具体的解决办法见下文。
- III. 完善的参数解析
如果前两个级别的参数解析,在自己编写脚本时还能凑合使用,那么最后一级别的参数解析应该可以视为一个开发者必备的工具了。完善的脚本在团队开发和代码维护中可以减少很多不必要的交接过程和重复工作。一个常见的场景是SDK的封装,一方面开发者希望保留足够的参数接口用于调试,另一方面又不希望要求用户提供大量输入参数。对于Python脚本,argparse完全够用,即使是pyinstaller类似的封装工具,也可以无脑支持使用argparse解析的脚本。然而linux环境下,我暂时还没找到这一级别对应的解析方法...😹,如果哪位朋友有经验,请不吝赐教~
二、Shell命令行设置默认参数
其实我目前只有一个比较生硬的办法,而且并不是用主流的getopts或者getopts,一方面目前还没有编写大型shell脚本的需求(稍微复杂的功能还是Python实现优先),另一方面,看到getopts的example就放弃了...参数解析部分的代码比我脚本都长...
废话终于说完了。现在上代码,除了有一些限制,可以说是无脑使用了。看一个例子:test_args.sh
#!/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"
运行方式:
sh test_args.sh -year 2017 -flag -month 12 -day 22
输出:
year=2017 month=12 day=22 flag=true
经过几次实验,我总结出这种参数解析的用法:
- 传递的形参名和实参名一致,且形参前加 "-",后面紧跟对应实参
- 参数顺序随意
- bool型参数无需显式传递值(上面的
-flag
)
那我们关系的默认参数设置问题解决了吗?答案是,至此还没有。观察下实验的输出:
可以看出几个现象:
- 用这种解析方式,
$#
命令不能给出正确的有效参数个数,因为它会按照正常shell脚本的参数(以空格隔开)来判断输入个数。 - 未传入的参数,显示值为空(等号后面没东西)。
根据这一观察,我们其实有一个比较粗暴的设置默认值的方法,即先判断某个指定参数是否为空,如果为空就给他赋一个值...否则就用实参的值。不过这里又出现一个问题,等号后面的空白代表啥?
首先排除Python里的None😒...其实我最后也没发现那个空白是啥符号...不过我发现一个判断条件,就是判断该参数的长度是否为0。
完整的Argparser模板可以是这样:
########## argparser ###########
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
########## set default ###########
if [ ${#my_arg} == 0 ]
then
my_arg=XXXX; # default value to give
else
my_arg=$my_arg;
fi
所以这就是我目前对shell命令行参数设置默认值的处理方法...显然,这不是最优的方法,但是对于逻辑简单的shell脚本也够用了(相比完全不解析参数还是有质的飞跃😁)
后续如果有稍微复杂一些的需求,可能会继续更新~如果有朋友有更好的方法,还请多多交流!
三、 更新: 使用getopts:
declare others=${@:4}
function Argparser () {
while getopts 'c:lh' arg "$@";
do
case $arg in
'c')
custom_config="--custom $OPTARG"
;;
'o')
is_online="--online"
;;
'l')
use_local_config="--local_config"
;;
'h')
echo "TBD"
exit
;;
?)
echo "UNKNWON ARGS: ${OPTARG} "
exit 1
;;
esac
done
}
Argparser ${others}
参考:
[1] https://stackoverflow.com/questions/192249/how-do-i-parse-command-line-arguments-in-bash