跳到主要内容

命令行参数解析

命令行参数是脚本与用户交互的重要方式。本章介绍 Shell 脚本中解析命令行参数的各种方法,包括位置参数、getopts 内置命令以及手动解析技术。

位置参数基础

Shell 脚本通过位置参数接收命令行参数。

基本用法

#!/bin/bash
# 文件名:args.sh

echo "脚本名称: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "参数个数: $#"
echo "所有参数: $@"

执行 ./args.sh hello world 123 输出:

脚本名称: ./args.sh
第一个参数: hello
第二个参数: world
参数个数: 3
所有参数: hello world 123

位置参数的特殊变量

变量含义
$0脚本名称或路径
$1-$9第 1 到第 9 个参数
${10}第 10 个及之后的参数(需要花括号)
$#参数个数
$@所有参数(作为独立字符串)
$*所有参数(作为单个字符串)
$?上一个命令的退出状态
$$当前进程 PID

shift 命令

shift 命令将位置参数向左移动:

#!/bin/bash

echo "原始参数: $@"
echo "第一个参数: $1"

shift
echo "shift 后: $@"
echo "第一个参数: $1"

shift 2
echo "shift 2 后: $@"

执行 ./shift.sh a b c d e 输出:

原始参数: a b c d e
第一个参数: a
shift 后: b c d e
第一个参数: b
shift 2 后: d e

shift 常用于遍历所有参数:

#!/bin/bash

while [[ $# -gt 0 ]]; do
echo "处理参数: $1"
shift
done

参数默认值

#!/bin/bash

# 使用默认值
name="${1:-World}"
count="${2:-1}"

echo "Hello, $name!"
echo "Count: $count"

执行:

./default.sh          # 输出:Hello, World! Count: 1
./default.sh Alice 5 # 输出:Hello, Alice! Count: 5

检查参数是否存在

#!/bin/bash

# 检查参数个数
if [[ $# -lt 2 ]]; then
echo "用法: $0 <name> <count>"
exit 1
fi

# 检查参数是否为空
if [[ -z "$1" ]]; then
echo "错误: name 不能为空"
exit 1
fi

getopts 内置命令

getopts 是 Shell 内置的命令行选项解析工具,用于解析短选项(如 -a-b value)。

基本语法

getopts optstring name [arg...]
  • optstring:定义有效的选项字母,选项后跟 : 表示需要参数
  • name:存储当前选项的变量名
  • arg:可选,指定要解析的参数(默认解析位置参数)

简单示例

#!/bin/bash

while getopts "ab" opt; do
case $opt in
a) echo "选项 -a 被设置" ;;
b) echo "选项 -b 被设置" ;;
\?) echo "无效选项: -$OPTARG" >&2; exit 1 ;;
esac
done

执行:

./getopts1.sh -a        # 输出:选项 -a 被设置
./getopts1.sh -ab # 输出:选项 -a 被设置 / 选项 -b 被设置
./getopts1.sh -c # 输出:无效选项: -c

带参数的选项

在选项字母后加 : 表示该选项需要参数:

#!/bin/bash

while getopts "f:o:" opt; do
case $opt in
f) file="$OPTARG"; echo "文件: $file" ;;
o) output="$OPTARG"; echo "输出: $output" ;;
\?) echo "无效选项: -$OPTARG" >&2; exit 1 ;;
:) echo "选项 -$OPTARG 需要参数" >&2; exit 1 ;;
esac
done

执行:

./getopts2.sh -f input.txt -o output.txt
# 输出:
# 文件: input.txt
# 输出: output.txt

./getopts2.sh -f
# 输出:选项 -f 需要参数

getopts 的特殊变量

变量含义
OPTIND下一个要处理的参数索引(初始值为 1)
OPTARG当前选项的参数值
OPTERR是否显示错误信息(设为 0 禁用)

OPTIND 的作用

OPTIND 记录处理到哪个参数。处理完选项后,$OPTIND 指向第一个非选项参数:

#!/bin/bash

while getopts "a:b:c" opt; do
case $opt in
a) arg_a="$OPTARG" ;;
b) arg_b="$OPTARG" ;;
c) arg_c=1 ;;
esac
done

