参数解析与错误处理

系统讲解 Shell 脚本的位置参数与 shift 移位、getopts 选项解析、IFS 字段分隔控制,以及退出码、错误捕获与输入校验的完整方案。

1. 位置参数与特殊参数

一句话总结: $1 到 $9 是位置参数,$0 是脚本名,$# 是数量,"$@" 保留每个参数原样,是参数处理的两根台柱。

位置参数是脚本接收输入的基本通道。函数内调用时,它们自动替换为函数的参数。

#!/usr/bin/env bash
# usage: ./deploy.sh <env> <app>
echo "脚本名: $0"
echo "环境:   $1"
echo "应用:   $2"
echo "参数个数: $#"

# 遍历所有参数,逐个打印(正确写法)
for arg in "$@"; do
  echo "arg=$arg"
done

1.1 参数相关内建与变量

变量/内建作用
$0脚本或函数名
$1~$9第 1~9 个参数
${10}第 10 个参数(两位数要花括号)
$#参数个数
$@ / $*全部参数
shift左移丢弃首个参数
set --重新设置位置参数
# 两位数参数必须花括号
echo "${10}"
# 空参数也要保留:用 "$@" 而不是 $*
print_args() { for a in "$@"; do printf '[%s] ' "$a"; done; echo; }
print_args "a b" c "" d     # [a b] [c] [] [d]

2. 参数展开与默认值

一句话总结: ${1:-默认} 为缺失参数兜底,${@:2} 取参数切片,配合 set -- 可以干净地重组参数集。

# 参数缺省:没有就给默认
env="${1:-prod}"
app="${2:-web}"

# 参数切片:从第 2 个参数开始取全部
rest=("${@:2}")
echo "剩余参数: ${rest[*]}"

# 参数必须存在:缺失直接报错退出
: "${1:?请指定环境,用法: $0 <env>}"

2.1 常用参数展开模式

写法含义例子
${1:-def}缺省用 def环境默认 prod
${1:=def}缺省并回写少用,改全局 $1
${1:?msg}缺失即报错必填参数
${@:2}从第 2 个起切片把前 1 个当选项
${!#}最后一个参数取末位
# 分离"选项参数"与"普通参数"
flag="${1}"
shift
rest=("$@")           # shift 之后 $@ 只剩普通参数

# 取最后一个参数
echo "${!#}"

3. getopts 选项解析

一句话总结: getopts 是内建选项解析器,按"选项字母:参数"语法循环解析 -a -b value 形式,支持组合与参数缺失报错。

getopts 适合解析 -a、-b value、-abc 这类 POSIX 风格选项。带参数的选项字母后要跟冒号。

#!/usr/bin/env bash
# usage: ./app.sh [-a] [-b value] [-h]
verbose=0
batch=""
while getopts "ab:h" opt; do
  case "$opt" in
    a) verbose=1 ;;
    b) batch="$OPTARG" ;;
    h) echo "用法: $0 [-a] [-b value] [-h]"; exit 0 ;;
    \?) echo "非法选项"; exit 2 ;;
  esac
done
shift $((OPTIND - 1))   # 跳过已解析的选项,$@ 剩普通参数

echo "verbose=$verbose batch=$batch"
echo "剩余参数: $@"

3.1 getopts 关键点

要点说明
optstring字母表,带 : 表示该选项需参数
$OPTARG当前选项的参数值
$OPTIND下一个要解析的参数下标
\?处理非法选项的分支
组合-ab value 等价 -a -b value
终止遇到 -- 停止解析
# 选项参数缺省:用 ${OPTARG:-默认}
while getopts "p:" opt; do
  case "$opt" in
    p) port="${OPTARG:-8080}" ;;
  esac
done

一句话总结: getopts 最大的好处是免费拿到非法选项检测,配合 shift $((OPTIND-1)) 把选项与普通参数干净分离。

4. shift 的用法与模式

一句话总结: shift 把位置参数整体左移丢弃 $1,shift N 一次丢 N 个,是"消费式"解析参数的核心工具。

# shift 基础
set -- a b c
shift
echo "$1"            # b
shift 2
echo "$1"            # 空(已全部消费完)

# 用 shift 手动解析"子命令"
command="$1"
shift
case "$command" in
  start) start_service "$@" ;;   # 剩余参数交给子命令
  stop)  stop_service "$@" ;;
  *)     echo "未知命令: $command"; exit 1 ;;
esac

4.1 shift 配合模式

