编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • ScriptHub 脚本工具箱
  • Python

  • Shell-Bash

    • Shell & Bash 从0到1实战专栏
    • Shell 入门与变量
    • 流程控制与函数
      • 2.1 条件判断
        • 2.1.1 [ ] 与 [[ ]] 的本质区别
        • 2.1.2 if-elif-else-fi 完整语法
        • 2.1.3 字符串判断
        • 2.1.4 数值判断
        • 2.1.5 文件判断
        • 2.1.6 && / || 短路运算
        • 2.1.7 case 多分支匹配
      • 2.2 循环控制
        • 2.2.1 for 循环的三种形态
        • 2.2.2 while 循环与 until 循环
        • 2.2.3 break / continue
        • 2.2.4 循环实战:批量处理文件
      • 2.3 函数
        • 2.3.1 函数定义与调用
        • 2.3.2 参数传递与返回值
        • 2.3.3 局部变量 local
        • 2.3.4 函数库与 source 引入
      • 2.4 综合案例:日志轮转脚本
      • 2.5 新手陷阱 Top 5
      • 2.6 综合思考题
    • 数据与 IO 处理
    • grep 搜索实战
    • sed 与 awk 编程
    • 文件查找与统计
    • 日志监控与告警
    • 备份进程与磁盘
    • 用户与服务管理
    • 网络调度与部署
    • 调试与脚本规范
    • 安全与兼容处理
    • 性能与打包分发
  • 工具脚本

  • ScriptHub
  • Shell-Bash
杨充
2025-11-22
目录

流程控制与函数

# 第 2 章 流程控制与函数

# 目录介绍

  • 2.1 条件判断
    • 2.1.1 [ ] 与 [[ ]] 的本质区别
    • 2.1.2 if-elif-else-fi 完整语法
    • 2.1.3 字符串判断
    • 2.1.4 数值判断
    • 2.1.5 文件判断
    • 2.1.6 && / || 短路运算
    • 2.1.7 case 多分支匹配
  • 2.2 循环控制
    • 2.2.1 for 循环的三种形态
    • 2.2.2 while 循环与 until 循环
    • 2.2.3 break / continue
    • 2.2.4 循环实战:批量处理文件
  • 2.3 函数
    • 2.3.1 函数定义与调用
    • 2.3.2 参数传递与返回值
    • 2.3.3 局部变量 local
    • 2.3.4 函数库与 source 引入
  • 2.4 综合案例:日志轮转脚本
  • 2.5 新手陷阱 Top 5
  • 2.6 综合思考题

# 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
1
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 的"反向关键字"风格
1
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
1
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; }  # 条件为假 → 执行右边
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
1
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
1
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"——完全反直觉
1
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
1
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 不存在——打印错误并退出
1
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
1
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 倒过来
1
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
1
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
1
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 "发射!"
1
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
1
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
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
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"     # 正确的行数
1
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) 就全停了
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
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)) 个文件"
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

# 2.3 函数

# 2.3.1 函数定义与调用

Shell 函数没有参数列表、没有返回类型——它的定义极简:

#!/bin/bash

# ===== 方式 1:标准语法(推荐)=====
function_name() {
    echo "这是一个函数"
}

# ===== 方式 2:function 关键字(Bash 专有)=====
function another_func {
    echo "这也是函数"
}

# ===== 调用——直接写函数名 =====
function_name
another_func
1
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
1
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, 世界!(都用默认值)
1
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
1
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"
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
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"
}
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
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——请先安装"
}
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 "所有检查通过——继续执行..."
1
2
3
4
5
6
7
8
9
10
11

🔑 SCRIPT_DIR 是脚本中最重要的一行——解决了"脚本在哪里"的问题:

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# 拆解:
# $0                  ← 脚本的路径(可能是相对路径)
# dirname "$0"        ← 去掉文件名,只留目录
# cd ... && pwd       ← 进入目录并打印绝对路径
# $(...)              ← 命令替换捕获结果
1
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; }
1
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 "$@"
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
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2.6 综合思考题

  1. [ ] vs [[ ]] 的空变量处理:[ $UNDEFINED == "test" ] 会报错([: ==: unary operator expected)——但 [[ $UNDEFINED == "test" ]] 不会。[[ ]] 内部做了什么让空变量不报错?

  2. while read 的三种变体:while read line vs while read -r line vs while IFS= read -r line——它们在处理反斜杠、行首空格时有什么差异?为什么生产级脚本都用最后一种?

  3. 函数返回值的设计哲学:Shell 同时提供了 return(退出码)和 echo(命令替换)两种方式传递数据——这比 Python/Java 单一 return 显得"分裂"。你认为是设计缺陷还是有意为之?这种设计在"函数链"(pipe)中有什么优势?

  4. (( )) 和 [[ ]] 的数值比较:(( a < b )) 和 [[ $a -lt $b ]] 都能判断数值大小——它们的本质区别是什么?为什么 (( )) 的变量前不需要 $?

  5. 递归的 Shell 限制:Bash 支持递归函数——但每次递归调用都会创建子 Shell。$BASHPID 和 $$ 在递归中的行为如何暴露这个问题?Bash 递归的深度上限是多少?

#Shell#基础
上次更新: 2026/06/17, 12:47:39
Shell 入门与变量
数据与 IO 处理

← Shell 入门与变量 数据与 IO 处理→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式