一、为什么需要更加复杂的命令行选项处理方式
我们经常会遇到 Shell 脚本中需要获取命令行参数的场景,最简单的方式就是通过 $1
、$2
……这样的方式获取。这种方式对于只需要一两个参数的场景简单实用,但对于需要多个参数、需要可选参数等复杂一些的场景会带来一些问题:
- 必须严格遵守参数传入顺序,不容易记忆,容易出错
- 无法提供“参数默认值”
- 参数含义不够明确
举个例子进行说明,假设我们有一个连接主机的脚本,需要传入以下参数:
- 用户名
- 密码
- 主机(必填项,并不是可选项,不需要指定参数名称)
- 端口
- 显示连接详情
如果通过 $1
、$2
的方式来获取参数,前面提出的几个问题就显得比较明显了。
我们更希望通过类似下方式来传参:
$ myscript -u username -p password -v -n 9999 192.168.1.2
这时 getopt
/ getopts
就就可以大展拳脚了。
二、getopts
和 getopt
的区别和应用场景
getopt
与 getopts
都是 Bash 中用来获取与分析命令行参数的工具,常用在 Shell 脚本中被用来分析脚本参数。
两者的比较:
-
getopts
是 Shell 内建命令,getopt
是一个独立外部工具 -
getopts
使用语法简单,getopt
使用语法较复杂 -
getopts
不支持长参数(如:--option
),getopt
支持长参数 -
getopts
出现的目的是为了在不太复杂的场景代替getopt
较快捷地执行参数分析工作 -
getopts
负责参数解析,可以方便地提取参数值,getopt
只负责按规则重新对参数进行排列,进一步解析需要自行编写代码处理
三、getopts
使用说明
1、举例
根据前面提到的案例:
$ myscript -u username -p password -v -n 9999 192.168.1.2
参数说明:
参数 | 说明 |
---|---|
-u |
用户名 |
-p |
密码 |
-n |
端口 |
-v |
显示详情 |
无名称参数 | 主机 |
#!/bin/bash
# 处理脚本参数
# -u 用户名
# -p 密码
# -v 是否显示详情
# -n 端口
while getopts ":u:p:n:v" opt_name # 通过循环,使用 getopts,按照指定参数列表进行解析,参数名存入 opt_name
do
case "$opt_name" in # 根据参数名判断处理分支
'u') # -u
CONN_USERNAME="$OPTARG" # 从 $OPTARG 中获取参数值
;;
'p') # -p
CONN_PASSWORD="$OPTARG"
;;
'v') # -v
CONN_SHOW_DETAIL=true
;;
'n') # -n
CONN_PORT="$OPTARG"
;;
?) # 其它未指定名称参数
echo "Unknown argument(s)."
exit 2
;;
esac
done
# 删除已解析的参数
shift $((OPTIND-1))
# 通过第一个无名称参数获取 主机
CONN_HOST="$1"
# 显示获取参数结果
echo 用户名 "$CONN_USERNAME"
echo 密码 "$CONN_PASSWORD"
echo 主机 "$CONN_HOST"
echo 端口 "$CONN_PORT"
echo 显示详情 "$CONN_SHOW_DETAIL"
2、说明
getopts
的工作思路是:
(1)指定命令行中需要解析的参数名称
本例中,:u:p:n:v
就是指定要解析的参数名称。
规则说明:
- 其中的字母表示需要解析的参数名称
- 字母后面的冒号
:
表示该参数除了其本身,还会带上一个参数作为选项的值,传入的值通过$OPTARG
变量获取 - 字母后面没有冒号
:
表示该参数为开关型选项,不需要再指定值,只作为是否存在的标记 - 字符串开头的冒号
:
表示解析过程中,遇到未在getopts
参数列表中指定的参数,不显示报错信息。否则会报出错误。
(2)通过循环逐个读取参数
使用 getopts
解析参数时,按照指定参数列表依次进行解析。如果本次解析符合指定参数规则,包括参数名称、是否需要传值等规则,则返回成功,进行下一次循环继续解析,否则退出循环。
失败规则:
- 遇到未定义的变量
- 遇到了意外的值,如:在不需要传值的参数后面指定了参数,或者传入了比期待更多的值
失败后退出循环。
注意!!!不带名称的参数一定要写到最后!否则会被人为是不期待的参数,导致停止解析。
(3)在解析完所有预期的参数之后,对剩余参数进行处理
在解析完所有预期的参数(这时会退出循环)之后,变量 $OPTIND
存储着最后一个解析的参数的 Index,如还有其它参数,则认为是 getopts
期待之外的参数。这时可以配合 shift
清理掉已经解析过的参数,并通过 $1
、$2
的方式获取获取剩余参数。
shift $((OPTIND-1))
3、getopts
的局限
对于常用的不太复杂的场景,使用 getopts
处理参数基本够用,也更方便,而且是内部命令,不用考虑安装问题,但也有一些局限:
- 选项参数的格式必须是
-d val
而不能是中间没有空格的-dval
- 所有
选项参数
必须写在其它参数的前面,因为getopts
是从命令行前面开始处理,遇到非-
开头的参数,或者选项参数结束标记--
就中止了,如果中间遇到非选项命令行参数
,后面的选项参数就都取不到了。 - 不支持
--debug
这样的长选项
四、getopt
使用说明
1、安装
getopt
是 util-linux 包中的一个命令,Linux 中基本都已预安装了getopt
,样例脚本一般安装到如下位置:
/usr/share/doc/util-linux-2.23.2
/usr/share/getopt/
/usr/share/docs/
本样例参考了如下脚本:
/usr/share/doc/util-linux-2.23.2/getopt-parse.bash
macOS 自带的 getopt
功能比较弱,不支持长选项,可以安装 GNU 版本 gnu-getopt
:
$ brew install gnu-getopt
2、介绍
帮助信息如下:
$ getopt --help
用法:
getopt optstring parameters
getopt [options] [--] optstring parameters
getopt [options] -o|--options optstring [options] [--] parameters
选项:
-a, --alternative 允许长选项以 - 开始
-h, --help 这个简短的用法指南
-l, --longoptions <长选项> 要识别的长选项
-n, --name <程序名> 将错误报告给的程序名
-o, --options <选项字符串> 要识别的短选项
-q, --quiet 禁止 getopt(3) 的错误报告
-Q, --quiet-output 无正常输出
-s, --shell <shell> 设置 shell 引用规则
-T, --test 测试 getopt(1) 版本
-u, --unquoted 不引用输出
-V, --version 输出版本信息
3、举例
根据前面提到的案例,这里增加一个日志级别
选项,此选项有默认值,也可以自行指定参数值。
# 短参数格式
$ myscript -u username -p password -v -n 9999 192.168.1.2 -l3
# 或 长参数格式
$ myscript --username username --password password --verbose --port 9999 192.168.1.2 --log-level=3
参数说明:
参数 | 说明 |
---|---|
-u , --username
|
用户名 |
-p , --password
|
密码 |
-n , --port
|
端口 |
-v , --verbose
|
显示详情 |
-l , --log-level
|
日志级别,默认级别为 1 |
无名称参数 | 主机 |
#!/bin/bash
# 使用 `"$@"' 来让每个命令行参数扩展为一个单独的单词。 `$@' 周围的引号是必不可少的!
# 使用 getopt 整理参数
ARGS=$(getopt -o 'u:p:n:vl::' -l 'username:,password:,port:,verbose,log-level::' -- "$@")
if [ $? != 0 ] ; then echo "Parse error! Terminating..." >&2 ; exit 1 ; fi
# 将参数设置为 getopt 整理后的参数
# $ARGS 需要用引号包围
eval set -- "$ARGS"
# 循环解析参数
while true ; do
# 从第一个参数开始解析
case "$1" in
# 用户名,需要带参数值,所以通过 $2 取得参数值,获取后通过 shift 清理已获取的参数
-u|--username) CONN_USERNAME="$2" ; shift 2 ;;
# 密码,获取规则同上
-p|--password) CONN_PASSWORD="$2" ; shift 2 ;;
# 端口,获取规则同上
-n|--port) CONN_PORT="$2" ; shift 2 ;;
# 是否显示详情,开关型参数,带上该选项则执行此分支
-v|--verbose) CONN_SHOW_DETAIL=true ; shift ;;
# 日志级别,默认值参数
# 短格式:-l3
# 长格式:--log-level=3
-l|--log-level)
# 如指定了参数项,未指定参数值,则默认得到空字符串,可以根据此规则使用默认值
# 如果指定了参数值,则使用参数值
case "$2" in
"") CONN_LOG_LEVEL=1 ; shift 2 ;;
*) CONN_LOG_LEVEL="$2" ; shift 2 ;;
esac ;;
--) shift ; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
# 通过第一个无名称参数获取 主机
CONN_HOST="$1"
# 显示获取参数结果
echo '用户名: ' "$CONN_USERNAME"
echo '密码: ' "$CONN_PASSWORD"
echo '主机: ' "$CONN_HOST"
echo '端口: ' "$CONN_PORT"
echo '显示详情: ' "$CONN_SHOW_DETAIL"
echo '日志级别: ' "$CONN_LOG_LEVEL"
4、说明
其实 getopt
只负责做参数的重新整理,并不管提取参数值。它会根据指定的参数列表,把命令行中的选项参数集中放到前面,仅此而已。这样处理之后,再自己通过代码进行解析就比较简单了。所以上面的代码样例,真正涉及 getopt 使用的只有一行,其余的代码都是配合 getopt 重新排列的参数,自行进一步解析而已。
在本例中,选项参数
与非选项参数
没有按顺序排列,所以先告诉 getopt
命令要解析哪些参数:
getopt -o 'u:p:n:vl::' -l 'username:,password:,port:,verbose,log-level::' -- "$@"
参数规则:
-
-o
参数指定端参数格式,-l
参数指定对应的长参数 - 冒号
:
规则与getopts
的规则基本一致。区别在于后面带有两个冒号::
的表示默认值参数 - 对于
默认值选项
,短参数形式参数名与值之间不能有空格,长参数形式参数名与值需要用等号.=
连接
五、参考资料
(完)