Shell脚本中参数处理方法
Shell 脚本中参数处理方法
在 Shell 脚本中处理命令行参数,可以使用 getopts/getopt 来进行——当然,手工解析也是可以的。
下面通过一个特定的情景来讲一下这三种参数处理方法。
手工解析
所谓的手工解析,就是取到参数后手工一个一个解析了,以下是手工解析上述情景参数的过程:
while [ $# -gt 0 ];do
case $1 in
-d)
shift
file_to_trash=$1
trash $file_to_trash # trash is a function
;;
-l)
print_trashed_file # print_trashed_file is a function
;;
-b)
shift
file_to_untrash=$1
untrash $file_to_untrash # untrash is a function
;;
-c)
clean_all # clean all is a function
;;
-h)
usage
exit 0
;;
\?)
usage
exit 1
;;
esac
done
这样,在使用了 shift 后,我们每次都只要去看参数列表中的第一个就行了。当然,其实不用'shift'也是可以的,比如说这样:
i=1
while [ $i -le $# ];do
case ${!i} in
-d)
i=$(expr $i + 1)
file_to_trash=${!i}
trash $file_to_trash # trash is a function
;;
-l)
print_trashed_file # print_trashed_file is a function
;;
-b)
i=$(expr $i + 1)
file_to_untrash=${!i}
untrash $file_to_untrash # untrash is a function
;;
-c)
clean_all # clean all is a function
;;
-h)
usage
exit 0
;;
\?)
usage
exit 1
;;
esac
i=$(expr $i + 1)
done
这样,在使用了 shift 后,我们每次都只要去看参数列表中的第一个就行了。当然,其实不用'shift'也是可以的,比如说这样:
i=1
while [ $i -le $# ];do
case ${!i} in
-d)
i=$(expr $i + 1)
file_to_trash=${!i}
trash $file_to_trash # trash is a function
;;
-l)
print_trashed_file # print_trashed_file is a function
;;
-b)
i=$(expr $i + 1)
file_to_untrash=${!i}
untrash $file_to_untrash # untrash is a function
;;
-c)
clean_all # clean all is a function
;;
-h)
usage
exit 0
;;
\?)
usage
exit 1
;;
esac
i=$(expr $i + 1)
done
getopts
'getopts'是 POSIX Shell 中内置的一个命令,其使用方法是:
getopts <opt_string> <optvar> <arguments>
本质上来说,'getopts'的处理和我们手工处理是差不多的,它不过是提供了更便利的方式而已。它的使用方式非常简单明了,其形式为:
while getopts <opt_string> <optvar>
case $<optvar> in
# ...
esac
done
其中 <opt_string> 是要处理的选项的一个集合,每个选项在其中用不包含连字符'-'的字母来表示,每个代表选项的字母前后可以有一个冒号,前面有冒号表示当处理该选项出错时不输出'getopts'自身产生的错误信息,这方便我们自己编写对应的错误处理方法;后面的冒号表示这个选项需要一个值。对于我们这个"安全删除"的例子,这个 <opt_string> 应该是:
d:lb:ch
冒号的归属的话,先到先得吧,大概是这样。
在使用'getopts'时,有两个特殊的变量,它们是 OPTIND 和 OPTARG ,前者表示当前参数在参数列表中的位置——相当于手工解析第二种方法中那个自定义的变量 i ,其值初始时为 1, 会在每次取了选项以及其值(如果有的话)后更新; OPTARG 则是在选项需要值时,存储这个选项对应的值。这样,我们这个例子用'getopts'就可以写成:
while getopts d:lb:ch OPT;do
case $OPT in
d)
file_to_trash=$OPTARG
trash $file_to_trash # trash is a function
;;
l)
print_trashed_file # print_trashed_file is a function
;;
b)
file_to_untrash=$OPTARG
untrash $file_to_untrash # untrash is a function
;;
c)
clean_all # clean all is a function
;;
h)
usage
exit 0
;;
\?)
usage
exit 1
;;
esac
done
对比可以看到,相比手工解析的第一种办法,又更为简洁一点了。不过需要注意的是,'getopts'会从第一个参数开始,只按照 <opt_string> 指定的形式来寻找并解析参数,如果给出的实际命令行参数与其所描述的参数形式不符,则会出错中止。
比如说,对于上面的例子,假设这个脚本已经完全写好了,脚本名为 trash.sh ,其参数处理就是上面这样,那么如果我在终端里执行:
./trash.sh a -b hello.txt
开始那个多余的参数'a'将会导致'getopts'在解析到选项'-b'前就出错终止。所以呢,像使用'getopts'这样的方法,其自由度不如手工解析,如果要保证脚本在任何情况下都能正确解析参数,它需要多做一点——当然啦,上面这个愚蠢的错误使用情况还是比较少出现的啦,反正我现在写的脚本里压根没考虑这样的情况。
getopt
'getopt'与'getopts'类似,不过'getopts'只能处理短选项,'getopt'则能处理短选项和长选项。所谓的短选项就是类似下面这样的选项:
-a
而下面这样的则是长选项
--action=delete
当然,事无绝对,通过一些技巧,用'getopts'处理长选项也是可能的。这里先说一下如何用'getopt'来处理参数吧。
需要事先说明的一点是,'getopt'不是 Shell 内建的命令,而是'util-linux'这个软件包提供的功能,它不是 POSIX 标准的一部分,所以也有人建议不使用'getopt'。
首先将之前说到的五种动作对应的短选项扩展一下,以便讲解'getopt'的使用:
- -d/–delete : 将文件移动到回收站,该选项后需要指定一个文件或目录名
- -l/–list : 列出被移动到回收站的文件及其 id,该选项不需要值
- -b/–back : 恢复被移动到回收站的文件,该选项需要指定一个文件对应的 id
- -c/–clear : 清空回收站,该选项不需要值
- -h/–help : 打印帮助信息
'getopt'既能处理短选项也能处理长选项,短选项通过参数 -o 指定,长选项通过参数 -l 指定。同'getopts'一样,它一次也只解析一个选项,所以也需要循环处理,不过与'getopts'不同的是,'getopt'没有使用 OPTIND 和 OPTARG 这两个变量,所以我们还得手动对参数进行'shift',对需要值的选项,也得手动去取出值。
可以看到,'getopt'将参数中以下形式的内容:
--longopt=argument
在返回结果中替换成下面这样的形式:
--longopt argument
这样就可以通过循环和'shift'来进行处理了,不过在脚本中,'shift'命令是对命令行参数起作用的,即特殊变量"$@",而我们在脚本中只能将'getopt'的返回结果作为字符串存储到一个变量中。为了让'shift'起作用,通常还要使用'set'命令来将变量的值赋给"$@"这个特殊变量。
真是有够麻烦的……算了,下面再集中吐槽吧……
然后,在设置好短选项和长选项后,在将实际的参数传给'getopt'时,要在实际参数前加上一个两个连字符 -- ,而'getopt'会将这两个连字符放到返回结果的最后面,在处理时可以将这两个连字符视为结束标志。
以下是针对本文假设的情景,使用'getopt'解析参数的流程:
arg=$(getopt -o d:lb:ch -l delete:,list,back:,clear,help -- $@)
set -- "$arg"
while true
do
case $1 in
-d|--delete)
file_to_trash=$2
trash $file_to_trash # trash is a function
shift 2
;;
-l|--list)
print_trashed_file # print_trashed_file is a function
shift
;;
-b|--back)
file_to_untrash=$2
untrash $file_to_untrash # untrash is a function
shift
;;
-c|--clear)
clean_all # clean all is a function
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
esac
done
然而,知道了'getopt'的使用及其原理后,自然而然地可以发现,我可以不用去管这个结束标志,用"$#"这个表示参数个数的特殊变量,同样可以控制参数解析的流程,这完全和手工解析是同一个道理。我甚至可以将'getopt'的返回结果存储到一个数组里,直接循环处理这个数组,而不用使用'set'命令了。
好了,吐槽时间。
我之前写脚本都是用的'getopts',一来我用不上长选项,二来'getopts'的使用足够简单。在写本文之前,我倒是知道'getopt'可以处理长选项,但没仔细了解过。这两天了解了一下,觉得还是别用'getopt'的好,理由如下:
-
'getopt'不是 Shell 内建命令,跨平台使用时可能会出现问题;
-
只是将'–longopt=val'这样的参数形式替换成了'–longopt val',但因此增加了许多复杂性,比如使用了'set'命令,在使用'set'命令时还要考虑'getopt'的返回结果中有无 Shell 命令,有的话应该使用'eval'命令来消除可能导致的错误
eval set -- "$arg"
-
调用完还要进行与手工解析类似的工作,相比手工解析,并没有多大优势;
-
真的需要长选项吗?我觉得短选项就足够了
getopts 处理长选项
既然不建议使用'getopt',那么怎么处理长选项呢?自然是有办法的。
为了方便讲解,这里假设一个简单的情景吧,在这个情景里,我们只需要处理两个可能的选项
- -f/–file: 设置文件名,该选项需要值
- -h/–help: 打印帮助信息,该选项不需要值
用'getopts'处理这种情况,可以这么做:
filename=""
while getopts f:h-: opt;do
case $opt in
-)
case $OPTARG in
help)
usage
exit 0
;;
file=*)
filename=${OPTARG#*=}
;;
esac
;;
f)
filename=$OPTARG
;;
h)
usage
exit 0
;;
\?)
usage
exit 1
;;
esac
done
当然,也许并不比手工解析简洁多少,但用起来肯定是比'getopt'要舒服的。
在函数中解析参数
有时候,我们也许想把参数解析的工作放到函数中去做,比如说定义了一个'main'函数然后在'main'函数中封装整个流程处理逻辑。又或者像我一样,写了几个小小的工具函数,放到了 Bash 的配置文件 .bashrc 中,参数解析的工作必须得在函数中做。
手工解析是能想到的最直接的办法,简单可行。
不过假如我们想用'getopts'来处理呢?动手尝试后,你会发现直接在函数中使用'getopts'是会出错的。要在函数中使用'getopts',必须在这个函数中使用'getopts'前,将 OPTIND 这个被'getopts'使用的特殊变量设置为函数局部变量,像这样:
function main() {
local OPTIND
while getopts d:lb:ch OPT;do
case $OPT in
d)
file_to_trash=$OPTARG
trash $file_to_trash # trash is a function
;;
l)
print_trashed_file # print_trashed_file is a function
;;
b)
file_to_untrash=$OPTARG
untrash $file_to_untrash # untrash is a function
;;
c)
clean_all # clean all is a function
;;
h)
usage
exit 0
;;
\?)
usage
exit 1
;;
esac
done
}
main $@
就是这样啦!