第 9 章 · 07 健壮性第三件:getopts 与参数解析


文档摘要

第 9 章 · 07 健壮性第三件:getopts 与参数解析 本节摘要:一个能上线的脚本,必须能让用户(或一周后的你自己)知道「它怎么用」。本节讲健壮性三件套的最后一件—— ,Bash 内建的命令行参数解析器。你会学到怎么用它解析短选项( 、 、 ),怎么处理「带值选项」和「开关选项」,怎么在解析失败时打印友好的用法提示。 的局限也要讲清:它只支持短选项( ),不支持长选项( ),长选项要自己手写或上第三方工具。最后,你将把 和上一节的 函数组合起来,给脚本配上规范的 帮助、必选参数检查、缺参报错——一个真正「能被别人正确使用」的命令行工具就成型了。第 08 节的综合备份脚本就会用上这一整套。 内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。

第 9 章 · 07 健壮性第三件:getopts 与参数解析

本节摘要:一个能上线的脚本,必须能让用户(或一周后的你自己)知道「它怎么用」。本节讲健壮性三件套的最后一件——getopts,Bash 内建的命令行参数解析器。你会学到怎么用它解析短选项(-a-o value-v),怎么处理「带值选项」和「开关选项」,怎么在解析失败时打印友好的用法提示。getopts 的局限也要讲清:它只支持短选项(-h),不支持长选项(--help),长选项要自己手写或上第三方工具。最后,你将把 getopts 和上一节的 usage() 函数组合起来,给脚本配上规范的 -h 帮助、必选参数检查、缺参报错——一个真正「能被别人正确使用」的命令行工具就成型了。第 08 节的综合备份脚本就会用上这一整套。

内容来源:综合 Shell 脚本知识与原项目「Linux 命令大全」整理,套用体系化模板。

学习目标

阅读完本节,你应当能够:

  1. 解释「位置参数」($1/$2)和「选项」(-a/-o value)的区别,说清为什么脚本需要 getopts
  2. 写出 getopts 的标准骨架:while getopts "abo:" opt; do ... done,说清选项串里每个字符和冒号的含义。
  3. 区分「开关选项」(不带值)和「带值选项」(带值,用冒号标记),从 $OPTARG 取值。
  4. 处理 getopts 解析失败的情况,配合 usage() 给出友好提示。
  5. shift $((OPTIND - 1)) 把剩余的位置参数(非选项)取出来。
  6. 说清 getopts 的局限(不支持长选项 --help),知道何时该用手写解析或第三方工具。

一、设计动机(为什么这样设计)

位置参数 vs 选项:脚本要像真正的命令

你已经会写接受位置参数的脚本:

#!/usr/bin/env bash src=$1 dst=$2 cp -r "$src" "$dst"

调用:./copy.sh /home/alice /backup。这种「位置参数」方式简单,但不够灵活——用户必须严格记住「第一个是源、第二个是目标」,没有默认值,不能换顺序,也没法表达「可选」。

真正的命令行工具(lsgrepcp)都用「选项」+「位置参数」混合的方式:

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
  • 不跟冒号的选项是「开关」(出现就是 true,不出现是 false)。
  • getopts 每次解析一个选项,把字母放进 $opt,循环直到解析完。

关键概念getopts 是 Bash 内建(不是外部命令),所以它能在所有 Bash 环境跑,不依赖系统工具。它的局限性也由此而来——它只支持「短选项」(单字母),不支持 GNU 风格的「长选项」--help

二、高频组合与实战

1. 标准骨架

#!/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 在遇到未知选项(\?)或缺失值(:)时不再自己打印错误,而是交给你处理。生产脚本推荐加这个前导冒号

2. 选项字符串的语法

选项字符串 含义
"abc" 三个开关选项 -a -b -c,都不带值
"ab:c" -b 带值(-b value),-a -c 不带
":abc" 同上,但开启安静模式(错误交给脚本处理)
":ab:c" -b 带值,安静模式
":ab:c:" -b-c 都带值