模式写法
取首参数后丢掉arg="$1"; shift
取选项参数value="$2"; shift 2
消费完所有参数while (( $# )); do ...; shift; done
跳过选项区shift $((OPTIND - 1))
# 边消费边处理
while (( $# > 0 )); do
  echo "处理: $1"
  shift
done

一句话总结: shift 与 getopts 是参数解析的两套思路:getopts 管 -x 选项,shift 管位置参数消费。两者组合可覆盖九成 CLI 形态。

5. IFS 与控制分隔

一句话总结: IFS 是字段分隔符,改 IFS 能自定义"分词与拼接"的规则,read -r 配合 IFS 能精确解析 CSV 之类结构。

IFS 默认是空格/制表/换行,参与词分裂、read 拆分、$* 拼接三处行为。

# read 按逗号拆分 CSV
IFS=, read -r name age city <<< "张三,25,杭州"
echo "name=$name age=$age city=$city"

# 自定义拼接:把数组拼成逗号串
arr=(a b c)
joined=$(IFS=,; echo "${arr[*]}")
echo "$joined"          # a,b,c

# 解析 /etc/passwd 冒号字段
while IFS=: read -r user pass uid gid rest; do
  echo "$user -> uid=$uid"
done < /etc/passwd

5.1 IFS 操作速查

操作写法结果
按逗号拆行IFS=, read -r a b拆分到变量
数组拼成串IFS=,; echo "${arr[*]}"a,b,c
保留整行IFS= read -r line不拆不删空格
去前导空格IFS=' ' read -r x可控
# 保留行首空格的读取
printf '  hello  \n' | { IFS= read -r line; echo "[$line]"; }

一句话总结: IFS 配合 read 是把半结构化文本转成变量的利器,但改 IFS 是"全局副作用",最好在子 Shell (IFS=,; ...) 或单条命令里用。

6. 输入校验

一句话总结: 位置参数默认全是字符串,使用前必须校验"是不是数字"“是不是合法选项”,正则与判空是最轻量的把关。

# 判空
[[ -z "$1" ]] && { echo "缺少参数"; exit 1; }

# 数字校验
[[ "$1" =~ ^[0-9]+$ ]] || { echo "必须是数字: $1"; exit 1; }

# 枚举校验
case "$1" in dev|test|prod) ;; *) echo "非法环境"; exit 1;; esac

# 文件路径校验
[[ -f "$2" ]] || { echo "文件不存在: $2"; exit 1; }

6.1 校验函数模板

require_env() {
  local env="$1"
  case "$env" in
    dev|test|prod) return 0 ;;
    *) echo "非法环境: $env(可选 dev/test/prod)" >&2; return 1 ;;
  esac
}

require_env "${1:-}" || exit 1

# 校验后设置强类型变量(全大写全局)
readonly ENV="${1:?}"
readonly APP="${2:-web}"

一句话总结: 参数校验的原则是早失败:在脚本开头把非法输入挡在门外,错误信息写明"错在哪、该给什么",比运行到一半爆炸友好得多。

7. 错误处理与退出码

一句话总结: 退出码 0 成功、1 通用错误、2 用法错误、130 被 Ctrl+C 中断;封装 error 函数与 set -e,错误路径也要可观测。

# 错误处理函数
error() {
  echo "[ERROR] $(date +%F\ %T) $*" >&2
  exit "${2:-1}"
}
die() { error "$@"; }

# 使用
[[ -f "$config" ]] || die "配置文件不存在: $config"

# 保留上一条命令的退出码再退出
cmd_fail; rc=$?
[ "$rc" -ne 0 ] && { echo "失败 rc=$rc" >&2; exit $rc; }

7.1 退出码约定与检查

退出码含义场景
0成功正常结束
1通用错误业务失败
2用法错误参数非法
130Ctrl+CSIGINT 中断
127命令不存在command not found
# 检查命令是否存在
if ! command -v jq >/dev/null 2>&1; then
  echo "缺少依赖 jq" >&2
  exit 1
fi

# 忽略特定失败:临时目录清理
rm -rf "$tmp" || true

一句话总结: 错误处理三件事:错误信息进 stderr、退出码有意义、关键前置条件先行检查。做到这三点,脚本在 CI 和运维里才可诊断。

8. 总结

环节要点
位置参数$1~$9 加花括号取高位,"$@" 保留原样
默认值${1:-def} 兜底,:? 必填校验
getoptsoptstring 声明选项,OPTARG/OPTIND 配套
shift左移消费参数,shift N 一次丢 N 个
IFS改分隔符拆分/拼接,配合 read 解析结构文本
校验正则/枚举/判空,早失败原则
错误退出码约定,stderr 报错,die 函数统一出口

参数解析与错误处理决定了脚本的可用性。getopts 管选项、shift 管消费、IFS 管拆分、die 管报错,四件套到位后脚本对外部输入就足够从容。再往上走,就是工程化与规范。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「shell」更多文章

  1. 脚本性能优化实战
  2. 网络请求与诊断实战
  3. 定时任务调度实战