使用 getopts 和 getopt 命令处理命令行选项

一、为什么需要更加复杂的命令行选项处理方式

我们经常会遇到 Shell 脚本中需要获取命令行参数的场景,最简单的方式就是通过 $1$2 ……这样的方式获取。这种方式对于只需要一两个参数的场景简单实用,但对于需要多个参数、需要可选参数等复杂一些的场景会带来一些问题:

  • 必须严格遵守参数传入顺序,不容易记忆,容易出错
  • 无法提供“参数默认值”
  • 参数含义不够明确

举个例子进行说明,假设我们有一个连接主机的脚本,需要传入以下参数:

  • 用户名
  • 密码
  • 主机(必填项,并不是可选项,不需要指定参数名称)
  • 端口
  • 显示连接详情

如果通过 $1$2 的方式来获取参数,前面提出的几个问题就显得比较明显了。

我们更希望通过类似下方式来传参:

$ myscript -u username -p password -v -n 9999 192.168.1.2

这时 getopt / getopts 就就可以大展拳脚了。

二、getoptsgetopt 的区别和应用场景

getoptgetopts 都是 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、安装

getoptutil-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 的规则基本一致。区别在于后面带有两个冒号 :: 的表示默认值参数
  • 对于默认值选项,短参数形式参数名与值之间不能有空格,长参数形式参数名与值需要用等号.= 连接

五、参考资料

(完)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容