3. 处理未知选项和缺失值

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 在错误情况下也很有用:\? 时它存未知选项字母,: 时它存缺值的选项字母。

4. 取剩余位置参数: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

5. 处理「可选带值」选项

getopts 默认不支持「可选值」(GNU getopt-o[value] 语法)。要实现「不带值用默认、带值用指定」,只能用两段:-o 开关 + 单独的 -O value这是 getopts 的已知局限

6. 长选项:手写解析(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),有两个选择:

  1. 手写上面的解析(适合简单场景)。
  2. 用外部命令 getopt(注意是 getopt 不是 getopts,前者是 GNU 工具、支持长选项,但不是所有系统都有)。

💡 技巧:日常脚本大多数情况,短选项 + 一段手写的 --help/--version 特例就够用了。getopts 处理短选项,再单独 case 处理 --help。例如 [[ $1 == "--help" ]] && { usage; exit 0; } 放在 getopts 循环前。

三、踩坑与排错

坑 1:忘了 shift $((OPTIND - 1)),位置参数拿不到

while getopts "vo:" opt; do ... done # 忘了 shift src=$1 # 这里 $1 是 "-v",不是源文件!

getopts 解析后,$1 还停在原来的位置(选项还在前面)。必须 shift $((OPTIND - 1)) 把选项移走,$1 才是真正的第一个位置参数。

坑 2:选项字符串忘了前导 :,错误信息不受控

while getopts "vo:" opt; do # 没前导冒号 ... done # 用户输入 -x 时,getopts 会自己打印 "illegal option -- x" # 你想自定义错误信息就办不到

加前导冒号 ":vo:"getopts 静默,把控制权交给你。

坑 3:带值选项的值没传

./script.sh -o # -o 后面没值 # getopts 在 ":vo:" 模式下会触发 : 分支,$OPTARG 是 "o"

记得在 : 分支里报错并退出,别让脚本继续带着空的 $output 跑下去。

坑 4:getopts 只能解析一次,循环里再用要重置 OPTIND

parse_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 }

坑 5:把 getoptsgetopt

两者只差一个字母,完全不同:

  • getopts:Bash 内建,支持短选项,跨平台,推荐
  • getopt:外部命令(GNU/BSD 实现不同),支持长选项,但行为依赖系统,坑多,慎用

记住:getoptss(内建),getopt 不带 s(外部)。

本节要点回顾

  1. 位置参数($1/$2)简单但不灵活,真正的命令行工具用「选项 + 位置参数」混合方式;getopts 是 Bash 内建的选项解析器。
  2. 标准骨架while getopts "字符串" opt; do case $opt in ... esac; done,选项字母后跟 : 表示带值(值在 $OPTARG)。
  3. 选项字符串前导 : 开启「安静模式」,让错误交给脚本处理:\? 是未知选项、: 是缺失值,两种都从 $OPTARG 拿到出错的字母。
  4. 解析完必须 shift $((OPTIND - 1)),把已处理的选项移走,剩下的 $@ 才是位置参数。
  5. getopts 的局限:只支持短选项-h),不支持长选项(--help)。长选项要手写解析或用外部 getopt(坑多慎用)。
  6. 在函数里用 getoptslocal OPTIND=1 重置,否则第二次解析失效。
  7. getopts(带 s,内建)≠ getopt(不带 s,外部命令),行为完全不同。

健壮性三件套到此集齐:set -euo pipefail(遇错即停)、trap(退出清理)、getopts(参数解析)。下一节是全章综合应用题——写一个完整的、能上线的备份脚本,把变量、条件、循环、函数、健壮性三件套全部串起来。你会看到一个真正「生产级」的 Shell 脚本长什么样,并理解为什么前面七节学的每一块都不可或缺。


发布者: 作者: 灏天文库 转发
评论区 (0)
U