Shell 脚本接收的是有顺序的参数列表,不是需要重新按空格拆分的一整条命令字符串。位置参数通过 $1、$2 等编号暴露这份列表,$# 表示数量。固定且简单的接口可以直接使用位置,带可选设置或开关的接口则适合明确解析。无论采用哪种形式,都需要规定允许几个参数、是否允许空值,以及输入错误时怎样失败。
解释含义之前,先保住列表边界
$0 表示脚本名称等特殊身份,不属于位置参数,也不计入 $#。使用 bash -c 时,命令字符串之后的第一个参数会成为 $0,包装脚本若误以为它是 $1,就会看起来少收到一个参数。超过第九个位置必须使用花括号:${10} 是第十个参数,$10 却是第一个参数再接一个字面数字零,两种写法不能互换。
"$@" 会为每个传入参数保留一个词,包括空字符串和包含空格的值。未引用的展开可能再次分词并展开通配符,"$*" 则会把列表连成一个字符串。转发或遍历时应使用带引号的完整列表。set -- 替换当前的位置参数,shift 删除前面的若干项;这些操作在对应脚本或函数作用域中生效,不会反向改写外部调用者原来的参数列表。
空参数仍然占据一个位置。检查 $# 可以区分完全没有提供参数,与明确传入 '' 两种情况。${1:?message} 同时拒绝缺失和空值,只有接口要求非空时才合适。应先验证数量,再读取必需的位置,最后判断内容是否合法,避免在参数本来不存在时就触发与接口说明无关的展开错误。
getopts 检查语法,业务规则检查含义
可移植的 getopts 处理短选项。选项字符后面的冒号表示该选项需要一个值,但不会自动提供 --user 这类长选项支持。选项说明开头的冒号表示采用安静错误报告:未知选项产生问号,缺少值产生冒号,相关选项字符放在 OPTARG 中。脚本必须明确处理这些分支,不能因为解析循环继续运行就误以为输入合法。
OPTIND 记录解析进度。在同一 Shell 中开始解析另一份参数之前,应把它重置为一;解析完成后,使用 shift "$((OPTIND - 1))" 让剩余操作数成为当前列表。-- 结束选项处理,因此以连字符开头的文件名也能被当作操作数。普通的非选项同样会结束扫描,不能假定写在文件名后面的选项会被自动搬到前面。
解析器拿到一个值,不等于它的业务含义有效。调用者显式提供空字符串,可以满足“这里有一个选项参数”的语法要求,却不满足“名称必须非空”的接口规则。默认值也只应用于省略的可选设置,不应掩盖错误输入。诊断应写到标准错误,并在执行实际工作前返回约定的非零状态,让调用方能够可靠识别参数错误。
可复用的 Bash 解析函数应当同时局部化 OPTARG 与 OPTIND。getopts 在处理选项时会更新或取消 OPTARG;即使每次正确重置索引,未局部化的 OPTARG 仍可能破坏外层解析器状态。下面让两者都局部化,并确认调用者预先设置的哨兵值在全部调用之后仍然保留。
坏掉的样子
常见问题之一是正常输入能跑,多余的操作数却被悄悄丢弃。另一种是忘记重置解析索引,导致函数第一次调用正确,后续调用跳过部分选项。未引用转发也可能先改变参数数量,产生类似症状。应分别检查接收到的列表与解析状态,而不是不断补几个移位操作,直到某一次特定调用碰巧成功。
下面的示例明确规定一个小接口:可选的名称选项、可选的详细输出开关,以及恰好一个输入操作数。名称必须非空。函数使用 Bash 的局部变量声明,因此完整示例依赖 Bash,虽然其中的选项解析器本身是可移植的。面向其他 Shell 的脚本,应采用目标 Shell 支持的作用域规则,而不能把局部声明也当成统一标准。
快速验证
在 Bash 中运行完整代码块。开头检查第十个参数的引用方式,以及空参数是否被保留。随后测试一次合法调用,再分别测试缺值、未知选项、空名称和多余操作数。演示函数只打印解析结果,不修改文件,也不启动外部程序,所以可以直接观察参数边界,而不把其他业务副作用混进验证。
BASH_ENV= bash --noprofile --norc <<'BASH'
set -- one 2 3 4 5 6 7 8 9 ten
printf 'without-braces=<%s> tenth=<%s>\n' "$10" "${10}"
set -- '' 'two words'
printf 'count=%s\n' "$#"
for demo_arg in "$@"; do printf 'argument=<%s>\n' "$demo_arg"; done
demo_cli() {
local OPTIND=1 OPTARG demo_option demo_name=guest demo_verbose=no
while getopts ':n:v' demo_option; do
case "$demo_option" in
n) demo_name=$OPTARG ;;
v) demo_verbose=yes ;;
:) printf 'missing value for -%s\n' "$OPTARG" >&2; return 2 ;;
\?) printf 'unknown option -%s\n' "$OPTARG" >&2; return 2 ;;
esac
done
shift "$((OPTIND - 1))"
if [ "$#" -ne 1 ] || [ -z "$demo_name" ]; then
printf 'need a nonempty name and exactly one input\n' >&2
return 2
fi
printf 'name=<%s> verbose=%s input=<%s>\n' "$demo_name" "$demo_verbose" "$1"
}
OPTARG=caller-sentinel
demo_cli -v -n 'Jane Doe' -- '-input file'
if demo_cli -n 2>/dev/null; then exit 1; else printf 'missing=%s\n' "$?"; fi
if demo_cli -x 2>/dev/null; then exit 1; else printf 'unknown=%s\n' "$?"; fi
if demo_cli -n '' input 2>/dev/null; then exit 1; else printf 'empty=%s\n' "$?"; fi
if demo_cli one two 2>/dev/null; then exit 1; else printf 'extra=%s\n' "$?"; fi
demo_cli second
[[ $OPTARG == caller-sentinel ]] || exit 1
printf 'caller OPTARG preserved\n'
BASH
第一行应分别得到 one0 与 ten,之后参数数量为二,打印出空字符串和带空格的第二项。合法调用保留 Jane Doe 和 -input file 的完整内容。四类错误输入都应返回状态二。最后一次调用重新使用默认名称、关闭详细开关,并接收 second,证明重复解析从新的状态开始,而不是沿用前一次的索引或选项值。
这些观察验证了接口边界,不只是证明存在一段用法提示。扩展真实命令时,应补充新选项的内容规则,同时维持已有的引用、空值和结束选项标记语义。参数能够被读到,是解析的开始;只有合法输入被接受、非法输入被明确拒绝,接口约定才真正成立。
来源
- Bash 位置参数,编号参数及函数作用域规则,已核验 2026-09-07。
- POSIX getopts,选项语法错误与解析状态重置,已核验 2026-09-07。