变量的正确打开方式:从定义到特殊符号
脚本写多了你就会发现,变量这东西看着简单,但坑是真不少。我刚开始写 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 知识体系结构图
先上一张图,帮你把整个变量体系理清楚。后面我再逐一展开讲。
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 前缀
- 参数必校验: 用
$#做入口防护,别把错误留给下游 - 出口必检查: 每条关键命令执行后别忘
$?,日志里留状态码