Shell 脚本传递参数详解:getopts、while + case 与其他方法
适用于 Bash 脚本。文中的“
getops”通常是“getopts”的笔误;Bash 内置命令是getopts,没有常用的getops。
1. 为什么要解析脚本参数
脚本参数来自命令行,例如:
./backup.sh -s /data -d /backup --compress
脚本通常需要区分:
- 选项:
-s、-d、--compress - 选项值:
/data、/backup - 位置参数:没有选项名、按顺序传入的参数
- 参数结束标记:
--
先看 Bash 自动提供的变量:
| 变量 | 含义 |
|---|---|
$0 |
脚本名称 |
$1、$2
|
第 1、2 个位置参数 |
$# |
参数个数 |
"$@" |
保留边界的全部参数,最常用 |
"$*" |
将全部参数拼成一个字符串 |
$? |
上一条命令的退出状态 |
处理参数时通常使用 "$@",避免参数中含空格时被错误拆分。
2. 方法一:直接使用位置参数
适合参数固定、数量少的脚本:
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 2 ]]; then
echo "用法:$0 <源目录> <目标目录>" >&2
exit 64
fi
source_dir=$1
target_dir=$2
printf '源目录:%s\n目标目录:%s\n' "$source_dir" "$target_dir"
调用:
./copy.sh /data /backup
这种写法简单,但调用者必须记住参数顺序,扩展性较弱。
3. 方法二:while + case 手动解析短选项
3.1 基本思路
- 用
while判断是否还有参数。 - 用
case "$1"判断当前参数类型。 - 用
shift移除已经处理的参数。 - 选项带值时,读取下一个参数
$2,再额外shift一次。
3.2 完整示例
#!/usr/bin/env bash
set -euo pipefail
src=''
dst=''
verbose=false
usage() {
cat <<'USAGE'
用法:backup.sh -s <源目录> -d <目标目录> [-v] [-h]
选项:
-s DIR 源目录
-d DIR 目标目录
-v 显示详细信息
-h 显示帮助
USAGE
}
while (($# > 0)); do
case "$1" in
-s|--source)
if (($# < 2)); then
echo "错误:$1 缺少参数" >&2
exit 64
fi
src=$2
shift 2
;;
-d|--destination)
if (($# < 2)); then
echo "错误:$1 缺少参数" >&2
exit 64
fi
dst=$2
shift 2
;;
-v|--verbose)
verbose=true
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-* )
echo "错误:未知选项:$1" >&2
usage >&2
exit 64
;;
*)
echo "错误:无法识别的普通参数:$1" >&2
exit 64
;;
esac
done
if [[ -z $src || -z $dst ]]; then
echo '错误:必须同时提供 -s 和 -d' >&2
usage >&2
exit 64
fi
if [[ $verbose == true ]]; then
printf '准备处理:%s -> %s\n' "$src" "$dst"
fi
调用示例:
./backup.sh -s /data -d /backup
./backup.sh --source /data --destination /backup --verbose
3.3 shift 的关键规律
假设当前参数是:
$1=-s $2=/data $3=-v
处理 -s 后执行 shift 2,结果变成:
$1=-v
只有布尔开关时执行 shift;选项和选项值成对出现时执行 shift 2。忘记移动参数会造成死循环或重复处理。
3.4 支持短选项合并
手写解析可以自行支持 -v -f 或 -vf,但需要额外拆分:
case "$1" in
-v|-f)
# 单个开关
shift
;;
-vf|-fv)
verbose=true
force=true
shift
;;
esac
选项数量增加后,维护成本会明显上升,通常改用 getopts。
4. 方法三:getopts 解析短选项
getopts 是 Bash/POSIX shell 常用的短选项解析工具,能够处理选项值、非法选项和缺少参数等情况。
4.1 基本语法
getopts optstring name
例如:
while getopts ':s:d:vh' opt; do
case "$opt" in
s) src=$OPTARG ;;
d) dst=$OPTARG ;;
v) verbose=true ;;
h) usage; exit 0 ;;
:) echo "选项 -$OPTARG 缺少参数" >&2; exit 64 ;;
\?) echo "未知选项:-$OPTARG" >&2; exit 64 ;;
esac
done
shift "$((OPTIND - 1))"
4.2 选项字符串规则
选项字符串 :s:d:vh 的含义:
| 写法 | 含义 |
|---|---|
s |
-s 不需要参数 |
s: |
-s 必须有一个参数 |
:s:d:vh 开头的 :
|
静默错误模式,交给脚本自行处理 |
getopts 相关变量:
| 变量 | 含义 |
|---|---|
$opt |
当前解析到的选项字母 |
$OPTARG |
当前选项的参数值 |
$OPTIND |
下一个待解析参数的位置 |
4.3 完整示例
#!/usr/bin/env bash
set -euo pipefail
src=''
dst=''
verbose=false
usage() {
echo "用法:$0 -s <源目录> -d <目标目录> [-v] [-h]"
}
while getopts ':s:d:vh' opt; do
case "$opt" in
s) src=$OPTARG ;;
d) dst=$OPTARG ;;
v) verbose=true ;;
h) usage; exit 0 ;;
:) echo "错误:选项 -$OPTARG 缺少参数" >&2; usage >&2; exit 64 ;;
\?) echo "错误:未知选项 -$OPTARG" >&2; usage >&2; exit 64 ;;
esac
done
shift "$((OPTIND - 1))"
if (($# > 0)); then
echo "错误:多余的位置参数:$*" >&2
exit 64
fi
if [[ -z $src || -z $dst ]]; then
echo '错误:-s 和 -d 都是必需的' >&2
usage >&2
exit 64
fi
printf '源目录:%s\n目标目录:%s\n详细模式:%s\n' "$src" "$dst" "$verbose"
调用:
./backup.sh -s /data -d /backup
./backup.sh -s/data -d/backup -v
./backup.sh -s /data -d /backup -- extra-file.txt
getopts 通常支持 -v、-s value、-svalue 等短选项形式。它主要处理短选项,长选项需要手写或使用其他工具。
5. getopts 与 while + case 对比
| 项目 | getopts |
手写 while + case
|
|---|---|---|
| 短选项 | 支持完善 | 需要自己实现 |
| 缺少参数检测 | 内置基础能力 | 自己检查 $#
|
| 长选项 | 通常需额外处理 | 容易直接支持 |
| 代码量 | 较少 | 较多 |
| 可控性 | 规范、统一 | 灵活 |
| 适用场景 | 标准短选项 CLI | 长选项、特殊语法、复杂参数 |
6. 长选项的常见写法
6.1 手写 while + case
while (($# > 0)); do
case "$1" in
--name)
(($# >= 2)) || { echo '--name 缺少值' >&2; exit 64; }
name=$2
shift 2
;;
--name=*)
name=${1#*=}
shift
;;
--dry-run)
dry_run=true
shift
;;
--)
shift
break
;;
*)
echo "未知参数:$1" >&2
exit 64
;;
esac
done
${1#*=} 表示删除第一个 = 及其左侧内容。例如 --name=alice 会得到 alice。
6.2 Bash 数组保存剩余位置参数
-- 后的参数通常作为文件名、命令或其他输入保存:
files=()
while (($# > 0)); do
case "$1" in
-v) verbose=true; shift ;;
--) shift; files+=("$@"); break ;;
*) files+=("$1"); shift ;;
esac
done
printf '文件:%s\n' "${files[@]}"
7. 其他方法
7.1 bash 的 [[ ... ]] 与数组
参数很少时,可以直接判断:
if [[ ${1:-} == '--version' ]]; then
echo '1.0.0'
exit 0
fi
${1:-} 可以避免未设置参数时触发错误。
7.2 read 交互式输入
参数适合自动化调用;read 适合交互式脚本:
read -r -p '请输入环境名:' environment
printf '环境:%s\n' "$environment"
生产脚本通常同时提供参数和交互模式,避免无人值守执行时卡住。
7.3 docopt、argbash、第三方 CLI 框架
当参数规则非常复杂时,可以使用项目允许的第三方工具生成或统一解析帮助信息。但要注意目标机器是否安装依赖、脚本是否仍需 POSIX 兼容,以及部署时如何携带依赖。
8. 参数校验与安全习惯
必做检查
[[ -n ${src:-} ]] || { echo 'src 不能为空' >&2; exit 64; }
[[ -d $src ]] || { echo "目录不存在:$src" >&2; exit 66; }
不要直接拼接未引用参数
错误:
rm -rf $target
正确:
rm -rf -- "$target"
-- 可以防止以 - 开头的路径被误认为选项;双引号可以保留空格和通配符。
不要用 eval 解析普通参数
eval 会再次执行字符串中的 shell 语法,容易产生命令注入。优先使用数组、case、参数展开和明确的校验。
退出码建议
常见约定:
-
0:成功 -
1:一般失败 -
2:命令用法错误(也可使用更明确的64) -
64:参数错误 -
66:输入文件或目录不存在
9. 调试和验证
查看 Bash 语法:
bash -n script.sh
跟踪执行过程:
bash -x script.sh -s /data -d /backup
建议至少验证:
./script.sh -h
./script.sh # 缺少参数
./script.sh -x # 未知选项
./script.sh -s # 选项缺少值
./script.sh -s '/path/with space' # 路径含空格
./script.sh -- -file.txt # 以短横线开头的普通参数
10. 推荐选择
- 参数固定且很少:位置参数
$1、$2。 - 标准短选项:
getopts + while + case。 - 同时需要
--source、--source=value等长选项:手写while + case。 - 参数规则复杂:考虑 Bash 数组配合手写解析,或引入经过评估的 CLI 工具。
- 面向生产:提供
-h,检查缺少值和未知选项,使用--、双引号、明确退出码,并先做语法和边界验证。