模块 :S08 函数与脚本结构
篇号 :S08-04 / 42
预计阅读 :50 分钟
主线:Bash
文章目录
-
- 本篇目标
- [30 秒速览](#30 秒速览)
- 正文
-
- [1. 为何需要 `getopts`](#1. 为何需要
getopts) - [2. `getopts` 基本结构](#2.
getopts基本结构) - [3. optstring 规则](#3. optstring 规则)
- [4. `OPTARG` 与 `OPTIND`](#4.
OPTARG与OPTIND) - [5. 错误分支:`\?` 与 `:`](#5. 错误分支:
\?与:) - [6. 完整入口模板](#6. 完整入口模板)
- [7. `getopts` 的局限与长选项](#7.
getopts的局限与长选项) -
- [7.1 先用 `case` 处理 `--`,再 `getopts`](#7.1 先用
case处理--,再getopts) - [7.2 只用短选项](#7.2 只用短选项)
- [7.1 先用 `case` 处理 `--`,再 `getopts`](#7.1 先用
- [8. 与 `case` + `shift` 对照](#8. 与
case+shift对照) - [9. `select`:交互菜单](#9.
select:交互菜单) - [10. `select` 读法要点](#10.
select读法要点) - [11. `select` + `PS3` + 无限菜单](#11.
select+PS3+ 无限菜单) - [12. 读脚本检查清单](#12. 读脚本检查清单)
- [1. 为何需要 `getopts`](#1. 为何需要
- 练习
- [S08 模块小结](#S08 模块小结)
- 下一篇预告
本篇目标
掌握 getopts 解析短选项(-h -o file ),以及 Bash select 做交互菜单。能写出带 usage 、OPTARG/OPTIND 的入口脚本,并在读代码时区分 getopts 与 case+shift 两种风格。本篇为 S08 模块收尾。
30 秒速览
getopts:POSIX 内置,适合单字符 选项-h-v-o;带参选项在 optstring 里写o:。- 循环后执行
shift $((OPTIND - 1)),剩下"$@"为位置参数。 OPTARG:当前选项的参数;OPTIND:下一个待处理参数下标。--、--long长选项getopts不直接支持 ,常另用case预扫 或只用短选项。select:从列表生成编号菜单,用户输入序号;PS3改提示符。- 非交互脚本用
getopts;运维小工具、本地菜单可用select。
正文
1. 为何需要 getopts
S03-03、S06-02 用 while + case + shift 解析选项,短脚本足够。选项一多,getopts 更规整:
- 自动处理
-abc粘在一起(等价-a -b -c) - 统一
OPTARG取参数 - 错误时退出码、
?分支一致
bash
# 手写要处理 -vh、-hv、-v -h 等多种写法
# getopts 一轮循环即可
2. getopts 基本结构
bash
verbose=0
outfile=
while getopts ":hvo:" opt; do
case "$opt" in
h)
usage
exit 0
;;
v)
verbose=1
;;
o)
outfile=$OPTARG
;;
\?)
echo "invalid option: -$OPTARG" >&2
usage
exit 2
;;
:)
echo "option -$OPTARG requires an argument" >&2
usage
exit 2
;;
esac
done
shift $((OPTIND - 1))
# 此处 "$@" 为剩余位置参数
| 部分 | 含义 |
|---|---|
getopts ":hvo:" opt |
合法选项为 h、v、o(o 需要参数) |
case "$opt" |
分支处理 |
shift $((OPTIND - 1)) |
丢掉已消费的选项,保留位置参数 |
3. optstring 规则
bash
while getopts "hf:o:" opt; do
| 写法 | 含义 |
|---|---|
h |
开关,无参数 |
o: |
选项 -o 必须跟一个 参数(存入 OPTARG) |
前导 : |
静默模式:非法选项、缺参时 getopts 不打印 默认错误,由你在 ? 、 : 分支处理 |
推荐 在 optstring 最前加 : (上例 :hvo:),错误信息自己控制。
示例:
bash
./tool.sh -v -o result.txt input1 input2
# 循环结束后 OPTIND 指向 input1,shift 后 $@ = input1 input2
粘写:
bash
./tool.sh -vh -o out.txt file
# 等价 -v -h -o out.txt
4. OPTARG 与 OPTIND
| 变量 | 含义 |
|---|---|
OPTARG |
当前选项的参数(如 -o FILE 里的 FILE);非法选项时可能是非法字符 |
OPTIND |
下一个要处理的 $n 下标;初始为 1 |
bash
echo "remaining: $*"
echo "OPTIND=$OPTIND"
shift $((OPTIND - 1)) 是固定套路:把 $1...$(OPTIND-1) 选项吃掉,$1 变成第一个非选项参数。
5. 错误分支:\? 与 :
case 分支 |
触发 |
|---|---|
\? |
遇到了 optstring 里没有的选项 |
: |
需要参数的选项后面没跟参数 (optstring 前导 : 时) |
bash
while getopts ":f:" opt; do
case "$opt" in
f) file=$OPTARG ;;
\?) echo "bad opt: -$OPTARG" >&2; exit 2 ;;
:) echo "-$OPTARG needs arg" >&2; exit 2 ;;
esac
done
6. 完整入口模板
bash
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "usage: $0 [-hv] [-o outfile] <inputs...>" >&2
}
verbose=0
outfile=
while getopts ":hvo:" opt; do
case "$opt" in
h) usage; exit 0 ;;
v) verbose=1 ;;
o) outfile=$OPTARG ;;
\?) usage; exit 2 ;;
:) usage; exit 2 ;;
esac
done
shift $((OPTIND - 1))
(( $# >= 1 )) || { usage; exit 2; }
(( verbose )) && set -x
# 使用 outfile 与 "$@"
与 S01-04 骨架、S08-01 main 可合并:选项解析在 main 开头。
7. getopts 的局限与长选项
getopts 只处理单字符 -x ,不识别 -output 为 -o + utput。
长选项常见做法:
7.1 先用 case 处理 --,再 getopts
bash
while (( $# > 0 )); do
case "$1" in
--help) usage; exit 0 ;;
--verbose) verbose=1; shift ;;
--) shift; break ;;
-*) break ;; # 交给 getopts
*) break ;;
esac
done
while getopts ":ho:" opt; do
...
done
shift $((OPTIND - 1))
7.2 只用短选项
许多内部脚本约定 -h -v -o,文档写清即可。
读脚本:见到 --foo ,先看是 case 手写 还是外部 getopt/getopt_long 命令(本专栏以内置 getopts 为主)。
8. 与 case + shift 对照
getopts |
case + shift |
|
|---|---|---|
短选项组合 -vh |
自动拆分 | 需自己写或多次 shift |
长选项 --help |
不支持 | 容易写 |
| 代码量 | 选项多时更短 | 选项少时直观 |
| 可移植 | POSIX Bash/sh | POSIX |
S06-03 里 while case 选项循环 与本篇 getopts 常出现在同一项目的不同脚本中。
9. select:交互菜单
bash
PS3="Choose action: "
select action in start stop status quit; do
case "$action" in
start) start_svc; break ;;
stop) stop_svc; break ;;
status) show_status ;;
quit) break ;;
"")
echo "invalid" ;;
*)
echo "unknown: $REPLY" ;;
esac
done
| 部分 | 含义 |
|---|---|
select var in words... |
把列表编成 1、2、3... 菜单 |
PS3 |
提示符(默认 #?) |
$REPLY |
用户输入的编号或文本 |
$action |
选中项的单词;无效选择时往往为空 |
运行效果示意:
text
1) start
2) stop
3) status
4) quit
Choose action: 2
用户输入 2 → action=stop。
10. select 读法要点
bash
select f in *.sh; do
[[ -n "$f" ]] || { echo "bad choice"; continue; }
shellcheck "$f"
break
done
| 点 | 说明 |
|---|---|
| 用户输入数字 | 选对应项;越界时 $f 为空 |
| 用户输入非数字文本 | 可能匹配列表中的词(少见用法) |
break |
退出 select 循环(S07-03) |
| 非交互 | 无 stdin 时 select 不合适;用参数或配置文件 |
select 是 Bash 特性 ,#!/bin/sh 脚本里不应出现。
11. select + PS3 + 无限菜单
bash
PS3="> "
while true; do
select cmd in build test clean exit; do
case "$cmd" in
build|test|clean) "$cmd"; break ;;
exit) return 0 2>/dev/null || exit 0 ;;
*) echo "?" ;;
esac
done
done
外层 while true 执行完一次操作后再显示菜单(运维小工具常见)。
12. 读脚本检查清单
- 选项解析后是
shift $((OPTIND-1))了吗? - 带参选项在 optstring 里是
o:吗? -
--long是getopts还是case处理的? -
usage是否与真实选项一致? -
select是否处理了空选择 、break退出?
练习
判断题
getopts可以直接解析--output=file这种长选项。-o需要参数时,optstring 应包含o:。getopts循环结束后应shift $((OPTIND-1))再处理位置参数。select中用户选无效编号时,循环变量常为空串。PS3用来设置select的提示字符串。
参考答案
- 错(内置 getopts 只管单字符
-x;长选项另处理)。 - 对。
- 对。
- 对。
- 对。
实操题 1:getopts
为脚本补全选项:-h 帮助,-n count 重复次数(默认 1),剩余参数为要 echo 的字符串。
./say.sh -n 3 hello → 打印三行 hello。
参考答案
bash
#!/usr/bin/env bash
set -euo pipefail
usage() { echo "usage: $0 [-n count] message" >&2; }
count=1
while getopts ":hn:" opt; do
case "$opt" in
h) usage; exit 0 ;;
n) count=$OPTARG ;;
\?) usage; exit 2 ;;
:) usage; exit 2 ;;
esac
done
shift $((OPTIND - 1))
(( $# >= 1 )) || { usage; exit 2; }
msg=$1
for (( i = 0; i < count; i++ )); do
echo "$msg"
done
实操题 2:select
写菜单:项为 list、show、quit;选 list 打印 listing,选 show 打印 status,选 quit 退出;无效输入提示 ? 并重新显示菜单。
参考答案
bash
PS3="menu> "
select act in list show quit; do
case "$act" in
list) echo listing; break ;;
show) echo status; break ;;
quit) break 2 ;;
*) echo "?" ;;
esac
done
(单次执行后 break;若要反复菜单可外包 while true。)
改错题
bash
while getopts "hv:o" opt; do
case $opt in
o) out=$OPTARG ;;
v) verbose=1 ;;
esac
done
shift $OPTIND
file=$1
参考
o应写作o:(getopts "hv:o:")。shift $OPTIND→shift $((OPTIND - 1))。- 建议 optstring 前导
:,并处理\?、:。 case $opt→case "$opt"。
读脚本题
下面片段执行 ./tool.sh -o a.txt -v b.txt 后,outfile 和第一个位置参数分别是什么?
bash
while getopts ":o:v" opt; do
case "$opt" in
o) outfile=$OPTARG ;;
v) verbose=1 ;;
esac
done
shift $((OPTIND - 1))
参考答案
outfile=a.txt,verbose=1,$1 为 b.txt (-v 后的 b.txt 被当作位置参数,不是 -v 的参数)。
S08 模块小结
| 篇号 | 主题 | 读脚本时 |
|---|---|---|
| S08-01 | 函数 | main、usage、return/exit |
| S08-02 | local |
函数内变量是否污染全局 |
| S08-03 | source |
库 vs 入口、BASH_SOURCE 路径 |
| S08-04 | getopts/select |
选项怎么解析、有无交互菜单 |
下一模块 S09 进入进程、管道与重定向。
下一篇预告
S09-01 :《重定向基础:> >> 2> &> 与 here-doc》--- 标准输出/错误、追加写文件与 here 文档。