第 9 章 · 07 健壮性第三件:getopts 与参数解析 本节摘要:一个能上线的脚本,必须能让用户(或一周后的你自己)知道「它怎么用」。本节讲健壮性三件套的最后一件—— ,Bash 内建的命令行参数解析器。你会学到怎么用它解析短选项( 、 、 ),怎么处理「带值选项」和「开关选项」,怎么在解析失败时打印友好的用法提示。 的局限也要讲清:它只支持短选项( ),不支持长选项( ),长选项要自己手写或上第三方工具。最后,你将把 和上一节的 函数组合起来,给脚本配上规范的 帮助、必选参数检查、缺参报错——一个真正「能被别人正确使用」的命令行工具就成型了。第 08 节的综合备份脚本就会用上这一整套。 内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。
本节摘要:一个能上线的脚本,必须能让用户(或一周后的你自己)知道「它怎么用」。本节讲健壮性三件套的最后一件——
getopts,Bash 内建的命令行参数解析器。你会学到怎么用它解析短选项(-a、-o value、-v),怎么处理「带值选项」和「开关选项」,怎么在解析失败时打印友好的用法提示。getopts的局限也要讲清:它只支持短选项(-h),不支持长选项(--help),长选项要自己手写或上第三方工具。最后,你将把getopts和上一节的usage()函数组合起来,给脚本配上规范的-h帮助、必选参数检查、缺参报错——一个真正「能被别人正确使用」的命令行工具就成型了。第 08 节的综合备份脚本就会用上这一整套。
内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。
阅读完本节,你应当能够:
$1/$2)和「选项」(-a/-o value)的区别,说清为什么脚本需要 getopts。getopts 的标准骨架:while getopts "abo:" opt; do ... done,说清选项串里每个字符和冒号的含义。$OPTARG 取值。getopts 解析失败的情况,配合 usage() 给出友好提示。shift $((OPTIND - 1)) 把剩余的位置参数(非选项)取出来。getopts 的局限(不支持长选项 --help),知道何时该用手写解析或第三方工具。你已经会写接受位置参数的脚本:
#!/usr/bin/env bash src=$1 dst=$2 cp -r "$src" "$dst"
调用:./copy.sh /home/alice /backup。这种「位置参数」方式简单,但不够灵活——用户必须严格记住「第一个是源、第二个是目标」,没有默认值,不能换顺序,也没法表达「可选」。
真正的命令行工具(ls、grep、cp)都用「选项」+「位置参数」混合的方式:
ls -l -a /tmp # -l -a 是选项,/tmp 是位置参数 cp -r --backup src dst # -r、--backup 是选项
选项有名字(-l 表示「长格式」)、可带值(-o file 表示「输出到 file」)、可省略(有默认值)、顺序自由。这些特性让命令「好用」。脚本也该这样。
你可以自己解析 $1、$2:
while [[ $# -gt 0 ]]; do case $1 in -o) shift; output=$1;; -v) verbose=1;; *) src=$1;; esac shift done
但手写解析有几个坑:选项合并(-vo file 等价 -v -o file)要单独处理、未知选项要报错、选项缺失值要报错、-- 终止选项解析要支持……自己写容易漏。getopts 是 Bash 内建,专门解决这些问题。
getopts:Bash 内建的解析器while getopts "abo:" opt; do case $opt in a) flag_a=1;; b) flag_b=1;; o) output=$OPTARG;; \?) echo "未知选项"; exit 1;; esac done
getopts 的核心是选项字符串("abo:"):
-a、-b、-o)。: 表示「这个选项需要带值」(-o value),值会存到 $OPTARG。getopts 每次解析一个选项,把字母放进 $opt,循环直到解析完。关键概念:
getopts是 Bash 内建(不是外部命令),所以它能在所有 Bash 环境跑,不依赖系统工具。它的局限性也由此而来——它只支持「短选项」(单字母),不支持 GNU 风格的「长选项」--help。
#!/usr/bin/env bash set -euo pipefail verbose=0 output="" usage() { cat <<EOF 用法: $0 [-v] [-o 输出文件] 源文件 -v 详细输出(可省,默认安静) -o FILE 指定输出文件(默认 stdout) 源文件 必填 示例: $0 -v -o result.txt data.in EOF } while getopts ":vo:" opt; do case $opt in v) verbose=1;; o) output=$OPTARG;; \?) echo "未知选项: -$OPTARG" >&2; usage; exit 1;; :) echo "选项 -$OPTARG 缺少值" >&2; usage; exit 1;; esac done shift $((OPTIND - 1)) # 关键!把已解析的选项移走,剩下位置参数 src=${1:-} [[ -z $src ]] && { echo "缺少源文件" >&2; usage; exit 1; } echo "verbose=$verbose output=$output src=$src"
注意选项字符串开头的 :——它开启「安静错误模式」,让 getopts 在遇到未知选项(\?)或缺失值(:)时不再自己打印错误,而是交给你处理。生产脚本推荐加这个前导冒号。
| 选项字符串 | 含义 |
|---|---|
"abc" |
三个开关选项 -a -b -c,都不带值 |
"ab:c" |
-b 带值(-b value),-a -c 不带 |
":abc" |
同上,但开启安静模式(错误交给脚本处理) |
":ab:c" |
-b 带值,安静模式 |
":ab:c:" |
-b 和 -c 都带值 |
while getopts ":vo:" opt; do case $opt in v) verbose=1;; o) output=$OPTARG;; \?) echo "未知选项: -$OPTARG" >&2; usage; exit 1;; # 输入了 -x :) echo "选项 -$OPTARG 缺少参数" >&2; usage; exit 1;; # 输入了 -o 但没值 esac done
$OPTARG 在错误情况下也很有用:\? 时它存未知选项字母,: 时它存缺值的选项字母。
shift $((OPTIND - 1))getopts 用一个特殊变量 $OPTIND 记录「下一个要处理的参数下标」。解析完所有选项后,用 shift $((OPTIND - 1)) 把所有已处理的选项移走,剩下的 $1/$2/$@ 就是「位置参数」(非选项)。
while getopts "vo:" opt; do ... done shift $((OPTIND - 1)) echo "剩余参数: $@" # 这里 $@ 是所有非选项参数
例如 ./script.sh -v -o out.txt file1 file2,解析后 $@ 是 file1 file2。
getopts 默认不支持「可选值」(GNU getopt 的 -o[value] 语法)。要实现「不带值用默认、带值用指定」,只能用两段:-o 开关 + 单独的 -O value。这是 getopts 的已知局限。
while [[ $# -gt 0 ]]; do case $1 in --help|-h) usage; exit 0;; --version|-V) echo "v1.0"; exit 0;; --output|-o) shift; output=$1;; --verbose|-v) verbose=1;; --) shift; break;; # -- 之后都是位置参数 -*) echo "未知选项: $1" >&2; exit 1;; *) break;; # 第一个非选项参数开始 esac shift done echo "剩余: $@"
如果你确实需要长选项(--help、--output=FILE),有两个选择:
getopt(注意是 getopt 不是 getopts,前者是 GNU 工具、支持长选项,但不是所有系统都有)。💡 技巧:日常脚本大多数情况,短选项 + 一段手写的
--help/--version特例就够用了。getopts处理短选项,再单独case处理--help。例如[[ $1 == "--help" ]] && { usage; exit 0; }放在getopts循环前。
shift $((OPTIND - 1)),位置参数拿不到while getopts "vo:" opt; do ... done # 忘了 shift src=$1 # 这里 $1 是 "-v",不是源文件!
getopts 解析后,$1 还停在原来的位置(选项还在前面)。必须 shift $((OPTIND - 1)) 把选项移走,$1 才是真正的第一个位置参数。
:,错误信息不受控while getopts "vo:" opt; do # 没前导冒号 ... done # 用户输入 -x 时,getopts 会自己打印 "illegal option -- x" # 你想自定义错误信息就办不到
加前导冒号 ":vo:" 让 getopts 静默,把控制权交给你。
./script.sh -o # -o 后面没值 # getopts 在 ":vo:" 模式下会触发 : 分支,$OPTARG 是 "o"
记得在 : 分支里报错并退出,别让脚本继续带着空的 $output 跑下去。
getopts 只能解析一次,循环里再用要重置 OPTINDparse_args() { while getopts "vo:" opt; do ... done } parse_args -v # 第一次 parse_args -o out # 第二次,OPTIND 还停在末尾,解析失败
在函数开头加 OPTIND=1 重置(或在函数开头 local OPTIND):
parse_args() { local OPTIND=1 # 局部 OPTIND,每次调用都从 1 开始 while getopts "vo:" opt; do ... done }
getopts 当 getopt两者只差一个字母,完全不同:
getopts:Bash 内建,支持短选项,跨平台,推荐。getopt:外部命令(GNU/BSD 实现不同),支持长选项,但行为依赖系统,坑多,慎用。记住:getopts 带 s(内建),getopt 不带 s(外部)。
$1/$2)简单但不灵活,真正的命令行工具用「选项 + 位置参数」混合方式;getopts 是 Bash 内建的选项解析器。while getopts "字符串" opt; do case $opt in ... esac; done,选项字母后跟 : 表示带值(值在 $OPTARG)。: 开启「安静模式」,让错误交给脚本处理:\? 是未知选项、: 是缺失值,两种都从 $OPTARG 拿到出错的字母。shift $((OPTIND - 1)),把已处理的选项移走,剩下的 $@ 才是位置参数。getopts 的局限:只支持短选项(-h),不支持长选项(--help)。长选项要手写解析或用外部 getopt(坑多慎用)。getopts 要 local OPTIND=1 重置,否则第二次解析失效。getopts(带 s,内建)≠ getopt(不带 s,外部命令),行为完全不同。健壮性三件套到此集齐:set -euo pipefail(遇错即停)、trap(退出清理)、getopts(参数解析)。下一节是全章综合应用题——写一个完整的、能上线的备份脚本,把变量、条件、循环、函数、健壮性三件套全部串起来。你会看到一个真正「生产级」的 Shell 脚本长什么样,并理解为什么前面七节学的每一块都不可或缺。