# 移动到非选项参数
shift $((OPTIND-1))

echo "选项 a: ${arg_a:-未设置}"
echo "选项 b: ${arg_b:-未设置}"
echo "选项 c: ${arg_c:-未设置}"
echo "剩余参数: $@"

执行:

./getopts3.sh -a hello -b world -c extra1 extra2
# 输出:
# 选项 a: hello
# 选项 b: world
# 选项 c: 1
# 剩余参数: extra1 extra2

静默模式

默认情况下,getopts 遇到无效选项会打印错误信息。在 optstring 开头加 : 可以进入静默模式:

#!/bin/bash

# 静默模式(开头加冒号)
while getopts ":a:b:" opt; do
case $opt in
a) echo "选项 a: $OPTARG" ;;
b) echo "选项 b: $OPTARG" ;;
\?)
echo "错误: 无效选项 -$OPTARG" >&2
exit 1
;;
:)
echo "错误: 选项 -$OPTARG 需要参数" >&2
exit 1
;;
esac
done

静默模式下:

  • 无效选项:$opt 设为 ?$OPTARG 包含无效选项字符
  • 缺少参数:$opt 设为 :$OPTARG 包含选项字符

完整的选项解析示例

#!/bin/bash

# 默认值
verbose=0
output=""
input=""

# 使用帮助
usage() {
cat << EOF
用法: $0 [选项] 文件...

选项:
-i FILE 输入文件
-o FILE 输出文件
-v 详细模式
-h 显示帮助信息

示例:
$0 -i input.txt -o output.txt
$0 -v -o result.txt data.txt
EOF
exit 0
}

# 解析选项
while getopts ":i:o:vh" opt; do
case $opt in
i) input="$OPTARG" ;;
o) output="$OPTARG" ;;
v) verbose=1 ;;
h) usage ;;
\?)
echo "错误: 无效选项 -$OPTARG" >&2
usage
;;
:)
echo "错误: 选项 -$OPTARG 需要参数" >&2
exit 1
;;
esac
done

# 移动到非选项参数
shift $((OPTIND-1))

# 处理参数
if [[ -n "$input" ]]; then
[[ $verbose -eq 1 ]] && echo "输入文件: $input"
fi

if [[ -n "$output" ]]; then
[[ $verbose -eq 1 ]] && echo "输出文件: $output"
fi

if [[ $# -gt 0 ]]; then
[[ $verbose -eq 1 ]] && echo "其他文件: $@"
fi

[[ $verbose -eq 1 ]] && echo "处理完成"

多次调用 getopts

如果需要多次解析参数,必须重置 OPTIND

#!/bin/bash

echo "第一次解析:"
while getopts "ab" opt; do
case $opt in
a) echo "选项 a" ;;
b) echo "选项 b" ;;
esac
done

# 重置 OPTIND
OPTIND=1

echo "第二次解析:"
while getopts "cd" opt; do
case $opt in
c) echo "选项 c" ;;
d) echo "选项 d" ;;
esac
done

长选项解析

getopts 只支持短选项(单字母)。如需解析长选项(如 --file),需要手动处理。

手动解析长选项

#!/bin/bash

verbose=0
output=""
input=""

while [[ $# -gt 0 ]]; do
case $1 in
-i|--input)
input="$2"
shift 2
;;
-o|--output)
output="$2"
shift 2
;;
-v|--verbose)
verbose=1
shift
;;
-h|--help)
echo "用法: $0 [-i|--input FILE] [-o|--output FILE] [-v|--verbose]"
exit 0
;;
-*)
echo "错误: 未知选项 $1" >&2
exit 1
;;
*)
# 非选项参数
break
;;
esac
done

echo "输入: $input"
echo "输出: $output"
echo "详细模式: $verbose"
echo "剩余参数: $@"

同时支持短选项和长选项

#!/bin/bash

usage() {
cat << EOF
用法: $0 [选项]

选项:
-f, --file FILE 指定文件
-o, --output FILE 指定输出文件
-v, --verbose 详细模式
-q, --quiet 静默模式
-h, --help 显示帮助
EOF
}

file=""
output=""
verbose=0

