一、什么是位置参数
位置参数(Positional Parameters)是 Shell 脚本接收命令行输入的基础机制。当你执行 ./script.sh arg1 arg2 arg3 时,Shell 会自动将这些参数映射到一组特殊变量中:
| 变量 | 含义 | 示例值 |
|---|---|---|
$0 |
当前脚本名称 | ./script.sh |
$1 ~ $9 |
第 1~9 个参数 | arg1, arg2 |
${10} |
第 10 个及以后的参数 | arg10 |
$# |
参数个数(不含 $0) |
3 |
$* |
所有参数,作为一个字符串 | "arg1 arg2 arg3" |
$@ |
所有参数,作为独立字符串数组 | "arg1" "arg2" "arg3" |
$? |
上条命令退出状态码 | 0(成功) |
$$ |
当前 Shell 进程 PID | 12345 |
记忆技巧:
$#的#像是在数数(count),$?的?像是在问“结果如何”,$$两个$表示“我自己的身份”。
1.1 基础用法
#!/bin/bash
echo "脚本名称: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "参数总数: $#"
执行:
$ ./demo.sh hello world
echo "脚本名称: ./demo.sh"
echo "第一个参数: hello"
echo "第二个参数: world"
echo "参数总数: 2"
1.2 超过 9 个参数的陷阱
当参数超过 9 个时,必须用花括号:
#!/bin/bash
echo "第10个参数: ${10}" # ✅ 正确
echo "第10个参数: $10" # ❌ 错误!会被解析为 $1 + "0"
执行 ./test.sh a b c d e f g h i j:
${10}→j$10→a0($1的值a后面拼接字符0)
二、$* 与 $@ 的区别:Shell 面试的“送命题”
这是 Shell 脚本中最经典、也最容易让人困惑的知识点。两者的区别只在加引号时体现。
2.1 不加引号时:行为完全相同
#!/bin/bash
echo "=== 不加引号 ==="
echo "使用 \$*:"
for arg in $*; do
echo " - [$arg]"
done
echo "使用 \$@:"
for arg in $@; do
echo " - [$arg]"
done
执行 ./test.sh "hello world" foo bar:
=== 不加引号 ===
使用 $*:
- [hello]
- [world]
- [foo]
- [bar]
使用 $@:
- [hello]
- [world]
- [foo]
- [bar]
注意:包含空格的参数 "hello world" 被拆分成两个词!这是因为不加引号时,Shell 会对展开结果进行单词拆分(Word Splitting)。
2.2 加引号时:天壤之别
#!/bin/bash
echo "=== 加引号 ==="
echo "使用 \"\$*\":"
for arg in "$*"; do
echo " -> [$arg]"
done
echo "使用 \"\$@\":"
for arg in "$@"; do
echo " -> [$arg]"
done
执行 ./test.sh "hello world" foo bar:
=== 加引号 ===
使用 "$*":
-> [hello world foo bar] ← 一个字符串
使用 "$@":
-> [hello world] ← 独立参数
-> [foo]
-> [bar]
2.3 区别总结
| 形式 | 展开结果 | 说明 |
|---|---|---|
$* |
hello world foo bar |
所有参数拆分为单词 |
$@ |
hello world foo bar |
所有参数拆分为单词 |
"$*" |
"hello world foo bar" |
一个字符串,用 IFS 第一个字符连接 |
"$@" |
"hello world" "foo" "bar" |
多个独立字符串,保留原始引号信息 |
最佳实践:几乎所有情况下都应该使用
"$@",它能正确保留参数中的空格和特殊字符。
三、让人费解的案例
现在进入本文的核心——那些让人“怀疑人生”的引号与位置参数组合案例。
3.1 案例一:单引号下的 $@
#!/bin/bash
echo '不加引号: '
echo $@
echo '单引号: '
echo '$@'
echo '双引号: '
echo "$@"
执行 ./test.sh "hello world" foo:
不加引号:
hello world foo
单引号:
$@ ← 字面量!
双引号:
hello world foo
原理:单引号(')是“强引用”,会剥夺其中所有字符的特殊含义。$@ 在单引号里不会被展开,原样输出为字面字符串 $@。双引号(")是“弱引用”,允许 $ 进行参数展开。
3.2 案例二:echo ‘$@’ | xargs 的陷阱
这是最常见的坑之一:
#!/bin/bash
# 假设我们想将所有参数通过管道传给另一个命令
# 错误写法 1:
echo '$@' | xargs echo
# 输出: $@ ← 只传了字面量 "$@"
# 错误写法 2:
echo $@ | xargs echo
# 输出: hello world foo bar ← 空格被拆分了,参数边界丢失
# 错误写法 3:
echo "$@" | xargs echo
# 输出: hello world foo bar ← 所有参数合并成一个字符串
# 正确写法:
printf '%s\n' "$@" | xargs -I {} echo "处理: {}"
# 或更简单地直接传参
some_command "$@"
问题分析:
echo '$@'→ 单引号阻止展开,管道接收到的就是两个字符$和@echo $@→ 先展开为hello world foo bar,然后echo输出这个字符串,管道接收的是合并后的一个字符串echo "$@"→ 展开为hello world foo bar(一个字符串),同样丢失了参数边界printf '%s\n' "$@"→"$@"展开为多个独立参数,printf每个参数单独输出一行,管道正确传递
3.3 案例三:管道中的子 Shell 陷阱
#!/bin/bash
var="before"
echo "$var" | while read line; do
var="after"
echo "循环内: $var"
done
echo "循环外: $var"
输出:
before
循环内: after
循环外: before ← 没变!
原理:管道符 | 会创建子 Shell(Subshell),子 Shell 中对变量的修改不会影响父 Shell。这是 Shell 中另一个让人困惑的地方,与位置参数传递密切相关。
3.4 案例四:IFS 与 “$*” 的隐藏行为
#!/bin/bash
# 默认 IFS 是 "\t\n"(空格、制表符、换行)
echo "默认 IFS:"
echo "$*"
# 修改 IFS
IFS=","
echo "修改 IFS 为逗号:"
echo "$*"
# 再改回来
IFS=" "
echo "改回空格:"
echo "$*"
执行 ./test.sh a b c:
默认 IFS:
a b c
修改 IFS 为逗号:
a,b,c
改回空格:
a b c
关键点:"$*" 使用 IFS 的第一个字符作为连接符,而 "$@" 不受 IFS 影响,始终保留独立参数。
3.5 案例五:嵌套引号的“俄罗斯套娃”
#!/bin/bash
name="Alice"
# 想要输出: Alice said "hello"
echo "$name said \"hello\"" # ✅ 正确
echo '$name said "hello"' # ❌ 输出: $name said "hello"
echo "$name said 'hello'" # ✅ 正确
echo "$name said \'hello\'" # ✅ 正确
# 更复杂的嵌套
cmd="echo 'hello world'"
echo "$cmd" # 输出: echo 'hello world'
eval "$cmd" # 执行: hello world
引号规则速查:
- 单引号
':内部所有字符都是字面量,不允许嵌套单引号 - 双引号
":内部$、`、"、\保持特殊含义 - 单引号内不能放单引号,双引号内可以放单引号
3.6 案例六:shift 命令与位置参数
#!/bin/bash
echo "原始参数:"
echo " \$1=$1, \$2=$2, \$3=$3, 总数=\$#=$#"
shift
echo "shift 一次后:"
echo " \$1=$1, \$2=$2, \$3=$3, 总数=\$#=$#"
shift 2
echo "shift 两次后:"
echo " \$1=$1, \$2=$2, 总数=\$#=$#"
执行 ./test.sh apple banana cherry date:
原始参数:
$1=apple, $2=banana, $3=cherry, 总数=$#=4
shift 一次后:
$1=banana, $2=cherry, $3=date, 总数=$#=3
shift 两次后:
$1=date, $2=, 总数=$#=1
用途:shift 常用于遍历所有参数,配合 while [ $# -gt 0 ] 实现命令行选项解析。
3.7 案例七:set 命令重置位置参数
#!/bin/bash
echo "原始参数: $@"
# 用 set 重置位置参数
set -- "new arg" "another one" "third"
echo "重置后参数: $@"
echo "\$1=$1, \$2=$2, \$3=$3"
输出:
原始参数: old1 old2
重置后参数: new arg another one third
$1=new arg, $2=another one, $3=third
注意:set -- 中的 -- 是必需的,防止后续参数以 - 开头时被误解析为 set 的选项。
四、位置参数传递流程
4.1 Shell 参数解析与传递全景图
flowchart TD
A["命令行输入: 点斜杠script.sh 双引号hello world双引号 foo bar"] --> B["Shell 词法分析"]
B --> C["引号配对与识别"]
C --> D["参数拆分"]
D --> E["位置0 = 点斜杠script.sh"]
D --> F["位置1 = hello world"]
D --> G["位置2 = foo"]
D --> H["位置3 = bar"]
D --> I["参数个数 = 3"]
E --> J["脚本执行"]
F --> J
G --> J
H --> J
I --> J
J --> K{"引用方式?"}
K -->|"美元星号 或 美元at"| L["空格切分<br>hello/world/foo/bar"]
K -->|"双引号加美元星号"| M["单个字符串<br>hello world foo bar"]
K -->|"双引号加美元at"| N["独立字符串数组<br>双引号hello world双引号 双引号foo双引号 双引号bar双引号"]
L --> O["传递给命令"]
M --> O
N --> O
style A fill:#e1f5fe
style N fill:#c8e6c9
style L fill:#ffcdd2
style M fill:#fff9c4
4.2 引号展开顺序与影响
flowchart LR
A["原始命令"] --> B{"外层引号类型"}
B -->|"单引号"| C["强引用: 所有字符字面量<br>美元at → 字面量 美元at"]
B -->|"双引号"| D["弱引用: 允许美元展开<br>美元at → 参数展开"]
B -->|"无引号"| E["默认处理: 空格切分<br>美元at → 拆分后传递"]
D --> F{"内层引号?"}
F -->|"单引号在内"| G["单引号被双引号包裹 → 字面量<br>双引号内容 单引号 hi 单引号"]
F -->|"双引号在内"| H["需转义: 反斜杠双引号<br>双引号内容 反斜杠 hi 反斜杠"]
C --> I["最终传递给命令"]
E --> I
G --> I
H --> I
style C fill:#ffcdd2
style D fill:#c8e6c9
style E fill:#fff9c4
4.3 管道与子 Shell 关系图
flowchart TD
subgraph 父Shell["父 Shell 进程"]
A["变量 var=before"]
B["echo \"$@\""]
C["位置参数: $1, $2, $3"]
end
subgraph 子Shell["子 Shell 进程 (管道创建)"]
D["while read line"]
E["var=after"]
F["修改仅在子Shell生效"]
end
B -->|"管道 |"| D
D --> E
E --> F
A -.->|"var 仍为 before"| G["父Shell中 var 不变"]
style 父Shell fill:#e3f2fd
style 子Shell fill:#fce4ec
4.4 位置参数处理决策树
flowchart TD
A["需要传递所有参数?"] --> B{"参数中可能包含空格?"}
B -->|"是"| C["使用 "$@""]
B -->|"否"| D{"需要合并为一个字符串?"}
D -->|"是"| E["使用 "$*""]
D -->|"否"| F["使用 $@ 或 $* 均可"]
C --> G["通过管道?"]
G -->|"是"| H["用 printf '%s\n' "$@" | cmd"]
G -->|"否"| I["直接 cmd "$@""]
E --> J["注意 IFS 影响连接符"]
F --> K["注意 Word Splitting"]
style C fill:#c8e6c9
style H fill:#c8e6c9
style I fill:#c8e6c9
style E fill:#fff9c4
style F fill:#ffcdd2
style K fill:#ffcdd2
五、最佳实践与避坑指南
5.1 黄金法则
-
始终使用
"$@"传递参数# ✅ 正确 some_command "$@" # ❌ 错误 some_command $@ some_command $* -
变量引用始终加双引号
# ✅ 正确 echo "$filename" # ❌ 错误 echo $filename -
通过管道传递多参数时用
printf# ✅ 正确 printf '%s\n' "$@" | xargs -I {} process {} # ❌ 错误 echo "$@" | xargs process -
使用
shift遍历参数while [ $# -gt 0 ]; do echo "处理: $1" shift done -
设置严格模式
#!/usr/bin/env bash set -euo pipefail
5.2 常见错误速查表
| 错误写法 | 问题 | 正确写法 |
|---|---|---|
echo '$@' | cmd |
单引号阻止展开,只传字面量 $@ |
printf '%s\n' "$@" | cmd |
echo $@ | cmd |
Word Splitting 导致参数边界丢失 | printf '%s\n' "$@" | cmd |
echo "$@" | cmd |
所有参数合并为一个字符串 | printf '%s\n' "$@" | cmd |
$10 |
被解析为 $1 + 0 |
${10} |
for i in $*; do |
空格参数被拆分 | for i in "$@"; do |
cmd $var |
变量含空格时被拆分 | cmd "$var" |
set -a -b |
-a 被解析为选项 |
set -- -a -b |
六、总结
Shell 位置参数的传递看似简单,实则暗藏玄机。核心要点:
-
单引号 vs 双引号:单引号是“强引用”,内部一切字符都是字面量;双引号是“弱引用”,允许
$展开。echo '$@'中的$@不会被展开。 -
"$@"是唯一安全的全参数传递方式:它能保留每个参数的独立性和内部空格,是编写健壮脚本的基石。 -
管道创建子 Shell:管道符
|右侧的命令在子 Shell 中执行,对变量的修改不会反映到父 Shell。 -
Word Splitting 是隐形杀手:不加引号的变量展开会触发单词拆分,导致含空格的参数被撕裂。
-
printf '%s\n' "$@"是管道传参的标准解法:当需要将多个参数通过管道传递时,用printf每个参数单独输出一行,再配合xargs -I {}或xargs -0处理。
掌握这些规则后,那些让人“怀疑人生”的案例就不再神秘了。
原文 https://blog.csdn.net/2301_79518550/article/details/162583442