Shell脚本精读 · S08-04 | 命令行与交互输入:`getopts` 与 `select` 菜单

模块 :S08 函数与脚本结构

篇号 :S08-04 / 42

预计阅读 :50 分钟

主线:Bash


文章目录

    • 本篇目标
    • [30 秒速览](#30 秒速览)
    • 正文
      • [1. 为何需要 `getopts`](#1. 为何需要 getopts)
      • [2. `getopts` 基本结构](#2. getopts 基本结构)
      • [3. optstring 规则](#3. optstring 规则)
      • [4. `OPTARG` 与 `OPTIND`](#4. OPTARGOPTIND)
      • [5. 错误分支:`\?` 与 `:`](#5. 错误分支:\?:)
      • [6. 完整入口模板](#6. 完整入口模板)
      • [7. `getopts` 的局限与长选项](#7. getopts 的局限与长选项)
        • [7.1 先用 `case` 处理 `--`,再 `getopts`](#7.1 先用 case 处理 --,再 getopts)
        • [7.2 只用短选项](#7.2 只用短选项)
      • [8. 与 `case` + `shift` 对照](#8. 与 case + shift 对照)
      • [9. `select`:交互菜单](#9. select:交互菜单)
      • [10. `select` 读法要点](#10. select 读法要点)
      • [11. `select` + `PS3` + 无限菜单](#11. select + PS3 + 无限菜单)
      • [12. 读脚本检查清单](#12. 读脚本检查清单)
    • 练习
    • [S08 模块小结](#S08 模块小结)
    • 下一篇预告

本篇目标

掌握 getopts 解析短选项(-h -o file ),以及 Bash select 做交互菜单。能写出带 usageOPTARG/OPTIND 的入口脚本,并在读代码时区分 getoptscase+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 合法选项为 hvoo 需要参数)
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. OPTARGOPTIND

变量 含义
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

用户输入 2action=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: 吗?
  • --longgetopts 还是 case 处理的?
  • usage 是否与真实选项一致?
  • select 是否处理了空选择break 退出

练习

判断题

  1. getopts 可以直接解析 --output=file 这种长选项。
  2. -o 需要参数时,optstring 应包含 o:
  3. getopts 循环结束后应 shift $((OPTIND-1)) 再处理位置参数。
  4. select 中用户选无效编号时,循环变量常为空串。
  5. PS3 用来设置 select 的提示字符串。

参考答案

  1. 错(内置 getopts 只管单字符 -x;长选项另处理)。
  2. 对。
  3. 对。
  4. 对。
  5. 对。

实操题 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

写菜单:项为 listshowquit;选 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

参考

  1. o 应写作 o:getopts "hv:o:")。
  2. shift $OPTINDshift $((OPTIND - 1))
  3. 建议 optstring 前导 : ,并处理 \?:
  4. case $optcase "$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.txtverbose=1$1b.txt-v 后的 b.txt 被当作位置参数,不是 -v 的参数)。


S08 模块小结

篇号 主题 读脚本时
S08-01 函数 mainusagereturn/exit
S08-02 local 函数内变量是否污染全局
S08-03 source 库 vs 入口、BASH_SOURCE 路径
S08-04 getopts/select 选项怎么解析、有无交互菜单

下一模块 S09 进入进程、管道与重定向


下一篇预告

S09-01 :《重定向基础:> >> 2> &> 与 here-doc》--- 标准输出/错误、追加写文件与 here 文档。