写 Shell 脚本最容易翻车的地方,不是记不住if和then的顺序,也不是分号该打在哪儿,而是很多人脑子里还停留在其他编程语言的思维方式里。我见过不少写了半年 Shell 的同学,一直在用if [ $? -eq 0 ]判断上一条命令成不成功;还有人想逐行统计日志,结果 while 循环里加的计数器到外面就归零了。这些现象,根源基本都在 Shell 流程控制的底层规则没理顺。
这篇内容就围绕 if-else、for/while 循环、case 语句展开,把条件判断、分支选择、循环控制的执行逻辑和典型坑点一次性讲透。我不打算让你背语法,而是想说明白:每条语法背后 Shell 实际做了什么,遇到异常输出时它到底是怎么走过来的。刚接触 Linux 脚本的新手可以通篇读,已经写过不少脚本但经常被“诡异现象”卡住的朋友,重点看每个小节后面的坑位提示,尤其是最后一部分的复盘清单,都是我在真实脚本里踩过或者救过火的案例。
1. Shell 的“真假”不是布尔值,是退出码
1.1 if 后面到底该放什么
如果你从 Python、Go 这类语言转过来,写条件判断时很容易浮现出 true/false 的概念,然后写类似if [ "$a" == 1 ]的代码。看到这段代码你会默认:中括号内部是一个表达式,表达式的结果是布尔值。
但 Shell 不是这样运作的。Shell 里的if不看布尔值,它看的是退出码:0 表示真,非 0 表示假。几乎所有命令执行完都会返回一个退出码,命令成功返回 0,失败返回非 0。if后面跟的其实是一串命令,它只管这串命令最终返回的退出码是多少。
刚开始你可能觉得这没什么,但它能解开很多困惑。比如最常见的“判断文件中是否包含某行”:
if grep -q "error" /var/log/app.log; then echo "日志里出现了 error 关键字" figrep找到匹配内容时返回 0,if 条件成立;找不到时返回 1,if 条件不成立。你没有写任何中括号,也不需要把 grep 的输出保存起来再判断,直接用命令本身作为条件。
同理也适用于cd:
if cd /data/backup; then echo "目录切换成功" ficd成功进入目录返回 0,目录不存在返回非 0。这种用法比写if [ $? -eq 0 ]干净得多,也安全得多。原因很简单:$?是一种“易失状态”,只要中间多执行了一条命令,它就被覆盖了。比如下面这种写法就很容易出隐蔽问题:
grep -q "error" file.log echo "grep 执行完了" if [ $? -eq 0 ]; then echo "找到了 error" fi第二条echo已经成功执行并返回 0,所以$?被覆盖成了 0,导致 if 判断结果完全不可信。直接用命令退出码当条件,从根本上绕开这个坑。
1.2 需要计算条件时,test 和 [ ] 才登场
刚才说 if 后面跟命令,那条“命令”可以是普通命令,也可以是一条专门做条件判断的命令,也就是test。test命令专门用来比较文件、字符串和数字,然后返回退出码。[ ]写法本质上是test的另一种形式,只不过要求最后一个参数必须是]。
# 等价 if test -f /etc/hosts; then echo "/etc/hosts 存在" fi if [ -f /etc/hosts ]; then echo "/etc/hosts 存在" fi这解释了一个新手上路必踩的语法错误:中括号内部必须保留空格。
if [-f /etc/hosts]; then # 错误,Shell 找不到一个叫 [-f 的命令 if [ -f /etc/hosts ]; then # 正确[本身是命令名,不是语法符号,所以它和后面的参数之间必须有空格分隔。很多编辑器会对这行标红,但初看的人不一定反应过来是空格问题。
日常最常用的是下面几类条件:
| 条件 | 含义 | 典型判断目标 |
|---|---|---|
[ -f "$file" ] | 是普通文件 | 配置文件是否存在 |
[ -d "$dir" ] | 是目录 | 目标目录是否存在 |
[ -e "$path" ] | 文件或目录存在 | 通用存在性判断 |
[ -z "$var" ] | 字符串长度为 0 | 参数是否为空 |
[ -n "$var" ] | 字符串长度不为 0 | 参数是否有值 |
[ "$a" = "$b" ] | 字符串相等 | 比较用户输入、状态值 |
[ "$a" != "$b" ] | 字符串不相等 | 判断非预期状态 |
[ "$n" -eq 10 ] | 数字等于 | 比较数量 |
[ "$n" -gt 10 ] | 数字大于 | 阈值判断 |
[ "$n" -lt 10 ] | 数字小于 | 阈值判断 |
注意看表格里的变量我都加了双引号。这不算谨慎过度,而是必需操作。空变量如果不加引号,展开后相当于那对引号消失了。比如变量a是空的,[ $a = "x" ]展开后变成[ = "x" ],Shell 会直接报unary operator expected语法错误。加上双引号后,空变量会展开成一个空参数,条件只是不成立而已,不会让脚本崩掉。
1.3 [ ] 和 [[ ]] 有什么区别
Bash 里还有一种[[ ]]写法,它和[ ]在外观上很像,但本质不同:[[ ]]不是命令,而是 Bash 的关键字。它支持的语法更丰富,比如内部可以直接用&&、||,可以用==做通配符匹配,还能用=~做正则匹配。写成if [[ $a == *.txt ]],不需要加引号也不会因为空变量崩掉,体验确实好很多。
但代价是兼容性。[[ ]]是 Bash、Zsh、Ksh 的扩展语法,严格遵循 POSIX 的/bin/sh不认它。Debian/Ubuntu 上/bin/sh实际是 dash,很多服务器脚本开头写#!/bin/sh,里面却用了[[ ]],一到目标机器就报语法错误。所以我建议:
- 脚本开头是
#!/usr/bin/env bash,用[[ ]]没问题。 - 脚本需要在多种精简系统、容器镜像里跑,或者必须用
sh执行,那就老老实实用[ ]。 - 需要做正则或通配符匹配的场景,优先确保脚本跑在 Bash 环境,用
[[ ]]最省事。
2. if-else 不只有一种形态,简单到复杂按场景选
2.1 单分支、双分支、多分支的完整结构
最基础的三段式结构是:
if [ "$1" = "start" ]; then echo "启动" elif [ "$1" = "stop" ]; then echo "停止" else echo "未知参数" fi注意三件事:then跟前一行同一行时,前一行末尾必须有分号;elif不是缩进美化,而是“else if”的合并写法;整个 if 块必须以fi结束。新手经常犯的一个错误是想用else if而不是elif。else if也能写,但它等于在 else 分支里嵌套了一层新 if,需要额外补一个fi,很容易搞混层级。如果你的分支超过三个,用elif永远比手动嵌套清晰。
判断参数数量和变量是否为空是另一个高频场景。比如一个脚本要求至少传两个参数:
if [ "$#" -lt 2 ]; then echo "用法: $0 <主机名> <环境名>" >&2 exit 1 fi if [ -z "$2" ]; then echo "环境名不能为空" >&2 exit 1 fi$#表示参数个数,-lt 2表示小于 2。把错误信息输出到标准错误>&2并设置非 0 退出码是脚本规范里很重要的习惯,后续用脚本做自动化时,这个退出码会成为上层调度程序判断成败的依据。
2.2 命令组合和取反逻辑
Shell 里的 if 条件可以叠加逻辑运算符。POSIX 环境里最稳的组合方式是把多个[ ]用&&或||连起来:
if [ -f "$config" ] && [ -r "$config" ]; then echo "配置文件存在且可读" fi if [ ! -d "$backup_dir" ]; then echo "备份目录不存在,立即创建" mkdir -p "$backup_dir" fi注意[ ]内部不能直接写&&,比如[ -f "$config" && -r "$config" ]是错的,因为[是个命令,它不认识这种参数组合。旧脚本里有人用-a和-o表示与和或,例如[ -f "$config" -a -r "$config" ],这种写法在部分环境下还能工作,但可移植性不好,建议抛弃。Bash 的[[ ]]则没有这个限制,可以直接写:
if [[ -f "$config" && -r "$config" ]]; then echo "配置文件存在且可读" fi!可以放在 if 后面或测试条件前面取反。
if ! grep -q "error" /var/log/app.log; then echo "日志干净,没有 error" fi这里同样不要写成先 grep 再判断$?的形式,没必要给自己留一个状态被覆盖的隐患。
2.3 用算术判断替代一堆文本比较
Bash 里(( ))做算术求值非常顺手,它同样可以作为 if 的条件。(( ))里的表达式结果如果是 0,退出码是 1,条件不成立;结果不是 0,退出码是 0,条件成立。
num=10 if (( num % 2 == 0 )); then echo "$num 是偶数" fi系统磁盘、内存这类带阈值的判断用算术表达式写起来很直观:
usage=$(df /data | awk 'NR==2 {print $5}' | tr -d '%') if (( usage > 80 )); then echo "磁盘使用率已达 ${usage}%,注意清理" else echo "磁盘状态正常,当前使用率 ${usage}%" fi这比写一长串[ "$usage" -gt 80 ]可读性好。注意不要在不