在编写Bash脚本时,处理命令行参数是不可避免的任务。无论是像TypeScript、Python、C++还是JavaScript这样的语言,都有成熟的库来解析参数,但Bash脚本却需要开发者手动实现。本文将深入探讨Bash中两种主流的参数解析方法——shift命令getopts命令,帮助你写出更健壮、更易维护的脚本。

命令行参数的基本约定

Unix命令通常遵循特定的参数语法。以常见的ls命令为例,其参数结构如下:

tar -x -f archive.tar -v -- file1 file2 file3

这里的约定很重要:选项必须出现在非选项参数之前。选项分为两类:

  • 独立选项:如-l,只需存在或不存在,无需额外参数
  • 带参选项:如-o,后面必须跟一个参数值

选项处理的核心是编写一个循环,遍历参数列表,识别并处理每个选项,最终将参数列表缩减为仅包含非选项参数(如文件名)。

shift命令:移动位置参数的基础工具

shift是Bash内置命令,用于向左移动位置参数列表。每次执行shift(默认移动1位),$1被删除,$2变为新的$1,以此类推。这就像从队列中出队元素。

基本用法示例:

#!/bin/bash
# test_shift.sh
echo "初始参数: $@"
echo "参数个数: $#"
echo "\$1 = $1"
shift
echo "--- shift 后 ---"
echo "剩余参数: $@"
echo "参数个数: $#"
echo "\$1 = $1"

运行结果:

$ ./test_shift.sh a b c d
初始参数: a b c d
参数个数: 4
$1 = a
--- shift 后 ---
剩余参数: b c d
参数个数: 3
$1 = b

你可以指定移动位数:

#!/bin/bash
echo "原始参数: $@"
shift 3
echo "shift 3 后: $@"

常见应用场景

  • 遍历所有参数:使用while [ $# -gt 0 ]循环,每次处理$1后执行shift
  • 解析带参选项:当选项需要参数时,shift次数比不带参选项多1
  • 分离第一个参数:先用$1保存特殊值,再shift处理剩余参数
#!/bin/bash
# 逐个处理所有参数(使用shift会修改参数列表)
count=1
while [ $# -gt 0 ]; do
echo "参数 $count: $1"
shift
((count++))
done

手动循环解析选项

手动解析选项是最灵活的方法,可以处理单字母选项、长选项、带或不带参数的选项。这里使用while循环配合case语句:

#!/bin/sh
die() {
printf '%s\n' "$1" >&2
exit 1
}
#初始化可能要用到的选项变量,防止环境中已有的同名变量对其产生干扰
# Initialize all the option variables.
# This ensures we are not contaminated by variables from the environment.
file= #默认文件名为空
verbose=0
# 编写选项处理的循环:手动循环
# 这里我们在while内部,esac后面放置一个shift语句,让各个选项解析分支内,少写或者不写shift,尤其是不带参数的命令行选项.
while :; do
case $1 in
-h|-\?|--help)
show_help    # Display a usage synopsis.
exit
;;
-f|--file)
if [[ -n "$2" && "$2" != -* ]]; then
file="$2"
shift
else
die 'ERROR: "--file" requires a non-empty option argument.'
fi
;;
-f=*|--file=*)
# 将等号以及前面的部分移除,剩下的就是文件名(路径)了
file=${1#*=} # Delete everything up to "=" and assign the remainder.
# 为了防止用户传入的文件名是空的,这里做检查
[[ -z "$file" ]] && die 'ERROR: "--file" requires an argument.'
shift
;;
-v|--verbose)
# 将日志的详细信息设置为多级,并且允许叠加使用此选项来增加详细程度(数值),-v -v 就是2级
verbose=$((verbose + 1))  # Each -v adds 1 to verbosity.
;;
-vv*)
# 提取 - 后面的部分
flags="${1#-}"
# 检查flags是否全为 v
if [[ "$flags" =~ ^v+$ ]]; then
# 计算 v 的数量
verbose=$((verbose + ${#flags}))
else
echo "未知选项: $1"
exit 1
fi
;;
--)              # End of all options.
shift
break
;;
-?*)
printf 'WARN: Unknown option (ignored): %s\n' "$1" >&2
;;
*)               # Default case: No more options, so break out of the loop.
break
esac
shift
done
# Rest of the program here.
# If there are input files (for example) that follow the options, they
# will remain in the "$@" positional parameters.

