应使用 getopts 处理单字符选项(如 -f file),用 getopt 支持长选项(--file),并严格校验必需参数、路径合法性及数值范围,避免静默失败。

shell 脚本里怎么正确读取命令行参数
直接用 $1、$2 硬编码位置参数,看似简单,但只要加个可选参数或换下参数顺序,脚本就容易出错甚至静默失败。真正健壮的参数处理必须区分必需参数、可选参数、带值参数(如 -f file)和开关型参数(如 -v)。
-
getopts是 bash 内置工具,轻量、POSIX 兼容,适合单字符选项(-a -b -c或合并写法-abc),但不支持长选项(--help) - 如果需要
--verbose或--output-dir=xxx,得用getopt(注意是外部命令,非内置,路径通常是/usr/bin/getopt),它支持长选项和参数重排,但输出需二次解析 - 手动解析
$@也能做,但容易漏掉引号包裹的含空格参数(如"my file.txt"),且逻辑臃肿,只建议极简场景临时用
getopts 处理带参数的选项时常见崩溃点
getopts 的选项字符串里,某个字母后加冒号(:)才表示该选项“必须跟一个参数”,否则会被当成无参开关。很多人漏写这个冒号,结果 -f filename 中的 filename 被当成下一个位置参数,$OPTARG 为空,脚本后续报错。
while getopts "f:o:v" opt; do
case $opt in
f) input_file="$OPTARG" ;; # ✅ -f 必须跟值,字符串中写 "f:"
o) output_dir="$OPTARG" ;; # ✅ 同理,"o:"
v) verbose=true ;; # ❌ -v 无参,字符串中是 "v"(无冒号)
:) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;;
\?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
esac
done- 选项字符串写成
"fo:v"是错的:这会让getopts认为-f后必须跟值,但-o后不许跟值 —— 实际上-o是要接目录的 -
getopts自动跳过非选项参数(比如./script.sh -f a.txt b.txt中的b.txt),需要用shift $((OPTIND - 1))把剩余参数挪到$1开始,否则无法访问它们 - 错误选项触发
\?分支,但缺参数触发的是:分支,二者必须都捕获,否则用户输错时脚本静默继续执行
用 getopt 支持 --help 和 --output-dir 这类长选项
getopt 命令本身不解析,它只是把原始参数重排成标准格式(比如把 --output-dir dir --verbose file.txt 变成 --output-dir 'dir' --verbose -- 'file.txt'),再交给 eval set -- 重载位置参数。这步极易出错 —— 少了 eval 或引号没处理好,含空格路径就会被拆开。
args=$(getopt -o f:o:v --long help,output-dir:,verbose -n "$0" -- "$@")
if [ $? -ne 0 ]; then
echo "Terminating..." >&2
exit 1
fi
eval set -- "$args"
while true; do
case "$1" in
-f | --file) input_file="$2"; shift 2 ;;
-o | --output-dir) output_dir="$2"; shift 2 ;;
-v | --verbose) verbose=true; shift ;;
--help) echo "Usage: $0 [-f FILE] [--output-dir DIR]"; exit 0 ;;
--) shift; break ;;
*) echo "Internal error!"; exit 1 ;;
esac
done-
getopt -o定义短选项,--long定义长选项;长选项后加冒号(output-dir:)表示必须带值,不加则为开关 -
eval set -- "$args"这行不能省,也不能写成set -- $args(未加引号会破坏空格) - 最后的
--是分隔符,之后的参数是“非选项参数”,比如输入文件列表,要用shift跳过它再遍历剩余$@
参数校验必须放在解析之后、业务逻辑之前
很多脚本把参数解析和校验混在一起,导致缺必要参数时仍执行了部分初始化逻辑(比如创建临时目录、连接数据库),既浪费资源又掩盖真实问题。校验应独立成块,明确检查“是否提供了所有必需项”“值是否合法(如文件是否存在、端口是否在 1–65535)”。
- 必需参数缺失:用
[ -z "$input_file" ] && { echo "Error: -f is required"; exit 1; } - 路径合法性:用
[ ! -f "$input_file" ] && { echo "Error: $input_file not found"; exit 1; },避免后续cat报错才提示 - 数值范围:用
[[ "$port" =~ ^[0-9]+$ ]] && [ "$port" -ge 1 ] && [ "$port" -le 65535 ] || { echo "Invalid port"; exit 1; } - 不要在
getopts循环里做 heavy 校验(比如远程连通性测试),那会让帮助信息变慢,也违背“快速失败”原则
参数解析和校验这两步,看着只是几行代码,但决定脚本能被别人放心复用,还是每次运行都要猜用户意图。最容易被忽略的是:没处理引号包裹的路径、没校验必需参数、以及把 getopt 的输出直接当普通字符串用而忘了 eval set --。










