Shell 脚本入门:变量、引号、条件与退出码
前 14 章的命令都是"敲一次执行一次"。把它们写进文件、加上变量和判断,就成了可复用的脚本——部署、备份、巡检的自动化起点。Shell 脚本语法有不少历史包袱(空格敏感、引号玄学),本章把最容易踩的坑一次讲透。
1. 第一个脚本
#!/bin/bash
# deploy.sh —— 第一行 shebang 声明用哪个解释器执行
echo "deploying..."三步跑起来:
chmod +x deploy.sh # 加执行权限(一次即可)
./deploy.sh # 通过 shebang 指定的 bash 执行
bash deploy.sh # 或者显式交给 bash(不需要 x 权限)ℹ️为什么是 ./deploy.sh 而不是 deploy.sh
直接敲 deploy.sh,Shell 只去 PATH 列表里找,不搜当前目录(安全设计:防止进入恶意目录后敲 ls 却执行了假 ls)。./ 明确表示"就是当前目录这个文件"。
2. 变量:赋值与使用
NAME=ada # 赋值:= 两边绝不能有空格!
PORT=8080
echo $NAME # 使用:加 $ 前缀
echo "$NAME" # 更稳妥:加双引号(后面讲为什么)三条铁律:
NAME = ada是错的——Shell 会把 NAME 当命令、= 当参数;- 变量默认是字符串,没有类型;算术要用
$(( )):NEXT=$((PORT + 1)); - 命令的输出赋给变量用
$(命令)(命令替换):TODAY=$(date +%F)。
NAME=ada
PORT=8080
echo "hello, $NAME! port=$PORT"
# 算术:$(( ))
NEXT=$((PORT + 1))
echo "next port: $NEXT"
# 命令替换:把命令输出装进变量
TODAY=$(date +%F)
FILES=$(ls / | wc -l)
echo "today is $TODAY, / has $FILES entries"
# 未定义的变量默认是空串,且不报错——这是很多事故的根源
echo "undefined: [$NOT_SET]"
# 环境变量:export 后子进程才能看到
export APP_ENV=prod
bash -c 'echo "child sees: $APP_ENV"'3. 引号三兄弟:脚本可靠性的分水岭
| 写法 | 行为 |
|---|---|
'单引号' | 内容完全原样,$ 也不展开 |
"双引号" | 展开 $变量 和 $(命令),但保护空格与通配符 |
| 不加引号 | 展开一切,且按空格拆词、通配符展开——事故高发区 |
为什么"变量要用双引号包住"是铁律:
FILE='my report.txt'
touch "$FILE"
echo '$ ls -l $FILE # 不加引号:被拆成 my 和 report.txt 两个参数'
ls -l $FILE 2>&1 | head -n 2
echo ''
echo '$ ls -l "$FILE" # 加引号:一个完整参数'
ls -l "$FILE"
echo ''
echo '$ 单引号 vs 双引号:'
NAME=ada
echo '单引号: $NAME 原样输出'
echo "双引号: $NAME 被展开"
echo ''
echo '$ 通配符也会在不加引号时抢跑:'
MSG='files: *'
echo $MSG
echo "$MSG"结论先背再理解:脚本里的变量引用,默认写 "$VAR",只有明确需要拆词/通配时才裸写。
4. 退出码:命令的成败信号
每个命令结束都返回退出码:0 成功、非 0 失败(1-255,各命令自定含义)。$? 保存上一条命令的退出码:
grep -q root /etc/passwd
echo $? # 0:找到了
grep -q xyz /etc/passwd
echo $? # 1:没找到
ls /nope
echo $? # 非 0:出错退出码驱动着两个短路操作符——它们是脚本和命令行里最常用的"迷你 if":
mkdir -p /data && cd /data # && :前面成功才执行后面
command -v jq || echo '缺 jq' # || :前面失败才执行后面
./backup.sh && echo OK || echo FAILED # 组合(注意不完全等价 if-else,够日用)5. 条件判断:if 与 test
if [ "$PORT" -eq 8080 ]; then
echo 'default port'
elif [ "$PORT" -gt 1024 ]; then
echo 'user port'
else
echo 'privileged port'
fi[ 其实是一个命令(test 的别名),所以方括号内侧必须有空格、变量必须加引号。常用判断:
| 判断 | 写法 |
|---|---|
| 字符串相等 / 非空 | [ "$A" = "$B" ] / [ -n "$A" ] |
| 数字比较 | -eq -ne -gt -lt -ge -le |
| 文件存在 / 是目录 | [ -f path ] / [ -d path ] |
| 文件可读 / 可执行 | [ -r path ] / [ -x path ] |
| 取反 / 与 / 或 | [ ! -f p ]、[ c1 ] && [ c2 ] |
bash 还提供增强版 [[ ]]:内部不拆词(变量可以不加引号)、支持 == 通配匹配和 =~ 正则。写 bash 专用脚本时推荐 [[ ]],但要理解老脚本里满地的 [ ]。
cat > check.sh << 'EOF'
#!/bin/bash
CONF=$1
if [ -z "$CONF" ]; then
echo "usage: check.sh <config-file>"
exit 1
fi
if [ ! -f "$CONF" ]; then
echo "FAIL: $CONF 不存在"
exit 2
fi
if grep -q 'env = prod' "$CONF"; then
echo "OK: 生产配置确认"
else
echo "WARN: 不是生产配置"
fi
LINES=$(wc -l < "$CONF")
if [ "$LINES" -gt 100 ]; then
echo "WARN: 配置超过 100 行,建议拆分"
fi
echo "check passed"
EOF
chmod +x check.sh
echo '$ 不带参数运行:'
./check.sh
echo "exit code = $?"
echo ''
echo '$ 指向不存在的文件:'
./check.sh missing.conf
echo "exit code = $?"
echo ''
printf 'env = prod\nport = 80\n' > app.conf
echo '$ 正常运行:'
./check.sh app.conf
echo "exit code = $?"注意脚本中的几个细节:$1 是第一个命令行参数(下一章详讲);exit N 主动结束脚本并设定退出码——不同错误用不同退出码,调用方才能区分失败原因;grep -q 静默模式不输出只给退出码,专为条件判断而生。
⚠️新手四大高频报错对照
command not found,而变量名没拼错 → 八成是NAME = value加了空格;unary operator expected→[ $VAR = x ]里 VAR 为空导致语法残缺,给变量加双引号即愈;Permission denied→ 忘了 chmod +x,或分区挂载了 noexec;bad interpreter→ 脚本是 Windows 换行符(CRLF),用sed -i 's/\r$//' file或 dos2unix 修复。
小结
- 脚本三件套:shebang
#!/bin/bash、chmod +x、./运行 - 变量:
=两侧无空格、$(cmd)命令替换、$(( ))算术、export 传给子进程 - 引号铁律:变量引用默认
"$VAR";单引号原样、双引号展开、裸奔拆词 - 退出码 0 为成功;
$?查看;&&/||短路串联;exit N区分错误 [ ]是命令(内侧留空格),bash 脚本可用更强的[[ ]];grep -q配 if 是常用素材- 下一章:循环、函数与更健壮的脚本 →
🎯练习
- 写脚本
sysinfo.sh:用命令替换收集主机名、内核版本(uname -r)、当前负载(/proc/loadavg 第一列),按key: value格式输出三行。 - 写脚本接收一个路径参数:参数为空输出用法并 exit 1;是目录则统计里面文件数;是普通文件则输出行数;都不是则报错 exit 2。用不同输入验证各分支的
$?。 - 用 && 与 || 写一行命令:目录 /tmp/build 存在就删除重建,不存在就直接创建,最后输出 ready(提示:可以组合 rm -rf 与 mkdir -p,思考更简单的等价写法)。
- 复现引号事故:创建带空格的文件名,分别用
rm $F和rm "$F"删除,观察前者的报错并解释拆词过程。