while [[ $# -gt 0 ]]; do
case $1 in
-f|--file)
file="$2"
shift 2
;;
-o|--output)
output="$2"
shift 2
;;
-v|--verbose)
verbose=1
shift
;;
-q|--quiet)
verbose=0
shift
;;
-h|--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
echo "错误: 未知选项 $1" >&2
usage
exit 1
;;
*)
break
;;
esac
done

echo "文件: $file"
echo "输出: $output"
echo "详细: $verbose"
echo "其他: $@"

带 = 的长选项

#!/bin/bash

while [[ $# -gt 0 ]]; do
case $1 in
--file=*)
file="${1#*=}"
shift
;;
--output=*)
output="${1#*=}"
shift
;;
--verbose)
verbose=1
shift
;;
*)
echo "未知选项: $1"
exit 1
;;
esac
done

echo "文件: $file"
echo "输出: $output"

参数验证

验证必填参数

#!/bin/bash

input=""
output=""

while getopts "i:o:" opt; do
case $opt in
i) input="$OPTARG" ;;
o) output="$OPTARG" ;;
esac
done

# 验证必填参数
if [[ -z "$input" ]]; then
echo "错误: 必须指定输入文件 (-i)" >&2
exit 1
fi

if [[ -z "$output" ]]; then
echo "错误: 必须指定输出文件 (-o)" >&2
exit 1
fi

echo "处理 $input -> $output"

验证文件存在性

#!/bin/bash

validate_file() {
local file=$1

if [[ ! -e "$file" ]]; then
echo "错误: 文件不存在: $file" >&2
return 1
fi

if [[ ! -f "$file" ]]; then
echo "错误: 不是普通文件: $file" >&2
return 1
fi

if [[ ! -r "$file" ]]; then
echo "错误: 文件不可读: $file" >&2
return 1
fi

return 0
}

input=""

while getopts "i:" opt; do
case $opt in
i) input="$OPTARG" ;;
esac
done

if [[ -n "$input" ]]; then
validate_file "$input" || exit 1
fi

验证数值参数

#!/bin/bash

is_number() {
[[ "$1" =~ ^[0-9]+$ ]]
}

count=1

while getopts "n:" opt; do
case $opt in
n)
if is_number "$OPTARG"; then
count="$OPTARG"
else
echo "错误: -n 需要数值参数,得到: $OPTARG" >&2
exit 1
fi
;;
esac
done

echo "计数: $count"

验证枚举值

#!/bin/bash

mode="default"

while getopts "m:" opt; do
case $opt in
m)
case $OPTARG in
fast|normal|slow)
mode="$OPTARG"
;;
*)
echo "错误: 模式必须是 fast、normal 或 slow" >&2
exit 1
;;
esac
;;
esac
done

echo "模式: $mode"

实用示例

完整的命令行工具模板

#!/bin/bash

set -e

# 配置
readonly SCRIPT_NAME=$(basename "$0")
readonly SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)

# 默认值
VERBOSE=0
DRY_RUN=0
OUTPUT=""
INPUT=""

# 颜色输出
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'

log_info() {
[[ $VERBOSE -eq 1 ]] && echo -e "${GREEN}[INFO]${NC} $*"
}

log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*" >&2
}

log_error() {
echo -e "${RED}[ERROR]${NC} $*" >&2
}

# 使用帮助
usage() {
cat << EOF
用法: $SCRIPT_NAME [选项] <输入文件>

描述:
这是一个示例脚本,展示命令行参数解析的最佳实践。

选项:
-i, --input FILE 输入文件(必需)
-o, --output FILE 输出文件(默认: stdout)
-v, --verbose 详细输出
-n, --dry-run 模拟运行,不执行实际操作
-h, --help 显示此帮助信息

示例:
$SCRIPT_NAME -i data.txt -o result.txt
$SCRIPT_NAME --verbose --input data.txt
$SCRIPT_NAME -n -i data.txt

作者:
Your Name <[email protected]>
EOF
}

# 解析短选项
parse_short_options() {
while getopts ":i:o:vnh" opt; do
case $opt in
i) INPUT="$OPTARG" ;;
o) OUTPUT="$OPTARG" ;;
v) VERBOSE=1 ;;
n) DRY_RUN=1 ;;
h) usage; exit 0 ;;
\?)
log_error "无效选项: -$OPTARG"
usage
exit 1
;;
:)
log_error "选项 -$OPTARG 需要参数"
exit 1
;;
esac
done
}

