Shell 脚本中 $0、$1、$@、$*、$#、$? 等特殊变量分别是什么含义?
Shell 脚本中 $0、$1、$2、$@、$*、$#、$? 等特殊变量是处理命令行参数和控制脚本流程的核心工具。掌握它们的含义和用法,是写好 Shell 脚本的基本功,也是运维和后端面试的高频考点。
一图总览
| 变量 | 含义 | 常见用途 |
|---|---|---|
| $0 | 脚本文件名或调用路径 | 日志输出、Usage 提示 |
| $1~$n | 第 1 到第 n 个位置参数 | 接收脚本入参 |
| $# | 参数个数 | 参数校验 |
| $@ | 所有参数(各自独立) | 遍历参数 |
| $* | 所有参数(合并为一个字符串) | 拼接参数 |
| $? | 上一条命令的退出状态码 | 判断命令是否成功 |
| $$ | 当前 Shell 进程 PID | 生成临时文件 |
| $! | 最近一个后台进程的 PID | 跟踪后台任务 |
| $- | 当前 Shell 选项标志 | 调试模式检测 |
| $_ | 上一条命令的最后一个参数 | 快速复用参数 |
位置参数:$0、$1、$2、...
$0 — 脚本自身的名称
$0 保存的是调用脚本时使用的路径,不一定是脚本的真实文件名。
bash#!/bin/bash echo "I am: $0"
bash# 不同调用方式,$0 的值不同 $ ./script.sh # $0 = ./script.sh $ /tmp/script.sh # $0 = /tmp/script.sh $ bash script.sh # $0 = script.sh $ source script.sh # $0 = /bin/bash(当前 shell 名)
用 basename 可以提取纯文件名,常用于 Usage 提示:
bashecho "Usage: $(basename "$0") <file> <pattern>"
$1, $2, ..., ${10} — 位置参数
$1 是第一个参数,$2 是第二个,依此类推。第 10 个及以后的参数必须用花括号:${10}。
bash#!/bin/bash echo "First: $1" echo "Second: $2" echo "Tenth: ${10}"
bash$ ./greet.sh Alice Engineer Beijing # First: Alice # Second: Engineer
注意:如果脚本未传参,$1 为空字符串,不会报错。这在脚本中容易埋坑,建议始终做参数校验。
shift — 轮转位置参数
shift 命令将所有位置参数左移一位:$2 变成 $1,$3 变成 $2,$0 不受影响。
bash#!/bin/bash while [ $# -gt 0 ]; do echo "Processing: $1" shift done
bash$ ./loop.sh a b c # Processing: a # Processing: b # Processing: c
这在解析带参数的选项时特别有用,比如 --port 8080 --host localhost。
特殊参数:$#、$@、$*、$?
$# — 参数个数
bash#!/bin/bash if [ $# -lt 2 ]; then echo "Usage: $0 <source> <dest>" exit 1 fi echo "Copying $1 to $2..."
$# 配合参数校验是脚本健壮性的第一道防线。
$@ 和 $* — 所有参数
这是最容易混淆的一对。不加引号时两者表现相同,加了双引号后行为完全不同:
bash#!/bin/bash echo '--- Without quotes ---' for arg in $@; do echo " [$arg]" done echo '--- With quotes: "$@" ---' for arg in "$@"; do echo " [$arg]" done echo '--- With quotes: "$*" ---' for arg in "$*"; do echo " [$arg]" done
bash$ ./test.sh "hello world" foo bar # --- Without quotes --- # [hello] # [world] ← 空格被拆分了 # [foo] # [bar] # --- With quotes: "$@" --- # [hello world] ← 参数保持独立 # [foo] # [bar] # --- With quotes: "$*" --- # [hello world foo bar] ← 所有参数合并成一个字符串
核心区别一句话:"$@" 保留每个参数的独立性,"$*" 把所有参数合并为一个字符串。
"$*" 合并时用什么分隔?由 IFS(内部字段分隔符)的第一个字符决定,默认是空格。
bashIFS='|' echo "$*" # 输出: hello world|foo|bar
实际开发中,99% 的场景应该用 "$@"。只有在需要把所有参数拼接成一个字符串时才用 "$*"。
$? — 上一条命令的退出状态码
0 表示成功,非 0 表示失败。Linux 约定 1~255 为不同的错误类型。
bashmkdir /root/test 2>/dev/null if [ $? -ne 0 ]; then echo "Failed to create directory (exit code: $?)" fi
更简洁的写法是直接判断:
bashif mkdir /root/test 2>/dev/null; then echo "OK" else echo "Failed" fi
常见退出码含义:1 一般错误,2 命令用法错误,126 权限不足,127 命令未找到,128+N 信号 N 导致的退出(如 128+9=137 表示被 SIGKILL 杀掉)。
$$ 和 $! — 进程 ID
bash#!/bin/bash # $$:当前脚本的 PID,常用于生成唯一的临时文件 TMPFILE="/tmp/myapp_$$.tmp" echo "Processing..." > "$TMPFILE" # $!:最近一个后台进程的 PID sleep 60 & BG_PID=$! echo "Background task PID: $BG_PID" # 可以在需要时杀掉后台任务 # kill $BG_PID
注意:在子 shell ( ) 中,$$ 仍然是父 shell 的 PID,不是子 shell 的。如果需要子 shell 的真实 PID,用 $BASHPID。
$- 和 $_
$- 显示当前 Shell 启用的选项标志:
bashecho $- # 常见输出: himBHs # h: 记录命令历史 i: 交互式 m: 作业控制 # B: 花括号展开 H: 历史替换 s: 从 stdin 读命令
可以用来检测脚本是否在调试模式:
bashif [[ $- == *x* ]]; then echo "Debug mode is ON" fi
$_ 保存上一条命令的最后一个参数:
bashls -la /etc/passwd echo $_ # 输出: /etc/passwd mkdir -p /very/long/path/to/dir cd $_ # 快速进入刚创建的目录
常见陷阱与避坑
陷阱 1:$@ 不加引号
bash# 错误:带空格的参数会被拆分 for arg in $@; do echo "$arg" done # 正确:始终加双引号 for arg in "$@"; do echo "$arg" done
陷阱 2:$0 不可靠
$0 的值取决于调用方式,不要用它判断脚本的真实路径。获取真实路径的正确方式:
bashSCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
陷阱 3:$? 被覆盖
$? 只保存最近一条命令的状态,必须紧跟要判断的命令使用:
bash# 错误:echo 会覆盖 $? some_command echo "Result: $?" # 这里 $? 已经是 echo 的状态 if [ $? -ne 0 ]; then ... # 永远为 0 # 正确:立即保存 some_command result=$? if [ $result -ne 0 ]; then echo "Failed with code $result" fi
陷阱 4:位置参数越界
bash# 错误:$10 会被解析为 $1 后面跟字符 "0" echo $10 # 正确:用花括号 echo ${10}
综合实战脚本
bash#!/bin/bash set -euo pipefail # 参数校验 if [ $# -lt 2 ]; then echo "Usage: $(basename "$0") <dir> <pattern> [files...]" exit 1 fi SEARCH_DIR="$1" PATTERN="$2" shift 2 # 移除已处理的参数,剩余的就是文件列表 echo "=== Search Info ===" echo "Script: $(basename "$0")" echo "PID: $$" echo "Search dir: $SEARCH_DIR" echo "Pattern: $PATTERN" echo "Extra files: $*" echo "File count: $#" # 在指定目录搜索 grep -rn "$PATTERN" "$SEARCH_DIR" if [ $? -eq 0 ]; then echo "Pattern found." else echo "Pattern not found (exit code: $?)" fi # 处理额外的文件 for file in "$@"; do if [ -f "$file" ]; then echo "Processing: $file" else echo "Skip (not found): $file" fi done echo "Done."
"$@" 与 "$*" 速查对比
| 对比项 | "$@" | "$*" |
|---|---|---|
| 引号中的行为 | 每个参数独立保留 | 合并为一个字符串 |
| 空格处理 | 保持参数中的空格 | 参数间用 IFS 首字符连接 |
| 遍历结果 | 逐个参数 | 整体一次 |
| 推荐程度 | 推荐 | 仅在拼接场景使用 |
记住一条原则:遍历参数用 "$@",拼接参数用 "$*"。