变量的正确打开方式:从定义到特殊符号

脚本写多了你就会发现,变量这东西看着简单,但坑是真不少。我刚开始写 Shell 那会儿,总觉得变量不就是存个值嘛,后来被坑过几次才明白——变量用得好不好,直接决定了脚本能不能扛住生产环境。今天咱们就把变量这块彻底聊透。

3.1 变量定义:记好这三条就够了

Shell 里定义变量,说白了就一个等号。但这里边有几个硬规矩,踩过一次你就忘不了。

变量命名规范

  • 只能包含字母、数字、下划线
  • 不能以数字开头
  • 大小写敏感 — $name 和 $Name 是两个东西
  • 习惯上:环境变量全大写,本地变量小写或驼峰
# 正确姿势
user_name="alice"
log_dir="/var/log/myapp"
MAX_RETRY=5

# 踩雷示范
2user="bob"       # 数字开头 → 报错
user-name="bob"   # 横线会被当成减号 → 出大问题
user name="bob"   # 空格是分隔符 → 语法错误

赋值时等号两边不能有空格。我把这个写进了团队的编码规范里,但新来的同事几乎都会犯一次这个错。

3.2 变量类型:三类核心变量各司其职

Shell 变量其实就三大类:本地变量、环境变量、位置参数。搞清楚它们的区别,脚本的健壮性至少提升一个档次。

类型 作用域 生命周期 常见场景
本地变量 当前 shell 进程 会话结束或 unset 中间计算结果、临时计数
环境变量 当前 shell 及其子进程 会话结束或 unset PATH、HOME、自定义配置
位置参数 脚本/函数内部 脚本/函数执行结束 传递命令行参数

本地变量:最常用的临时工

本地变量说白了就是只在当前 shell 会话里有效。export 之后它才会变成环境变量,传给子进程。

count=0              # 本地变量
echo $count          # 输出 0
bash -c 'echo $count'  # 子进程看不到 → 输出空

实战技巧: 我写脚本的时候,所有临时计算值都用本地变量。这能防止意外污染外部环境,尤其是在 source 别的脚本时,这点很重要。

环境变量:父子进程的传话筒

父进程设了环境变量,子进程就能拿到。这就是为什么 export PATH 之后,所有子命令都能找到你的二进制文件。

export APP_HOME="/opt/myapp"
export JAVA_HOME="/usr/lib/jvm/java-11"

# 子进程直接读取
bash -c 'echo $APP_HOME'  # 输出 /opt/myapp

踩坑实录: 我曾经在生产脚本里犯过一个错误 — 没 export 一个数据库连接字符串,结果子进程拿不到变量,连接失败了三个小时。从那以后,所有配给别的进程用的变量,我都会第一时间 export。

位置参数:脚本的接收器

位置参数就是脚本后面跟的那一串参数。写自动化工具的时候,这个是用得最多的。

#!/bin/bash
echo "脚本名字: $0"
echo "第一个参数: $1"
echo "第二个参数: $2"
echo "参数总数: $#"
echo "所有参数: $@"

你运行 ./check_logs.sh /var/log/app error,它就会输出路径和关键词。我在巡检脚本里几乎每天都会用到这类变量。

3.3 特殊变量:$?、$#、$@ 到底怎么用

特殊变量是 Shell 给你留的一组快捷方式。一开始看着有点懵,但用熟了之后,你会觉得它就像脚本编程的瑞士军刀。

变量 含义 典型用法
$? 上一条命令的退出状态码 检查命令是否执行成功
$# 位置参数的个数 验证用户是否传了足够参数
$@ 所有位置参数字符串列表 遍历所有传入参数
$* 所有位置参数字符串整体 参数拼接时使用
$$ 当前 shell 进程 PID 生成临时文件名、进程管理
$0 当前脚本的文件名 帮助信息、日志标记

$?:命令执行的状态监视器

0 表示成功,非 0 表示失败。这是一个基础但极其强大的判断工具。

grep "ERROR" /var/log/app.log
if [ $? -eq 0 ]; then
    echo "发现错误日志!"
else
    echo "日志干净,一切正常"
fi

好习惯: 我建议每一行关键命令执行后都做 $? 判断,尤其是文件操作和网络调用。别问我为什么要建议这个 — 我见过因为 rm 删除失败但没检查状态码,导致后续逻辑全部跑飞的场景。

$#:参数数量的守护者

写巡检脚本时,这个变量能帮你挡住大量低级错误。

if [ $# -lt 2 ]; then
    echo "用法: $0 <日志目录> <关键字>"
    exit 1
fi

$@ vs $*:看似一样的两个兄弟

区别其实在于引号下的表现。$@ 会保留参数的原样分割,$* 会把它们粘成一个字符串。

#!/bin/bash
echo "--- \$@ ---"
for arg in "$@"; do
    echo "[$arg]"
done

echo "--- \$* ---"
for arg in "$*"; do
    echo "[$arg]"
done

如果你传参 ./test.sh "hello world" foo,$@ 输出两个元素,$* 输出一个。这个区别在日志巡检场景里特别重要 — 有时你需要保留带空格的路径参数原样。

3.4 知识体系结构图

先上一张图,帮你把整个变量体系理清楚。后面我再逐一展开讲。

🟢 Shell 变量知识体系框架 Shell 变量 本地变量 作用域 生命周期 环境变量 export 导出 子进程继承 位置参数 $0 $1 $2 … $# $@ $* 特殊变量 $? 退出码 $$ 当前PID $- 选项旗 💡 分清作用域 处理好父子关系

3.5 实战复盘:巡检脚本里的变量设计

拿一个典型的磁盘巡检脚本来说,变量设计是这样的:

#!/bin/bash

# 本地变量 — 脚本内部用,不对外暴露
LOG_DIR="${1:-/var/log/disk}"
THRESHOLD="${2:-80}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# 环境变量 — 给子进程用
export ALERT_EMAIL="ops@company.com"
export MAIL_CMD="/usr/bin/mail"

# 位置参数检查
if [ $# -lt 1 ]; then
    echo "用法: $0 [日志目录] [阈值]"
    echo "示例: $0 /var/log 85"
    exit 1
fi

你想想看,如果脚本里混用变量类型,子进程拿不到配置值,参数传错也没人检查——那这脚本跑在生产上,早晚要出事。

避坑指南: 我遇到过最典型的一个坑是这样的:运维写了一个巡检脚本,把日志目录配成了环境变量 export LOG_DIR。结果另一个脚本也引用了同名的本地变量,值不一样。两个脚本 source 到一起时,LOG_DIR 被覆盖,巡检日志写到了错误的目录,导致关键告警没有触发。嗯,后来我们统一规定:全局配置用全大写环境变量,脚本内部用驼峰本地变量,命名空间彻底分开。

3.6 小结:变量设计是脚本架构的第一步

变量的定义方式、作用域选择、特殊符号的使用习惯,直接决定了脚本是否好读、好改、好维护。你去看那些流传多年的优秀开源脚本,变量设计都非常清晰——每条命名都有道理,每个 export 都有考量。

我这里给你三个马上能用的实践建议:

  • 统一命名风格: 环境变量全大写,本地变量小写,函数内变量加 local 前缀
  • 参数必校验: 用 $# 做入口防护,别把错误留给下游
  • 出口必检查: 每条关键命令执行后别忘 $?,日志里留状态码