# 解析长选项
parse_long_options() {
while [[ $# -gt 0 ]]; do
case $1 in
--input)
INPUT="$2"
shift 2
;;
--output)
OUTPUT="$2"
shift 2
;;
--verbose)
VERBOSE=1
shift
;;
--dry-run)
DRY_RUN=1
shift
;;
--help)
usage
exit 0
;;
--)
shift
break
;;
-*)
log_error "未知选项: $1"
usage
exit 1
;;
*)
break
;;
esac
done
}

# 验证参数
validate_args() {
if [[ -z "$INPUT" ]]; then
log_error "必须指定输入文件"
usage
exit 1
fi

if [[ ! -f "$INPUT" ]]; then
log_error "输入文件不存在: $INPUT"
exit 1
fi

if [[ ! -r "$INPUT" ]]; then
log_error "输入文件不可读: $INPUT"
exit 1
fi
}

# 主逻辑
main() {
log_info "开始处理"
log_info "输入文件: $INPUT"
log_info "输出文件: ${OUTPUT:-stdout}"

if [[ $DRY_RUN -eq 1 ]]; then
log_warn "模拟运行模式,不执行实际操作"
return 0
fi

# 实际处理逻辑
if [[ -n "$OUTPUT" ]]; then
cat "$INPUT" > "$OUTPUT"
log_info "已写入: $OUTPUT"
else
cat "$INPUT"
fi

log_info "处理完成"
}

# 入口
if [[ $# -eq 0 ]]; then
usage
exit 0
fi

# 判断是否有长选项
if [[ "$*" == *--* ]]; then
parse_long_options "$@"
else
parse_short_options "$@"
shift $((OPTIND-1))
fi

validate_args
main "$@"

带子命令的脚本

#!/bin/bash

usage() {
cat << EOF
用法: $SCRIPT_NAME <命令> [选项]

命令:
add 添加项目
remove 删除项目
list 列出项目
help 显示帮助

全局选项:
-v, --verbose 详细输出
-h, --help 显示帮助

运行 '$SCRIPT_NAME <命令> --help' 获取命令详情。
EOF
}

SCRIPT_NAME=$(basename "$0")
VERBOSE=0

# 全局选项
while [[ $# -gt 0 ]]; do
case $1 in
-v|--verbose)
VERBOSE=1
shift
;;
-h|--help)
usage
exit 0
;;
-*)
echo "错误: 未知选项 $1" >&2
exit 1
;;
*)
break
;;
esac
done

# 获取子命令
COMMAND="${1:-}"
shift || true

# 子命令处理
case $COMMAND in
add)
name=""
while [[ $# -gt 0 ]]; do
case $1 in
-n|--name)
name="$2"
shift 2
;;
-h|--help)
echo "用法: $SCRIPT_NAME add -n <name>"
exit 0
;;
esac
done

[[ -z "$name" ]] && { echo "错误: 必须指定名称"; exit 1; }
echo "添加项目: $name"
;;

remove)
id=""
while [[ $# -gt 0 ]]; do
case $1 in
-i|--id)
id="$2"
shift 2
;;
-h|--help)
echo "用法: $SCRIPT_NAME remove -i <id>"
exit 0
;;
esac
done

[[ -z "$id" ]] && { echo "错误: 必须指定 ID"; exit 1; }
echo "删除项目: $id"
;;

list)
echo "列出所有项目"
;;

help|"")
usage
;;

*)
echo "错误: 未知命令 '$COMMAND'" >&2
usage
exit 1
;;
esac

小结

本章介绍了 Shell 脚本的命令行参数解析:

  • 位置参数$1$@$# 等特殊变量,shift 命令
  • getopts:内置的短选项解析工具,支持 -a-a value 格式
  • 长选项:手动解析 --option--option=value 格式
  • 参数验证:检查必填参数、文件存在性、数值有效性
  • 完整模板:包含帮助信息、错误处理、详细模式的脚本模板

良好的参数解析设计可以让脚本更易用、更健壮。下一章将介绍脚本安全与最佳实践。