流程控制与函数
# 第 2 章 流程控制与函数
# 目录介绍
# 2.1 条件判断
# 2.1.1 [ ] 与 [[ ]] 的本质区别
Shell 有两种条件测试语法——它们的差异是新手最困惑的点之一:
#!/bin/bash
# ===== [ ] —— POSIX 标准,所有 Shell 通用 =====
# [ 是一个命令!不是语法符号
if [ "$a" = "$b" ]; then # [ 两边必须有空格!
echo "相等"
fi
# ===== [[ ]] —— Bash 扩展,更强大更安全 =====
# [[ 是 Bash 关键字,不是命令——空格要求更宽松
if [[ "$a" == "$b" ]]; then # 甚至可以用 ==(POSIX 不认)
echo "相等"
fi
2
3
4
5
6
7
8
9
10
11
12
13
| 特性 | [ ] | [[ ]] |
|---|---|---|
| 本质 | 命令(/bin/[) | Bash 关键字 |
| 空格要求 | 必须有空格 [ "$a" = "$b" ] | 关键字,语法稍宽松 |
> < 比较 | ❌ 被当作重定向 | ✅ 字符串比较 |
&& \|\| 逻辑 | 需要 -a / -o | ✅ 用 && / \|\| |
=~ 正则 | ❌ 不支持 | ✅ |
| 空变量 | [ $a = "test" ] 报错 | [[ $a == "test" ]] 安全 |
| 可移植性 | ✅ 所有 POSIX Shell | ❌ 仅 Bash/Zsh |
📌 写新脚本一律用
[[ ]]——除非你需要兼容/bin/sh的极致场景。
# 2.1.2 if-elif-else-fi 完整语法
#!/bin/bash
read -r -p "请输入分数:" score
if [[ "$score" -ge 90 ]]; then
echo "等级:A"
elif [[ "$score" -ge 80 ]]; then
echo "等级:B"
elif [[ "$score" -ge 70 ]]; then
echo "等级:C"
elif [[ "$score" -ge 60 ]]; then
echo "等级:D"
else
echo "等级:F"
fi # ← fi 关闭 if——Shell 的"反向关键字"风格
2
3
4
5
6
7
8
9
10
11
12
13
14
15
🔑 Shell 的"反向关键字"——和 C/Java/Python 完全不同的思路:
# Shell 用"反向关键字"关闭代码块——ADM-3A 终端时代的遗产
if ...; then ...; fi # if → fi
case ... in ... esac # case → esac
# do ... done # 不"反向"——do 倒过来是 od
2
3
4
一行 if:
# 分号替代换行
if [[ "$a" -gt 0 ]]; then echo "正数"; elif [[ "$a" -lt 0 ]]; then echo "负数"; else echo "零"; fi
# 更清晰——用 && 和 ||(见 §2.1.6)
[[ "$DEBUG" == "true" ]] && echo "调试模式开启" # 条件为真 → 执行右边
[[ -f config.yaml ]] || { echo "配置文件缺失!"; exit 1; } # 条件为假 → 执行右边
2
3
4
5
6
# 2.1.3 字符串判断
#!/bin/bash
str1="hello"
str2="world"
empty=""
# ===== 相等 / 不等 =====
[[ "$str1" == "$str2" ]] && echo "相等" || echo "不相等" # 不相等
# ===== 空字符串判断 =====
[[ -z "$empty" ]] && echo "是空字符串" # 是空字符串
[[ -n "$str1" ]] && echo "非空" # 非空
[[ -z "${UNDEFINED:-}" ]] && echo "未定义变量也是空" # ✅ 用 :- 避免 unbound
# ===== 通配符匹配 =====
filename="backup_2025.tar.gz"
[[ "$filename" == *.tar.gz ]] && echo "是 tar.gz 文件"
[[ "$filename" == backup_* ]] && echo "以 backup_ 开头"
# ===== 正则匹配 =====(仅 [[ ]])
phone="13812345678"
if [[ "$phone" =~ ^1[3-9][0-9]{9}$ ]]; then
echo "有效的手机号"
fi
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 2.1.4 数值判断
#!/bin/bash
a=10
b=20
# ===== 方式 1:[[ ]] + 数值比较符(推荐)=====
[[ "$a" -eq "$b" ]] && echo "相等" # equal
[[ "$a" -ne "$b" ]] && echo "不等" # not equal
[[ "$a" -lt "$b" ]] && echo "小于" # less than
[[ "$a" -le "$b" ]] && echo "小于等于" # less or equal
[[ "$a" -gt "$b" ]] && echo "大于" # greater than
[[ "$a" -ge "$b" ]] && echo "大于等于" # greater or equal
# ===== 方式 2:(( )) —— 算术判断(更像 C 语言)=====
if (( a < b )); then
echo "$a 小于 $b"
fi
# 区间判断
if (( a >= 5 && a <= 15 )); then
echo "$a 在区间 [5, 15] 内"
fi
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
🔑 -lt vs <——数值比较的两套语法:
# [[ ]] 用字母缩写( -eq / -ne / -lt / -le / -gt / -ge )
[[ 10 -eq 10 ]] # ✅
[[ 10 -gt 5 ]] # ✅
# (( )) 用数学符号——更直观
(( 10 == 10 )) # ✅
(( 10 > 5 )) # ✅
# 注意:[[ ]] 里不要用 > <
[[ 10 > 5 ]] # ❌ 这是字符串比较!"10" > "5"?——按 ASCII 序比较
# "1" 的 ASCII 码 < "5",所以 "10" < "5"——完全反直觉
2
3
4
5
6
7
8
9
10
11
# 2.1.5 文件判断
Shell 脚本中最常用的判断之一——检查文件是否存在、是否可读等:
#!/bin/bash
path="/var/log/app.log"
# ===== 存在性 =====
[[ -e "$path" ]] && echo "存在(文件或目录均可)"
[[ -f "$path" ]] && echo "是普通文件"
[[ -d "$path" ]] && echo "是目录"
[[ -L "$path" ]] && echo "是符号链接"
# ===== 权限 =====
[[ -r "$path" ]] && echo "可读"
[[ -w "$path" ]] && echo "可写"
[[ -x "$path" ]] && echo "可执行"
# ===== 大小与时间 =====
[[ -s "$path" ]] && echo "文件非空(大小 > 0)"
[[ "$path" -nt "$path.bak" ]] && echo "比备份文件新" # newer than
[[ "$path" -ot "$path.bak" ]] && echo "比备份文件旧" # older than
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
文件判断速查表:
| 测试 | 含义 |
|---|---|
-e | 存在(exist) |
-f | 是普通文件(file) |
-d | 是目录(directory) |
-L | 是符号链接(link) |
-r | 可读 |
-w | 可写 |
-x | 可执行 |
-s | 文件非空(size > 0) |
-nt | 比...新(newer than) |
# 2.1.6 && / || 短路运算
Shell 中 && 和 || 是条件链——极其简洁但需要理解"短路":
#!/bin/bash
# ===== && :前面成功(退出码 0),才执行后面 =====
cd /var/log && echo "成功进入 /var/log" # cd 成功 → 打印
# ===== || :前面失败(退出码 ≠ 0),才执行后面 =====
cd /nonexistent || echo "目录不存在" # cd 失败 → 打印
# ===== 组合——精巧的单行 if-else =====
[[ -f config.yaml ]] && echo "配置存在" || echo "配置缺失"
# 等价于:
# if [[ -f config.yaml ]]; then echo "配置存在"; else echo "配置缺失"; fi
# ===== 多条件链 =====
# 只有前两步都成功,才执行第三步
mkdir -p /backup && tar -czf /backup/data.tar.gz /data && echo "备份成功"
# 任何一步失败——后面的都不执行(短路保护)
# ===== 大括号组合多条命令 =====
[[ -d /data ]] || { echo "数据目录不存在!"; exit 1; }
# 如果 /data 不存在——打印错误并退出
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
⚠️ && ... || 不是真正的 if-else:
# ❌ 陷阱:第二个命令也失败了会导致 else 触发
[[ -f file.txt ]] && cat file.txt || echo "文件读取失败"
# 如果 file.txt 存在但是 cat 失败(权限问题)——
# && 左边成功 → 执行 cat → cat 失败 → || 右边也执行了!
# 输出:"文件读取失败"——但实际文件存在!
# ✅ 正确——老老实实用 if
if [[ -f file.txt ]]; then
cat file.txt
else
echo "文件读取失败"
fi
2
3
4
5
6
7
8
9
10
11
12
# 2.1.7 case 多分支匹配
case 是 Shell 的 switch——但比 C 语言的功能强得多(支持通配符):
#!/bin/bash
read -r -p "请输入命令(start/stop/restart/status):" cmd
case "$cmd" in
start)
echo "启动服务..."
;;
stop)
echo "停止服务..."
;;
restart)
echo "重启服务..."
;;
status)
echo "查看状态..."
;;
*) # 默认分支——类似 C 的 default
echo "用法:$0 {start|stop|restart|status}"
exit 1
;;
esac # ← case 倒过来
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
🔑 case 的通配符匹配——比 C 的 switch 强 10 倍:
read -r -p "输入 y/n:" answer
case "$answer" in
[yY] | [yY][eE][sS] )
echo "你选择了 是"
;;
[nN] | [nN][oO] )
echo "你选择了 否"
;;
*)
echo "请输入 y 或 n"
;;
esac
2
3
4
5
6
7
8
9
10
11
12
13
# 实战:按文件扩展名处理
for file in *; do
case "$file" in
*.tar.gz | *.tgz)
tar -xzf "$file"
;;
*.zip)
unzip "$file"
;;
*.log)
gzip "$file" # 压缩日志
;;
*)
echo "跳过:$file"
;;
esac
done
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 2.2 循环控制
# 2.2.1 for 循环的三种形态
#!/bin/bash
# ===== 形态 1:遍历列表 =====
for fruit in 苹果 香蕉 橘子 葡萄; do
echo "今天吃 $fruit"
done
# ===== 形态 2:遍历数组 =====
files=( "a.txt" "b.txt" "c.txt" )
for f in "${files[@]}"; do # ← 一定要加引号——防止文件名含空格被拆散
echo "处理:$f"
done
# ===== 形态 3:C 语言风格(算术循环)=====
for (( i = 1; i <= 5; i++ )); do
echo "第 $i 次"
done
# 倒序
for (( i = 10; i >= 1; i-- )); do
echo "$i..."
done
echo "发射!"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
🔑 for 循环的万能源——命令输出、通配符、大括号展开:
# 遍历命令输出
for user in $(cut -d: -f1 /etc/passwd); do
echo "用户:$user"
done
# 遍历通配符匹配的文件
for log in /var/log/*.log; do
wc -l "$log"
done
# 遍历序列(大括号展开——bash 专有)
for n in {1..10..2}; do # 1 3 5 7 9
echo "n = $n"
done
# 或 seq 命令(POSIX 兼容)
for n in $(seq 1 2 10); do # 同上
echo "n = $n"
done
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 2.2.2 while 循环与 until 循环
#!/bin/bash
# ===== while:条件为真时循环 =====
count=0
while (( count < 5 )); do
echo "count = $count"
((count++))
done
# ===== 读取文件每一行——while read 是最常见的 Shell 循环 =====
while IFS= read -r line; do # IFS= 保留行首空白,-r 保留反斜杠
echo "行内容:$line"
done < /etc/hosts # < 重定向文件给 while 循环
# ===== 管道输入给 while =====
grep "ERROR" /var/log/app.log | while read -r line; do
echo "发现错误:$line"
done
# ===== until:条件为假时循环(while 的反面)=====
# 用法很少——通常 while 就够了
count=10
until (( count < 5 )); do
echo "count = $count(还在等它小于 5...)"
((count--))
done
# ===== select:交互式菜单 =====
PS3="请选择:"
select option in "启动" "停止" "重启" "退出"; do
case "$option" in
启动) echo "启动中..." ;;
停止) echo "停止中..." ;;
重启) echo "重启中..." ;;
退出) break ;;
esac
done
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
🔑 while read 的管道陷阱——子 Shell 变量丢失:
# ❌ 管道中的 while 在子 Shell 里——外部变量不会被修改!
count=0
cat /etc/hosts | while read -r line; do
((count++))
done
echo "行数:$count" # 0——变量丢失了!
# ✅ 修复:用 < 重定向代替管道
count=0
while read -r line; do
((count++))
done < /etc/hosts
echo "行数:$count" # 正确的行数
# ✅ 或:用进程替换(Bash)
count=0
while read -r line; do
((count++))
done < <(cat /etc/hosts)
echo "行数:$count" # 正确的行数
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2.2.3 break / continue
#!/bin/bash
# break:跳出循环
for i in {1..10}; do
if (( i == 5 )); then
break
fi
echo "i = $i" # 1 2 3 4
done
# continue:跳过本次
for i in {1..10}; do
if (( i % 2 == 0 )); then
continue
fi
echo "奇数:$i" # 1 3 5 7 9
done
# break N:跳出 N 层循环
for i in {1..3}; do
for j in {1..3}; do
if (( i == 2 && j == 2 )); then
break 2 # 跳出两层循环
fi
echo "($i, $j)"
done
done
# 输出:(1,1)(1,2)(1,3)(2,1)——碰到 (2,2) 就全停了
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 2.2.4 循环实战:批量处理文件
#!/bin/bash
# batch_rename.sh —— 批量给文件加前缀
PREFIX="${1:-IMG_}"
START_NUM="${2:-1}"
counter=$START_NUM
# 处理当前目录所有 .jpg 文件
for file in *.jpg; do
# 如果 glob 没匹配到——file 就是字面量 "*.jpg"
[[ -e "$file" ]] || { echo "没有 .jpg 文件"; exit 0; }
new_name="${PREFIX}$(printf '%04d' $counter).jpg" # IMG_0001.jpg
# 跳过已存在的同名文件
if [[ -e "$new_name" ]]; then
echo "⚠️ $new_name 已存在,跳过"
((counter++))
continue
fi
mv -v "$file" "$new_name"
((counter++))
done
echo "✅ 完成——处理了 $((counter - START_NUM)) 个文件"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 2.3 函数
# 2.3.1 函数定义与调用
Shell 函数没有参数列表、没有返回类型——它的定义极简:
#!/bin/bash
# ===== 方式 1:标准语法(推荐)=====
function_name() {
echo "这是一个函数"
}
# ===== 方式 2:function 关键字(Bash 专有)=====
function another_func {
echo "这也是函数"
}
# ===== 调用——直接写函数名 =====
function_name
another_func
2
3
4
5
6
7
8
9
10
11
12
13
14
15
🔑 Shell 函数和变量在同一命名空间——函数名和变量名会冲突:
# 先定义变量
greet="Hello"
# 再定义同名函数——会报错!
# greet() { echo "Hi"; } # bash: greet: 已定义为变量
# 查看所有已定义的函数
declare -F
# 查看函数体
declare -f function_name
2
3
4
5
6
7
8
9
# 2.3.2 参数传递与返回值
Shell 函数用位置参数——不需要在定义时声明参数:
#!/bin/bash
greet() {
# $1 $2 ... 是函数的参数——不是脚本的参数!
local name="${1:-世界}"
local greeting="${2:-Hello}"
echo "$greeting, $name!"
}
# 调用时直接传参——和命令一样
greet "张三" "你好" # 你好, 张三!
greet "李四" # Hello, 李四!(第二个参数用默认值)
greet # Hello, 世界!(都用默认值)
2
3
4
5
6
7
8
9
10
11
12
13
🔑 函数内部的位置参数:
#!/bin/bash
show_args() {
echo "函数名:${FUNCNAME[0]}"
echo "参数个数:$#"
echo "所有参数:$@"
echo "第 1 个参数:$1"
echo "第 2 个参数:$2"
# 遍历所有参数
for arg in "$@"; do
echo " → $arg"
done
}
# 调用
show_args a b c d
# 参数个数:4
# 所有参数:a b c d
# 第 1 个参数:a
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
🔑 返回值——只能用整数(0-255):
#!/bin/bash
# Shell 的 return ≠ Python 的 return
# return 返回退出码(0=成功,非0=失败),不是数据!
is_even() {
if (( $1 % 2 == 0 )); then
return 0 # 成功——偶数
else
return 1 # 失败——奇数(数字 1,不是字符串)
fi
}
if is_even 42; then
echo "是偶数"
fi
# ===== 返回数据?用 echo + 命令替换 =====
get_timestamp() {
date '+%Y-%m-%d %H:%M:%S' # echo 输出
}
now=$(get_timestamp) # 命令替换捕获
echo "现在时间:$now"
# ===== 返回多个值?逐行 echo =====
get_limits() {
echo "min=1"
echo "max=100"
}
# 读取两行
read -r min < <(get_limits | head -1)
read -r max < <(get_limits | tail -1)
echo "$min ~ $max"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
🔑 返回值风格对比:
| 风格 | 获取方式 | 适用 |
|---|---|---|
return N | if func "$@" 或 $? | 成功/失败判定 |
echo | result=$(func) | 返回字符串数据 |
| 写全局变量 | 直接读 $MY_RESULT | 多个返回值 |
# 2.3.3 局部变量 local
不加 local 的变量是全局的——会污染整个脚本:
#!/bin/bash
demo_scope() {
local local_var="我是局部的" # ✅ 仅函数内可见
global_var="我是全局的" # ❌ 函数外也能访问!
}
demo_scope
echo "函数内 local_var = ${local_var:-未定义}" # 未定义
echo "函数内 global_var = $global_var" # 我是全局的
# ===== 陷阱:不加 local 时,循环计数器外泄 =====
sum_numbers() {
total=0 # ❌ 没有 local
for i in 1 2 3 4 5; do
((total += i))
done
echo "总和:$total"
}
i=999 # 脚本顶层定义的 i
sum_numbers
echo "脚本的 i = $i" # 5 ← 被破坏了!
# ✅ 正确的写法
sum_numbers_fixed() {
local total=0
local i
for i in 1 2 3 4 5; do
((total += i))
done
echo "总和:$total"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
📌 铁律:函数内的所有变量用
local声明——除非你明确需要修改全局变量。
# 2.3.4 函数库与 source 引入
把通用函数放到一个文件里——多个脚本 source 引入:
📁 lib/common.sh:
#!/bin/bash
# lib/common.sh —— 公共函数库
# ANSI 颜色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $(date '+%H:%M:%S') $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $(date '+%H:%M:%S') $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $(date '+%H:%M:%S') $*" >&2; }
die() {
log_error "$*"
exit 1
}
require_root() {
if [[ "$EUID" -ne 0 ]]; then
die "此脚本需要 root 权限——请用 sudo 运行"
fi
}
check_command() {
command -v "$1" >/dev/null 2>&1 || die "未安装 $1——请先安装"
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
📁 main.sh:
#!/bin/bash
set -euo pipefail
# 引入函数库——路径相对于 main.sh
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/lib/common.sh"
# 或 . "$SCRIPT_DIR/lib/common.sh"
require_root
check_command "nginx"
log_info "所有检查通过——继续执行..."
2
3
4
5
6
7
8
9
10
11
🔑 SCRIPT_DIR 是脚本中最重要的一行——解决了"脚本在哪里"的问题:
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# 拆解:
# $0 ← 脚本的路径(可能是相对路径)
# dirname "$0" ← 去掉文件名,只留目录
# cd ... && pwd ← 进入目录并打印绝对路径
# $(...) ← 命令替换捕获结果
2
3
4
5
6
# 2.4 综合案例:日志轮转脚本
把本章全部知识串联——条件判断 + 循环 + 函数 + 函数库:
📁 lib/logger.sh:
#!/bin/bash
# lib/logger.sh —— 日志函数库
_log() {
local level="$1"; shift
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*"
}
log_info() { _log INFO "$@"; }
log_warn() { _log WARN "$@" >&2; }
log_error() { _log ERROR "$@" >&2; }
2
3
4
5
6
7
8
9
10
11
📁 log_rotate.sh:
#!/bin/bash
# log_rotate.sh —— 日志轮转脚本
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/lib/logger.sh"
# ===== 配置区 =====
LOG_DIR="${LOG_DIR:-/var/log/myapp}" # 日志目录(环境变量可覆盖)
MAX_SIZE_MB="${MAX_SIZE_MB:-100}" # 单个日志最大 100MB
KEEP_COUNT="${KEEP_COUNT:-7}" # 保留最近 7 个备份
PATTERN="${PATTERN:-*.log}" # 轮转的日志匹配模式
# ===== 函数区 =====
get_file_size_mb() {
# 获取文件大小(MB)
local size_bytes
size_bytes=$(stat -f%z "$1" 2>/dev/null || stat -c%s "$1" 2>/dev/null)
echo $((size_bytes / 1024 / 1024))
}
is_large() {
# 判断文件是否超过阈值
local size
size=$(get_file_size_mb "$1")
[[ "$size" -ge "$MAX_SIZE_MB" ]]
}
rotate_file() {
# 轮转单个文件:app.log → app.log.1 → app.log.2 → ...
local file="$1"
local dir base ext number old new
dir=$(dirname "$file")
base=$(basename "$file")
# 删除最老的备份
local oldest="${file}.${KEEP_COUNT}"
if [[ -f "$oldest" ]]; then
rm -f "$oldest"
log_info "已删除旧备份:$oldest"
fi
# 依次递增文件名
for (( i = KEEP_COUNT - 1; i >= 1; i-- )); do
old="${file}.${i}"
new="${file}.$((i + 1))"
[[ -f "$old" ]] && mv "$old" "$new"
done
# 当前日志 → .1
cp "$file" "${file}.1" # cp 后清空——不中断写入
: > "$file" # 清空原文件(truncate)
log_info "已轮转:$file → ${file}.1($(get_file_size_mb "${file}.1")MB)"
}
cleanup_old_logs() {
# 删除超过 KEEP_DAYS 天的轮转日志
local keep_days="${1:-30}"
local count
count=$(find "$LOG_DIR" -name "${PATTERN}.[0-9]*" -mtime "+${keep_days}" -delete -print | wc -l)
if (( count > 0 )); then
log_info "已清理 $count 个超过 ${keep_days} 天的旧日志"
fi
}
# ===== 主流程 =====
main() {
# 1. 检查目录
if [[ ! -d "$LOG_DIR" ]]; then
log_error "日志目录不存在:$LOG_DIR"
exit 1
fi
log_info "开始日志轮转(阈值:${MAX_SIZE_MB}MB,保留:${KEEP_COUNT} 个备份)"
# 2. 遍历需要轮转的文件
local rotated=0 skipped=0
local file
while IFS= read -r -d '' file; do
if is_large "$file"; then
rotate_file "$file"
((rotated++))
else
((skipped++))
fi
done < <(find "$LOG_DIR" -maxdepth 1 -name "$PATTERN" -type f -print0)
# 3. 清理旧日志
cleanup_old_logs 30
# 4. 汇总
log_info "轮转完成——轮转: $rotated 个, 跳过: $skipped 个"
echo ""
echo "【轮转后日志列表】"
ls -lh "$LOG_DIR"/*.log* 2>/dev/null | awk '{print $5, $9}'
}
main "$@"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
案例知识融合:这个脚本覆盖了本章全部核心——[[ ]] 文件判断、if/for/while 三种循环、(( )) 算术判断、local 局部变量、函数定义 + 参数 $1、return 退出码、source 引入函数库、SCRIPT_DIR 路径计算——100 行代码完成了一个可部署的日志轮转系统。
# 2.5 新手陷阱 Top 5
| # | 陷阱 | 说明 |
|---|---|---|
| 1 | [ 与 [[ 空格混乱 | [$a -eq 1] → [ 是命令!必须 [ "$a" -eq 1 ] |
| 2 | 字符串比较用了 -eq | [[ "$a" -eq "$b" ]]——-eq 是数值!字符串用 == |
| 3 | 管道让 while 在子 Shell 运行 | 管道右侧的 while 里修改的变量在外面看不到——用 < <(cmd) 进程替换 |
| 4 | 函数内忘写 local | 函数内的 i=0 会覆盖脚本顶层的 i——一直 local 直到你确定要全局 |
| 5 | return 返回数据 | return "hello" 是语法错误——return 只能返回退出码 0-255。返回数据用 echo + 命令替换 |
陷阱 3 详解——修复管道子 Shell 问题:
# ❌ 管道——子 Shell 变量丢失
found=0
ls | while read -r f; do
if [[ "$f" == *.log ]]; then
found=1 # 在子 Shell 里改——外面不知道!
fi
done
echo "found=$found" # 0——依然是 0!
# ✅ 方案一:输入重定向
found=0
while read -r f; do
[[ "$f" == *.log ]] && found=1
done < <(ls)
echo "found=$found" # 1——正确!
# ✅ 方案二:直接用 for——最简单
found=0
for f in *; do
[[ "$f" == *.log ]] && found=1
done
echo "found=$found" # 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 2.6 综合思考题
[ ]vs[[ ]]的空变量处理:[ $UNDEFINED == "test" ]会报错([: ==: unary operator expected)——但[[ $UNDEFINED == "test" ]]不会。[[ ]]内部做了什么让空变量不报错?while read的三种变体:while read linevswhile read -r linevswhile IFS= read -r line——它们在处理反斜杠、行首空格时有什么差异?为什么生产级脚本都用最后一种?函数返回值的设计哲学:Shell 同时提供了
return(退出码)和echo(命令替换)两种方式传递数据——这比 Python/Java 单一return显得"分裂"。你认为是设计缺陷还是有意为之?这种设计在"函数链"(pipe)中有什么优势?(( ))和[[ ]]的数值比较:(( a < b ))和[[ $a -lt $b ]]都能判断数值大小——它们的本质区别是什么?为什么(( ))的变量前不需要$?递归的 Shell 限制:Bash 支持递归函数——但每次递归调用都会创建子 Shell。
$BASHPID和$$在递归中的行为如何暴露这个问题?Bash 递归的深度上限是多少?