// Linux · 2026-07-04

Shell 位置参数传递:从入门到“怀疑人生“

一、什么是位置参数

位置参数(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 "$@"

问题分析:

  1. echo '$@' → 单引号阻止展开,管道接收到的就是两个字符 $ 和 @
  2. echo $@ → 先展开为 hello world foo bar,然后 echo 输出这个字符串,管道接收的是合并后的一个字符串
  3. echo "$@" → 展开为 hello world foo bar(一个字符串),同样丢失了参数边界
  4. 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["使用 &#34;$@&#34;"]
    B -->|"否"| D{"需要合并为一个字符串?"}
    D -->|"是"| E["使用 &#34;$*&#34;"]
    D -->|"否"| F["使用 $@ 或 $* 均可"]
    
    C --> G["通过管道?"]
    G -->|"是"| H["用 printf '%s\n' &#34;$@&#34; | cmd"]
    G -->|"否"| I["直接 cmd &#34;$@&#34;"]
    
    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 黄金法则

  1. 始终使用 "$@" 传递参数

    # ✅ 正确
    some_command "$@"
    
    # ❌ 错误
    some_command $@
    some_command $*
  2. 变量引用始终加双引号

    # ✅ 正确
    echo "$filename"
    
    # ❌ 错误
    echo $filename
  3. 通过管道传递多参数时用 printf

    # ✅ 正确
    printf '%s\n' "$@" | xargs -I {} process {}
    
    # ❌ 错误
    echo "$@" | xargs process
  4. 使用 shift 遍历参数

    while [ $# -gt 0 ]; do
        echo "处理: $1"
        shift
    done
  5. 设置严格模式

    #!/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 位置参数的传递看似简单,实则暗藏玄机。核心要点:

  1. 单引号 vs 双引号:单引号是“强引用”,内部一切字符都是字面量;双引号是“弱引用”,允许 $ 展开。echo '$@' 中的 $@ 不会被展开。

  2. "$@" 是唯一安全的全参数传递方式:它能保留每个参数的独立性和内部空格,是编写健壮脚本的基石。

  3. 管道创建子 Shell:管道符 | 右侧的命令在子 Shell 中执行,对变量的修改不会反映到父 Shell。

  4. Word Splitting 是隐形杀手:不加引号的变量展开会触发单词拆分,导致含空格的参数被撕裂。

  5. printf '%s\n' "$@" 是管道传参的标准解法:当需要将多个参数通过管道传递时,用 printf 每个参数单独输出一行,再配合 xargs -I {} 或 xargs -0 处理。

掌握这些规则后,那些让人“怀疑人生”的案例就不再神秘了。

原文 https://blog.csdn.net/2301_79518550/article/details/162583442