Shell脚本特殊变量详解:从$?、$1到$@的实战应用

📅 2026/8/1 15:50:34
Shell脚本特殊变量详解:从$?、$1到$@的实战应用
1. Shell脚本中的“瑞士军刀”特殊变量总览如果你写过Shell脚本或者经常在终端里敲命令那你肯定遇到过像$?、$1、$#这样的符号。它们看起来神秘兮兮的像是脚本世界里的暗号。我第一次接触时也是一头雾水直到有一次写了个自动化部署脚本因为没搞懂$和$*的区别导致脚本在传递带空格的文件名时彻底崩溃把测试环境搞得一团糟才痛下决心要把这些“特殊变量”摸透。这些变量是Shell解释器内置的用来访问脚本运行时的关键信息比如你传了哪些参数、上一条命令执行成功没有、脚本自己的进程ID是多少。它们不像是你用namevalue定义的普通变量而是Shell直接赋予的、有特殊含义的“只读”状态变量。理解它们是你从“会写几行命令”到“能写出健壮、可靠脚本”的关键一步。无论你是想写个简单的备份脚本还是构建复杂的CI/CD流水线这些特殊变量都是你工具箱里不可或缺的“瑞士军刀”。2. 位置参数$n——脚本如何接收外部指令当我们执行一个脚本./deploy.sh production app-v1.2.3时脚本怎么知道我们要部署到production环境版本是app-v1.2.3呢答案就是位置参数$nn代表从1开始的数字。2.1$0,$1,$2... 的基本含义$0 代表脚本或Shell本身的名称。这是一个非常容易混淆的点。在脚本内部$0是脚本的路径。如果你以./script.sh方式执行$0就是./script.sh如果你用/home/user/scripts/script.sh执行它就是完整路径。在交互式Shell中$0通常是当前Shell的名称如-bash或/bin/bash。$1,$2,$3... 分别代表传递给脚本的第一个、第二个、第三个……参数。$1对应production$2对应app-v1.2.3。一个简单的例子#!/bin/bash # 文件名: greet.sh echo “脚本名称是: $0” echo “第一个参数是: $1” echo “第二个参数是: $2”执行./greet.sh Alice Bob输出脚本名称是: ./greet.sh 第一个参数是: Alice 第二个参数是: Bob2.2 处理第10个及以后的参数${10}这里有个经典的坑。如果你想获取第10个参数不能直接用$10。因为Shell会把$10解释为$1后面跟着一个字符0。正确的做法是使用花括号${10}、${11}以此类推。花括号的作用是明确变量名的边界。2.3 实战技巧与避坑指南参数判空是必须的 直接使用未提供的参数会导致脚本行为不可预测。务必在使用前检查。# 不安全的写法 backup_file$1 tar -czf backup.tar.gz $backup_file # 如果$1为空这条命令会变成 tar -czf backup.tar.gz含义完全不同 # 安全的写法 if [ -z “$1” ]; then echo “错误请指定要备份的文件或目录。” exit 1 fi backup_file“$1”注意[ -z “$1” ]中的双引号至关重要。如果$1包含空格不加引号会导致语法错误。shift命令参数队列的“左移” 当你需要循环处理所有参数但又不想关心具体位置时shift命令就派上用场了。它会把参数列表向左“推”一位原来的$2变成$1$3变成$2以此类推而原来的$1就被丢弃了。$#的值也会相应减1。# 循环处理所有参数的传统方法 while [ $# -gt 0 ]; do echo “正在处理参数: $1” # 对 $1 进行一些操作... shift # 将参数列表左移一位 done这在编写支持类似-f filename -v这样选项的脚本时非常有用你可以用case语句判断$1然后shift再获取下一个参数作为选项的值。3. 参数统计与聚合$#,$*,$的微妙差异当参数不止一两个时我们常常需要知道有多少个参数或者需要把所有的参数作为一个整体来操作。这时$#、$*和$就登场了尤其是后两者它们的区别是Shell脚本面试和实际踩坑的高发区。3.1$#参数数量的“计数器”$#非常简单它表示传递给脚本或函数的位置参数的数量。注意它不包含$0脚本名。#!/bin/bash echo “总共传递了 $# 个参数。”执行./count.sh a b “c d” e输出总共传递了 4 个参数。。即使“c d”是一个带空格的字符串它也被算作一个参数。3.2$*与$一字之差天壤之别这是本章节也是整个特殊变量学习的核心难点。两者都代表所有的位置参数从$1开始但它们在引号中的行为截然不同。$* 将所有参数视为一个单一的字符串。相当于“$1 $2 $3 …”。参数之间用IFSInternal Field Separator内部字段分隔符默认为空格、制表符、换行符的第一个字符连接。$ 将每个参数视为独立的、被引号包裹的字符串。相当于“$1” “$2” “$3” …。3.3 通过循环看透本质区别理论可能有点抽象我们用一个经典的例子来演示这个例子能清晰地揭示两者的不同#!/bin/bash # 文件名: diff_demo.sh echo “测试用例传递三个参数 ‘a’ ‘b’ ‘c d’” set -- a b “c d” # 使用 set 命令模拟传入参数 echo “” echo “1. 使用 \$* (不加引号):” for arg in $*; do echo “参数: [$arg]” done echo “” echo “2. 使用 \$ (不加引号):” for arg in $; do echo “参数: [$arg]” done echo “” echo “3. 使用 \“\$*\” (加双引号):” for arg in “$*”; do echo “参数: [$arg]” done echo “” echo “4. 使用 \“\$\” (加双引号):” for arg in “$”; do echo “参数: [$arg]” done执行这个脚本输出如下测试用例传递三个参数 ‘a’ ‘b’ ‘c d’ 1. 使用 $* (不加引号): 参数: [a] 参数: [b] 参数: [c] 参数: [d] 2. 使用 $ (不加引号): 参数: [a] 参数: [b] 参数: [c] 参数: [d] 3. 使用 “$*” (加双引号): 参数: [a b c d] 4. 使用 “$” (加双引号): 参数: [a] 参数: [b] 参数: [c d]分析结果第1、2种情况不加引号$*和$的行为完全一样。Shell会先对它们进行“单词拆分”“c d”被拆成了c和d两个单词。这通常不是我们想要的行为。第3种情况“$*”所有参数被合并成了一个字符串“a b c d”所以循环只执行了一次。第4种情况“$”这才是我们绝大多数情况下需要的它完美地保留了参数的原始边界a、b、“c d”被当作三个独立的参数处理带空格的c d也完好无损。3.4 黄金法则与实战应用黄金法则在需要将参数列表原封不动地传递给其他命令或进行循环时永远使用“$”。场景一包装脚本你想写一个脚本在运行某个命令前后做一些日志记录。#!/bin/bash # wrapper.sh echo “[$(date)] 开始执行命令: $” /var/log/myapp.log # 使用 “$” 将接收到的所有参数原样传递给原命令 “$” exit_code$? echo “[$(date)] 命令执行结束退出码: $exit_code” /var/log/myapp.log exit $exit_code你可以这样用./wrapper.sh cp -r “/path with spaces/source” /dest/。“$”确保了包含空格的路径参数不会被错误拆分。场景二遍历所有参数安全地处理用户输入的所有文件。for file in “$”; do if [ ! -f “$file” ]; then echo “警告: $file 不是普通文件跳过。” continue fi echo “正在处理文件: $file” # ... 你的处理逻辑 done4. 状态与进程$?和$$——脚本的“体检报告”与“身份证”脚本不仅要干活还得知道自己干得怎么样以及自己在系统里的“身份”。$?和$$就是用来获取这些信息的。4.1$?上一条命令的“退出状态码”在Unix/Linux哲学中一个程序执行完毕后应该告诉调用者它是否成功。这个信号就是退出状态码。$?变量保存了刚刚执行结束的那条命令的退出状态码。0 表示成功。这是几乎所有Unix工具遵循的约定。非0 表示失败。不同的非零值通常代表不同的错误类型例如1表示通用错误2表示误用shell命令127表示命令未找到。4.2$?的实战应用错误处理与流程控制这是编写健壮脚本的基石。立即检查原则$?的值非常“脆弱”它只代表上一条命令。如果你在执行command后又执行了echo “Done”那么$?就变成了echo命令的退出码通常是0。因此检查必须紧跟在你关心的命令之后。grep “error” /var/log/syslog if [ $? -eq 0 ]; then echo “系统日志中发现错误信息需要关注” # 发送警报邮件等... else echo “系统日志检查正常。” fi短路操作符和|| 这是利用退出状态的更简洁写法。command1 command2 只有command1成功返回0才执行command2。command1 || command2 只有command1失败返回非0才执行command2。# 如果创建目录成功则进入目录 mkdir /backup/$(date %Y%m%d) cd /backup/$(date %Y%m%d) # 如果ping不通网关则记录错误 ping -c 1 192.168.1.1 /dev/null 21 || echo “$(date): 网络不可达” /tmp/network.log在函数中返回状态 函数中return语句的参数就是该函数的退出状态码可以被调用者通过$?捕获。is_file_exist() { if [ -f “$1” ]; then return 0 # 成功文件存在 else return 1 # 失败文件不存在 fi } is_file_exist “/etc/passwd” if [ $? -eq 0 ]; then echo “文件存在。” fi4.3$$当前Shell进程的ID$$展开为当前Shell脚本进程的PID。这个变量在需要生成唯一临时文件、进行进程管理或记录日志时非常有用。生成唯一临时文件 避免多个脚本实例或并行任务产生文件名冲突。# 创建一个只属于本进程的临时文件 TEMP_LOG“/tmp/myscript_$$.log” echo “进程 $$ 开始运行” “$TEMP_LOG” # ... 脚本逻辑 # 脚本结束时可以清理 rm -f “$TEMP_LOG”在日志中标识进程 当多个脚本实例同时运行时通过PID可以清晰地区分日志来源。log() { echo “[$(date %Y-%m-%d %H:%M:%S)] [PID:$$] $1” /var/log/myapp.log } log “开始执行数据同步任务。”进程管理 你可以用kill命令向$$发送信号或者在脚本中保存自己的PID以便其他进程与之通信。# 脚本启动时将自己的PID写入文件方便管理 echo $$ /var/run/myscript.pid # 在脚本末尾或收到退出信号时删除这个文件 trap “rm -f /var/run/myscript.pid” EXIT5. 进阶变量与实战综合案例除了上述最常用的几个Shell还有一些其他有用的特殊变量它们在特定场景下能发挥关键作用。5.1$!最后一个后台运行进程的PID当你用将一个命令放到后台执行时$!就保存了那个进程的PID。这常用于启动一个后台服务然后记录其PID以便后续管理。redis-server --daemonize yes REDIS_PID$! echo “Redis 已启动PID: $REDIS_PID” # 稍后如果需要可以用 kill $REDIS_PID 来停止它5.2$_上一个命令的最后一个参数在交互式Shell中$_非常方便。比如你刚创建了一个长路径的目录mkdir -p /a/very/long/path/to/somewhere接下来想立刻进入这个目录只需要输入cd $_即可。5.3 综合实战一个健壮的备份脚本让我们把学到的所有知识融会贯通写一个实用的、带错误处理和日志的目录备份脚本。#!/bin/bash # 文件名: robust_backup.sh # 描述一个健壮的目录备份脚本演示特殊变量的综合运用 # 使用 “$” 获取所有参数第一个参数是源目录其余的是排除项 BACKUP_SOURCE“$1” shift # 移除第一个参数剩下的 ($) 全是排除模式 EXCLUDE_PATTERNS(“$”) # 将剩余的排除模式存入数组 # 1. 参数检查 ($#, $?) if [ $# -lt 1 ]; then # 注意此时 $# 已经 shift 过一次代表排除模式的数量 echo “用法: $0 源目录 [排除模式1 排除模式2 …]” echo “示例: $0 /home/user/project ‘*.log’ ‘tmp/’” exit 1 fi if [ ! -d “$BACKUP_SOURCE” ]; then echo “错误源目录 ‘$BACKUP_SOURCE’ 不存在或不是一个目录。” 2 exit 2 fi # 2. 生成带日期和PID的唯一备份文件名 (使用 $$) BACKUP_NAME“$(basename “$BACKUP_SOURCE”)_$(date %Y%m%d_%H%M%S)_$$.tar.gz” BACKUP_PATH“/backups/$BACKUP_NAME” # 3. 构建 tar 命令处理排除项 TAR_CMD(tar -czf “$BACKUP_PATH” -C “$(dirname “$BACKUP_SOURCE”)” “$(basename “$BACKUP_SOURCE”)”) for pattern in “${EXCLUDE_PATTERNS[]}”; do TAR_CMD(--exclude“$pattern”) done echo “[$(date)] [PID:$$] 开始备份 ‘$BACKUP_SOURCE’ 到 ‘$BACKUP_PATH’…” echo “排除模式: ${EXCLUDE_PATTERNS[*]}” # 4. 执行备份并检查状态 ($?) if “${TAR_CMD[]}” 2/tmp/backup_error_$$.log; then BACKUP_STATUS$? echo “[$(date)] [PID:$$] 备份成功完成退出码: $BACKUP_STATUS” # 计算备份文件大小 if [ -f “$BACKUP_PATH” ]; then FILESIZE$(du -h “$BACKUP_PATH” | cut -f1) echo “备份文件大小: $FILESIZE” fi else BACKUP_STATUS$? echo “[$(date)] [PID:$$] 备份失败退出码: $BACKUP_STATUS” 2 echo “错误信息:” 2 cat /tmp/backup_error_$$.log 2 fi # 5. 清理临时文件 rm -f /tmp/backup_error_$$.log # 6. 根据备份状态决定脚本最终退出码 exit $BACKUP_STATUS这个脚本展示了使用“$1”和shift结合“$”来灵活处理主参数和可变数量的选项参数。使用$$创建进程唯一的临时日志文件避免冲突。在关键命令tar执行后立即检查$?并根据结果进行成功/失败的分支处理。使用$0在错误信息中输出脚本名。脚本最后使用exit $BACKUP_STATUS将内部命令的执行状态“传递”出去使得调用这个脚本的上层程序也能知道备份是否成功。掌握这些特殊变量就像拿到了Shell脚本编程的底层钥匙。它们让你能精确地控制脚本的输入、感知命令的状态、管理进程的身份从而写出能应对各种复杂场景的、真正可靠的生产级脚本。最开始可能会觉得有些绕尤其是$*和$但多写几次多在真实场景中踩踩坑你就会发现它们的设计是如此的精妙和实用。