注意事项

  • 选项带参数时,shift的位数比不带参时多1
  • 支持-o value-ovalue两种风格
  • 长选项如--output也可处理,但通常不与单字母选项拆分同时支持

这种方法的优点在于完全可控,适合复杂需求。但缺点是代码量较大,且容易出错。

getopts命令:标准化的选项解析方案

getopts是Bash内置的选项解析命令,适用于简单到中等复杂度的脚本。其基本语法如下:

getopts optstring name [arg ...]

核心概念

  • optstring:定义可接受的选项字母,后面跟:表示该选项需要参数
  • name:存储当前解析到的选项字母的变量名
  • $OPTARG:存储当前选项的参数值
  • $OPTIND:存储下一个待处理参数的索引,初始为1
变量描述
选项值变量,存储当前选项的参数值(如果该选项需要参数)。在错误报告模式是静默模式时,也可能保存到是当前未被正确处理的选项的名字(字母)
选项索引变量,存储下一个要处理的参数(命令行中传递给脚本的参数)的索引值(从 1 开始按需增加)。
选项错误变量,控制是否显示错误信息(默认为 1 开启,设为 0 关闭)。如果变量 OPTERR 的值为 0,getopts 会禁止打印错误信息,即使 OPTSTRING 的第一个字符不是冒号(非静默模式)。

最简单的用例

#!/bin/bash
while getopts ":a" opt; do
case $opt in
a)
# 将消息打印到stdout指向stderr,默认都输出到屏幕上
echo "-a was triggered!"
;;
\?)
# 例如使用选项-b,由于未在optstring中定义,会触此分支(非法选项b会被记录到变量OPTARG中)
echo "Invalid option: -$OPTARG"
;;
esac
done

这个例子中,getopts放在while循环的条件测试部分,每次循环解析一个选项。选项字母存储在opt变量中,参数值存储在$OPTARG中。

getopts的错误处理模式

getopts支持两种错误处理模式:

  • 静默模式optstring:开头,不输出错误消息,$OPTARG存储错误选项字母
  • 详细模式:默认模式,输出错误消息,$OPTARG被置空

示例(静默模式):

# (base) cxxu@CxxuDesk 15:28:18> <~>
$ bash test_getopts.sh -a
-a was triggered!
# 错误处理:
# (base) cxxu@CxxuDesk 15:31:23> <~>
$ bash test_getopts.sh -b
Invalid option: -b
$ bash test_getopts.sh -b -c
Invalid option: -b
Invalid option: -c

退出状态:当所有选项解析完毕或遇到错误时,getopts返回非零值,循环结束。

最佳实践:在开发阶段使用详细模式便于调试,生产环境使用静默模式并提供自定义错误消息。

getopts与shift的协同使用

getopts循环结束后,通常需要手动shift掉已处理的选项,使剩余参数变为非选项参数:

# (base) cxxu@CxxuDesk 17:53:26> <~>
$ bash ./test_getopts.sh normal -a
normal mode.
args for getopts: [-a]
arg 1: -a
optstring=[a:x]
./test_getopts.sh: option requires an argument -- a
Invalid option: (opt=?) [OPTARG=]
(opt=?) [OPTARG=]

这种组合方式非常实用:getopts负责解析选项,shift负责清理参数列表,最终$@中只剩下文件名等非选项参数。

[AFFILIATE_SLOT_1]

总结与选择建议

选择哪种方法取决于你的需求:

  • 简单脚本:使用getopts,代码简洁,标准化
  • 复杂需求:手动循环解析,灵活性更高
  • 跨平台兼容:优先使用getopts,它遵循POSIX标准

无论选择哪种方法,记住:选项必须在非选项参数之前,并且始终使用--作为选项结束指示符。对于更复杂的脚本,考虑使用Python、TypeScript或JavaScript等语言,它们提供了更完善的参数解析库。

[AFFILIATE_SLOT_2]

OPTARGOPTINDOPTERR