目录
[二、if-else 语法完整回顾](#二、if-else 语法完整回顾)
[2.1 三种基本形态](#2.1 三种基本形态)
[2.2 单行写法(命令式风格)](#2.2 单行写法(命令式风格))
[2.3 嵌套if:多层条件判断](#2.3 嵌套if:多层条件判断)
[2.4 if-else的适用场景](#2.4 if-else的适用场景)
[3.1 基本语法](#3.1 基本语法)
[3.2 用case重写服务管理脚本](#3.2 用case重写服务管理脚本)
[3.3 case支持通配符模式](#3.3 case支持通配符模式)
[3.4 用 | 合并多个匹配项](#3.4 用 | 合并多个匹配项)
[3.5 case运用场景总结](#3.5 case运用场景总结)
一、引言:当if-else不够用了
先看一个需求:写一个脚本,根据用户输入的第一个参数执行不同操作:
bash
./script.sh start → 启动服务
./script.sh stop → 停止服务
./script.sh restart → 重启服务
./script.sh status → 查看状态
用if-elif-else实现:
bash
#!/bin/bash
if [[ "$1" = "start" ]]; then
echo "启动服务..."
elif [[ "$1" = "stop" ]]; then
echo "停止服务..."
elif [[ "$1" = "restart" ]]; then
echo "重启服务..."
elif [[ "$1" = "status" ]]; then
echo "查看状态..."
else
echo "用法: $0 {start|stop|restart|status}"
fi
功能没问题,但每增加一个选项就要加一段elif,代码冗长。这时就该case语句登场了。
二、if-else 语法完整回顾
2.1 三种基本形态
bash
# 形态一:单分支
if 条件; then
命令
fi
# 形态二:双分支
if 条件; then
命令
else
命令
fi
# 形态三:多分支
if 条件1; then
命令1
elif 条件2; then
命令2
elif 条件3; then
命令3
else
命令4
fi
2.2 单行写法(命令式风格)
bash
# 只关心"条件成立时执行"
[[ -f /etc/nginx/nginx.conf ]] && echo "配置文件存在"
# 只关心"条件不成立时执行"
[[ -f /etc/nginx/nginx.conf ]] || { echo "配置文件缺失"; exit 1; }
# 三元表达式模拟
[[ $USER = "root" ]] && prefix="#" || prefix="$"
echo "${prefix} 当前用户是 ${USER}"
2.3 嵌套if:多层条件判断
bash
#!/bin/bash
if [[ -d "$1" ]]; then
if [[ -r "$1" ]] && [[ -x "$1" ]]; then
echo "目录可访问"
else
echo "目录存在但权限不足"
fi
else
echo "目录不存在"
fi
2.4 if-else的适用场景
-
条件之间有层级关系(先判断A,再判断B,且B是A的子集)
-
条件逻辑是非此即彼(成功/失败、存在/不存在)
-
分支数量不超过3个 时可用
if-elif
三、case语句:多分支匹配的利器
3.1 基本语法
bash
case 变量 in
模式1)
命令1
;;
模式2)
命令2
;;
模式3|模式4|模式5)
命令3 # 匹配多个模式,任一个成立都执行
;;
*)
默认命令
;;
esac
语法要点:
-
case和esac成对(esac是case的反写) -
每个分支必须以
;;结束(部分Bash也支持;;&和;&) -
*是默认分支(类似else),必须放在最后 -
|用来在一个分支中匹配多个模式
3.2 用case重写服务管理脚本
bash
#!/bin/bash
case "$1" in
start)
echo "启动服务..."
;;
stop)
echo "停止服务..."
;;
restart)
echo "重启服务..."
;;
status)
echo "查看状态..."
;;
*)
echo "用法: $0 {start|stop|restart|status}"
exit 1
;;
esac
相比if-elif的版本,case的优势很明显:
-
不需要反复写
"$1" = "xxx" -
视觉上更清晰,一目了然四个分支
-
多个匹配项可以用
|合并(见3.4节)
3.3 case支持通配符模式
case的匹配不是简单的字符串比较,而是模式匹配(通配符风格):
bash
#!/bin/bash
read -p "请输入一个字符: " char
case "$char" in
[0-9]) echo "你输入了一个数字" ;;
[a-z]) echo "你输入了一个小写字母" ;;
[A-Z]) echo "你输入了一个大写字母" ;;
?) echo "你输入了一个其他单个字符" ;;
*) echo "你输入了多个字符" ;;
esac
这个特性在需要根据模式匹配做分支时特别有用。
3.4 用 | 合并多个匹配项
bash
#!/bin/bash
read -p "是否继续?(y/n/yes/no): " answer
case "${answer,,}" in # ${var,,} 把变量值转成小写
y|yes) echo "继续执行..." ;;
n|no) echo "已取消"; exit 0 ;;
*) echo "请输入 y 或 n" ;;
esac
3.5 case运用场景总结
| 场景 | 推荐 | 理由 |
|---|---|---|
| 服务管理脚本(start/stop/restart) | case | 经典用法,可读性最好 |
| 用户交互选择(y/n、1/2/3/4) | case | 简洁明了 |
| 按模式匹配(数字/字母范围) | case | if-else不方便处理模式 |
| 条件有优先级或依赖关系 | if-else | 例如必须先判断目录存在才检查权限 |
| 只判断true/false | if | 更直观 |
四、实战:编写完整的服务管理脚本
把前面学到的变量、条件判断、case整合起来,写一个生产级简单的服务管理脚本:
bash
#!/bin/bash
# 服务管理脚本 demo-daemon.sh
SERVICE_NAME="demo"
PID_FILE="/var/run/${SERVICE_NAME}.pid"
LOG_FILE="/var/log/${SERVICE_NAME}.log"
# 启动函数
start_service() {
# 检查是否已在运行
if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo "服务已在运行 (PID: $pid)"
return 1
else
# PID文件存在但进程不存在,清理残留
rm -f "$PID_FILE"
fi
fi
# 启动服务(以sleep 3600模拟一个常驻程序)
echo "正在启动 ${SERVICE_NAME}..."
nohup sleep 3600 >> "$LOG_FILE" 2>&1 &
echo $! > "$PID_FILE"
echo "服务已启动 (PID: $!)"
}
# 停止函数
stop_service() {
if [[ ! -f "$PID_FILE" ]]; then
echo "服务未在运行"
return 1
fi
pid=$(cat "$PID_FILE")
if kill "$pid" 2>/dev/null; then
echo "正在停止服务 (PID: $pid)..."
sleep 1
# 如果还没死,强制杀死
kill -9 "$pid" 2>/dev/null
rm -f "$PID_FILE"
echo "服务已停止"
else
echo "无法停止服务,可能已经退出"
rm -f "$PID_FILE"
fi
}
# 状态检查函数
check_status() {
if [[ -f "$PID_FILE" ]]; then
pid=$(cat "$PID_FILE")
if kill -0 "$pid" 2>/dev/null; then
echo "服务正在运行 (PID: $pid)"
return 0
fi
fi
echo "服务已停止"
return 1
}
# 主流程:根据参数分发
case "$1" in
start)
start_service
;;
stop)
stop_service
;;
restart)
echo "正在重启 ${SERVICE_NAME}..."
stop_service
sleep 2
start_service
;;
status)
check_status
;;
*)
echo "用法: $0 {start|stop|restart|status}"
exit 1
;;
esac
演示:
bash
chmod +x demo-daemon.sh
sudo ./demo-daemon.sh start # 启动服务
sudo ./demo-daemon.sh status # 查看状态
sudo ./demo-daemon.sh restart # 重启服务
sudo ./demo-daemon.sh stop # 停止服务
./demo-daemon.sh # 不带参数,显示用法
脚本设计要点
(1)PID文件的作用
用/var/run/demo.pid文件记录服务进程的PID。这是Linux守护进程的标准做法:
-
启动时把
$!(最后一个后台进程的PID)写入文件 -
停止时读取PID文件,kill对应进程
-
状态检查用
kill -0判断进程是否存在(不发信号,仅检查)
(2)返回值约定
它符合Linux命令行惯例:成功返回0,失败返回非0。check_status返回0表示正在运行,1表示已停止。
(3)分支设计
start分支先检查是否已运行(幂等性),stop分支处理PID文件残留的情况,restart简单复用前两个函数避免重复代码。
五、case与if-else的选择指南
| 特征 | 选case | 选if-else |
|---|---|---|
| 分支基于精确值匹配 | ✅ | ❌ |
| 分支基于范围或条件 | ❌ | ✅ |
| 分支数量3个以上 | ✅ | ❌ |
| 条件之间有依赖关系 | ❌ | ✅ |
| 模式匹配(通配符) | ✅ | ❌ |
总结 :如果变量有2-3个明确取值,优先用case;如果需要复杂的逻辑判断(文件是否存在 + 是否可写 + 大小是否超过阈值),用if。
六、本篇小结
if-else:适合条件判断,特别是条件之间有依赖关系(先判断存在再判断权限)。
case语句:
-
语法:
case 变量 in 模式) 命令 ;; esac -
支持通配符模式匹配(
*、?、[0-9]) -
多个模式用
|合并 -
最适合上述服务管理脚本的子命令分发
核心差异 :if判断条件是否为真 ,case判断变量匹配哪个模式。
动手练习
bash
#!/bin/bash
# 练习:编写一个文件类型识别脚本
FILE="$1"
# 检查是否传入参数
if [[ -z "$FILE" ]]; then
echo "用法: $0 <文件路径>"
exit 1
fi
# 检查文件是否存在
if [[ ! -e "$FILE" ]]; then
echo "错误: $FILE 不存在"
exit 1
fi
# 根据文件扩展名判断类型
case "${FILE##*.}" in
sh|bash) echo "Shell脚本" ;;
py) echo "Python脚本" ;;
js) echo "JavaScript文件" ;;
html|htm) echo "HTML文件" ;;
css) echo "样式表文件" ;;
json) echo "JSON数据文件" ;;
yaml|yml) echo "YAML配置文件" ;;
conf|cfg) echo "配置文件" ;;
log) echo "日志文件" ;;
gz|bz2|xz) echo "压缩包" ;;
*) echo "未知文件类型: .${FILE##*.}" ;;
esac
# 输出文件大小信息
echo "文件大小: $(du -h "$FILE" | cut -f1)"
保存为filetype.sh,测试:
bash
chmod +x filetype.sh
./filetype.sh test.py → Python脚本
./filetype.sh /etc/nginx/nginx.conf → 配置文件
七、下篇预告
掌握了判断和分支,脚本已经能做出"智能决策"。但很多任务需要重复执行------比如遍历所有.log文件、等待某个条件成立、循环处理直到完成。
下一篇我们将进入循环结构的世界,学习:
-
for循环遍历列表和文件 -
while循环读取文件和等待条件 -
break和continue控制循环流程 -
批量重命名、批量处理日志等实战案例
延伸思考 :case中的;;、;;&和;&有什么区别?;;匹配后退出case块,;;&继续匹配下一个模式(Bash 4.0+),;&无条件继续执行下一个分支(几乎不用)。这种设计让case具有了"贯穿"能力,但也增加了行为复杂度。日常使用保持;;即可,高级场景再查阅文档。