1. Shell脚本流程控制基础概念
在Linux系统管理和自动化运维中,Shell脚本的流程控制能力是区分初级用户和高级用户的关键技能。流程控制本质上是对代码执行顺序的智能管理,它让脚本具备了根据不同条件做出判断和重复执行的能力。
Shell脚本中主要有三类流程控制结构:
- 条件判断(if/then/else, case)
- 循环控制(for, while, until)
- 流程跳转(break, continue, exit)
这些结构配合测试命令(test或[])以及各种运算符,可以构建出复杂的业务逻辑。比如系统监控脚本需要根据CPU使用率决定是否发送告警,批量处理文件时需要遍历目录下的所有文件,这些场景都离不开流程控制。
提示:Shell脚本中的流程控制语法虽然简单,但实际使用时有许多细节需要注意,比如条件判断中的空格要求、循环中的变量作用域等,这些我们会在后续章节详细讲解。
2. 条件判断结构详解
2.1 if语句的完整语法体系
if语句是Shell脚本中最基础也是最重要的条件判断结构,其完整语法如下:
bash复制if condition1
then
command1
elif condition2
then
command2
else
command3
fi
其中condition可以是:
- 使用test命令或[]进行的测试表达式
- 命令执行后的返回状态(0表示成功,非0表示失败)
- 使用(( ))进行的算术运算
- 使用[[ ]]进行的增强型条件测试
一个实际的文件检查示例:
bash复制if [ -f "/var/log/syslog" ]; then
echo "系统日志文件存在"
grep "error" /var/log/syslog | wc -l
else
echo "警告:系统日志文件不存在!"
fi
2.2 测试条件的深入解析
Shell脚本中常用的测试条件包括:
文件测试:
- -e file:文件是否存在
- -f file:是否是普通文件
- -d file:是否是目录
- -r/w/x file:是否可读/写/执行
- -s file:文件大小是否大于0
字符串测试:
- -z str:字符串长度是否为0
- -n str:字符串长度是否非0
- str1 = str2:字符串是否相等
- str1 != str2:字符串是否不等
数值比较:
- -eq:等于
- -ne:不等于
- -gt:大于
- -lt:小于
- -ge:大于等于
- -le:小于等于
组合条件:
- !:逻辑非
- -a:逻辑与
- -o:逻辑或
注意:在[]中使用-a/-o时要注意加引号避免解析问题,建议使用[[ ]]或者将多个条件拆分成多个[]通过&&/||连接。
2.3 case语句的多分支选择
case语句适合处理多分支选择场景,语法如下:
bash复制case $变量 in
模式1)
命令1
;;
模式2)
命令2
;;
*)
默认命令
;;
esac
实际应用示例(服务管理脚本):
bash复制case "$1" in
start)
/usr/local/bin/start_service.sh
;;
stop)
/usr/local/bin/stop_service.sh
;;
restart)
/usr/local/bin/restart_service.sh
;;
status)
ps aux | grep my_service
;;
*)
echo "用法: $0 {start|stop|restart|status}"
exit 1
esac
case模式支持通配符匹配:
- *:匹配任意字符
- ?:匹配单个字符
- [abc]:匹配a、b或c中的任意一个字符
- a|b:匹配a或b
3. 循环结构全面解析
3.1 for循环的多种用法
Shell中的for循环有两种主要形式:
列表for循环:
bash复制for 变量 in 列表
do
命令
done
示例(处理当前目录下的.txt文件):
bash复制for file in *.txt
do
echo "处理文件: $file"
mv "$file" "${file%.txt}.bak"
done
C风格for循环:
bash复制for ((初始值; 条件; 步进))
do
命令
done
示例(生成1-10的平方数):
bash复制for ((i=1; i<=10; i++))
do
echo "$i的平方是$((i*i))"
done
3.2 while与until循环的对比
while循环在条件为真时执行,until循环则在条件为假时执行:
bash复制# while循环示例
count=1
while [ $count -le 5 ]
do
echo "这是第$count次循环"
count=$((count+1))
done
# until循环示例
password=""
until [ "$password" = "secret" ]
do
read -p "请输入密码: " password
done
echo "密码正确!"
实际应用:读取文件内容逐行处理
bash复制while IFS= read -r line
do
echo "处理行: $line"
# 其他处理逻辑
done < "/path/to/file"
3.3 循环控制命令
- break [n]:跳出n层循环
- continue [n]:跳过当前循环的剩余部分,进入下一次循环
- exit [n]:退出整个脚本,返回状态码n
示例(查找文件中第一个包含"error"的行):
bash复制while IFS= read -r line
do
if [[ "$line" == *error* ]]; then
echo "发现错误行: $line"
break
fi
done < "/var/log/app.log"
4. 流程控制实战技巧
4.1 条件判断中的常见陷阱
-
空格问题:在[]和[[ ]]中,运算符两边必须有空格
- 正确:[ "$var" = "value" ]
- 错误:["$var"="value"]
-
字符串比较:使用=而不是==(虽然bash支持==但不具可移植性)
- 建议:[ "$str1" = "$str2" ]
-
数值比较:在[]中使用-eq/-ne等,或者使用(( ))
- 建议:(( count > 10 )) 比 [ "$count" -gt 10 ]更直观
-
文件测试:检查文件是否存在应该用-e而不是-f,因为-f只检查普通文件
4.2 循环性能优化技巧
-
避免在循环中调用外部命令:尽量使用shell内置功能
- 慢:for i in $(seq 1 100); do ... done
- 快:for ((i=1; i<=100; i++)); do ... done
-
大文件处理:使用while read而不是for循环处理大文件
- while read可以逐行处理,内存效率高
-
提前计算循环次数:对于已知次数的循环,提前计算好次数
-
使用子shell减少变量污染:
bash复制(for i in {1..10}; do temp=$((i*2)) echo "$temp" done) # 这里的$temp在外部不可见
4.3 复杂条件逻辑的清晰写法
对于复杂的条件判断,可以采用以下方法提高可读性:
-
使用函数封装条件判断:
bash复制is_valid_user() { [[ "$1" =~ ^[a-z_][a-z0-9_]{0,31}$ ]] && id "$1" &>/dev/null } if is_valid_user "$username"; then echo "有效用户" fi -
使用临时变量存储条件结果:
bash复制has_error=$(grep -c "ERROR" logfile) is_weekend=$([ $(date +%u) -gt 5 ] && echo true || echo false) if (( has_error > 0 )) && $is_weekend; then send_alert fi -
多条件分行书写:
bash复制if [[ -f "$config_file" ]] \ && [[ -r "$config_file" ]] \ && grep -q "enable_feature=1" "$config_file" then enable_feature fi
5. 实战案例解析
5.1 系统监控脚本示例
以下是一个结合多种流程控制结构的系统监控脚本:
bash复制#!/bin/bash
MAX_CPU=80
MAX_MEM=90
LOG_FILE="/var/log/system_monitor.log"
# 检查日志目录是否存在
if [ ! -d "$(dirname "$LOG_FILE")" ]; then
mkdir -p "$(dirname "$LOG_FILE")" || {
echo "无法创建日志目录" >&2
exit 1
}
fi
# 获取系统指标
cpu_usage=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}')
mem_usage=$(free | grep Mem | awk '{print $3/$2 * 100.0}')
# 记录指标
echo "$(date '+%Y-%m-%d %H:%M:%S') CPU: ${cpu_usage}% MEM: ${mem_usage}%" >> "$LOG_FILE"
# 检查阈值
if (( $(echo "$cpu_usage > $MAX_CPU" | bc -l) )); then
echo "警告:CPU使用率过高 (${cpu_usage}%)" >> "$LOG_FILE"
# 可以添加发送告警的逻辑
fi
if (( $(echo "$mem_usage > $MAX_MEM" | bc -l) )); then
echo "警告:内存使用率过高 (${mem_usage}%)" >> "$LOG_FILE"
# 可以添加发送告警的逻辑
fi
# 检查日志文件大小
if [ -f "$LOG_FILE" ] && [ $(stat -c%s "$LOG_FILE") -gt 1048576 ]; then
echo "日志文件超过1MB,执行轮转"
gzip -c "$LOG_FILE" > "${LOG_FILE}.$(date +%Y%m%d%H%M%S).gz"
> "$LOG_FILE"
fi
5.2 批量文件处理脚本
以下脚本展示如何使用流程控制处理批量文件:
bash复制#!/bin/bash
SRC_DIR="/data/source"
DEST_DIR="/data/backup"
FILE_PATTERN="*.dat"
# 检查目录是否存在
if [ ! -d "$SRC_DIR" ]; then
echo "错误:源目录 $SRC_DIR 不存在" >&2
exit 1
fi
[ -d "$DEST_DIR" ] || mkdir -p "$DEST_DIR"
# 处理文件
processed=0
skipped=0
errors=0
for file in "$SRC_DIR"/$FILE_PATTERN; do
# 确保是文件而不是模式本身
[ -f "$file" ] || continue
filename=$(basename "$file")
dest_file="$DEST_DIR/${filename%.dat}.bak"
# 检查目标文件是否已存在
if [ -e "$dest_file" ]; then
echo "跳过已存在的文件: $filename"
((skipped++))
continue
fi
# 处理文件
if process_file "$file" "$dest_file"; then
echo "成功处理: $filename"
((processed++))
else
echo "处理失败: $filename" >&2
((errors++))
fi
done
# 输出摘要
echo "处理完成:"
echo "成功: $processed"
echo "跳过: $skipped"
echo "失败: $errors"
# 如果有错误文件,发送通知
if [ "$errors" -gt 0 ]; then
send_alert "文件处理过程中出现 $errors 个错误"
fi
# 处理文件的函数
process_file() {
local src=$1
local dest=$2
# 模拟处理过程
sleep 0.5
# 随机失败(仅示例)
if (( RANDOM % 10 == 0 )); then
return 1
fi
cp "$src" "$dest" && return 0 || return 1
}
5.3 交互式菜单系统
以下是一个使用case语句构建的交互式菜单系统:
bash复制#!/bin/bash
show_menu() {
clear
echo "系统管理菜单"
echo "1. 显示系统信息"
echo "2. 显示磁盘使用情况"
echo "3. 显示内存使用情况"
echo "4. 重启服务"
echo "5. 备份日志"
echo "6. 退出"
}
system_info() {
echo "===== 系统信息 ====="
echo "主机名: $(hostname)"
echo "操作系统: $(lsb_release -d | cut -f2)"
echo "内核版本: $(uname -r)"
echo "启动时间: $(uptime -s)"
read -p "按任意键继续..." -n1
}
disk_usage() {
echo "===== 磁盘使用情况 ====="
df -h | grep -v tmpfs
read -p "按任意键继续..." -n1
}
memory_usage() {
echo "===== 内存使用情况 ====="
free -h
read -p "按任意键继续..." -n1
}
restart_service() {
echo "===== 服务列表 ====="
systemctl list-units --type=service --no-pager | head -10
read -p "输入要重启的服务名: " service_name
if systemctl is-active --quiet "$service_name"; then
sudo systemctl restart "$service_name" && \
echo "服务 $service_name 重启成功" || \
echo "服务 $service_name 重启失败"
else
echo "服务 $service_name 未运行"
fi
read -p "按任意键继续..." -n1
}
backup_logs() {
echo "===== 日志备份 ====="
local backup_dir="/var/log/backups/$(date +%Y%m%d)"
mkdir -p "$backup_dir"
sudo find /var/log -name "*.log" -type f -mtime -1 -exec cp {} "$backup_dir" \;
echo "日志已备份到 $backup_dir"
read -p "按任意键继续..." -n1
}
while true; do
show_menu
read -p "请输入选择 [1-6]: " choice
case $choice in
1) system_info ;;
2) disk_usage ;;
3) memory_usage ;;
4) restart_service ;;
5) backup_logs ;;
6) echo "退出系统"; exit 0 ;;
*) echo "无效选择,请重新输入"; sleep 1 ;;
esac
done
6. 调试与错误处理
6.1 调试技巧
-
使用set命令开启调试模式:
bash复制# 在脚本开头添加 set -x # 开启命令打印 set -e # 遇到错误立即退出 set -o pipefail # 管道命令中任意一个失败则整个失败 -
输出变量值调试:
bash复制debug() { echo "DEBUG: $*" >&2 } debug "变量值: var1=$var1, var2=$var2" -
使用trap捕获信号:
bash复制trap 'echo "脚本被中断"; cleanup; exit 1' INT TERM
6.2 错误处理最佳实践
-
检查命令返回值:
bash复制if ! command; then echo "命令执行失败" >&2 exit 1 fi -
提供有意义的错误信息:
bash复制config_file="/etc/app.conf" [ -f "$config_file" ] || { echo "错误:配置文件 $config_file 不存在" >&2 exit 1 } -
使用函数封装可能失败的操作:
bash复制safe_mkdir() { local dir=$1 if [ -e "$dir" ] && [ ! -d "$dir" ]; then echo "错误:$dir 存在但不是目录" >&2 return 1 fi mkdir -p "$dir" || { echo "无法创建目录 $dir" >&2 return 1 } } -
实现重试逻辑:
bash复制max_retries=3 retry_delay=5 for ((i=1; i<=max_retries; i++)); do if perform_operation; then break else echo "操作失败,尝试 $i/$max_retries" if [ "$i" -lt "$max_retries" ]; then sleep "$retry_delay" else echo "达到最大重试次数,放弃" exit 1 fi fi done
6.3 常见错误与解决方案
-
变量未引号导致的解析问题:
- 错误:if [ $var = "value" ] (当$var为空时会变成 [ = "value" ])
- 正确:if [ "$var" = "value" ]
-
循环中的管道问题:
- 错误:echo "1 2 3" | for i in $(cat); do ... done (循环在子shell中执行)
- 正确:for i in 1 2 3; do ... done
-
算术运算的语法错误:
- 错误:count=count+1
- 正确:count=$((count+1)) 或 ((count++))
-
文件路径中的空格问题:
- 错误:for file in /path/*; do rm $file; done
- 正确:for file in /path/*; do rm "$file"; done
-
条件判断中的字符串与数值混淆:
- 错误:[ "10" -gt "2" ] (虽然能工作但不规范)
- 正确:[ 10 -gt 2 ] 或 (( 10 > 2 ))
