编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机组成原理
  • 操作系统原理
  • 网络协议原理
  • 数据库系统原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • ScriptHub 脚本工具箱
  • Python

  • Shell-Bash

    • Shell & Bash 从0到1实战专栏
    • Shell 入门与变量
      • 1.1 Shell 简介与类型
        • 1.1.1 Shell 是什么?
        • 1.1.2 bash / zsh / sh 的区别
        • 1.1.3 第一个 Shell 脚本
        • 1.1.4 shebang (#!) 的奥秘
        • 1.1.5 四种执行方式对比
        • 1.1.6 脚本调试三板斧
      • 1.2 变量与替换
        • 1.2.1 变量定义与引用
        • 1.2.2 环境变量 vs 局部变量
        • 1.2.3 特殊变量速查
        • 1.2.4 命令替换 $() 与反引号
        • 1.2.5 变量替换与默认值
        • 1.2.6 算术运算
        • 1.2.7 数组
        • 1.2.8 Here Document 与交互
        • 1.2.9 综合案例:系统巡检脚本
      • 1.3 新手陷阱 Top 5
      • 1.4 综合思考题
    • 流程控制与函数
    • 数据与 IO 处理
    • grep 搜索实战
    • sed 与 awk 编程
    • 文件查找与统计
    • 日志监控与告警
    • 备份进程与磁盘
    • 用户与服务管理
    • 网络调度与部署
    • 调试与脚本规范
    • 安全与兼容处理
    • 性能与打包分发
  • 工具脚本

  • 自动化脚本

  • ScriptHub
  • Shell-Bash
杨充
2022-06-17
目录

Shell 入门与变量

# 第 1 章 Shell 入门与变量

# 目录介绍

  • 1.1 Shell 简介与类型
    • 1.1.1 Shell 是什么?
    • 1.1.2 bash / zsh / sh 的区别
    • 1.1.3 第一个 Shell 脚本
    • 1.1.4 shebang (#!) 的奥秘
    • 1.1.5 四种执行方式对比
    • 1.1.6 脚本调试三板斧
  • 1.2 变量与替换
    • 1.2.1 变量定义与引用
    • 1.2.2 环境变量 vs 局部变量
    • 1.2.3 特殊变量速查
    • 1.2.4 命令替换 $() 与反引号
    • 1.2.5 变量替换与默认值
    • 1.2.6 算术运算
    • 1.2.7 数组
  • 1.3 新手陷阱 Top 5
  • 1.4 综合思考题

# 1.1 Shell 简介与类型

# 1.1.1 Shell 是什么?

Shell 是操作系统最外层的"壳"——它接受你输入的命令,翻译给内核执行,然后把结果返回给你。

用户 (你)
   │ 输入命令
   ▼
┌──────────────┐
│    Shell     │  ← 命令解释器——翻译人类语言 → 系统调用
└──────────────┘
   │ 系统调用
   ▼
┌──────────────┐
│    Kernel    │  ← 内核——管理硬件、进程、文件
└──────────────┘

Shell 脚本的价值:把一系列手动敲的命令写入文件——一次编写、反复执行、完全可重复。服务器运维、CI/CD 流水线、自动化部署——全部依赖 Shell 脚本。

# 手动操作——每次都要敲
cd /var/log
grep "ERROR" app.log | wc -l
tar -czf backup_$(date +%Y%m%d).tar.gz /data/

# 写成脚本——一行 cron 搞定
# check_errors.sh
#!/bin/bash
ERROR_COUNT=$(grep "ERROR" /var/log/app.log | wc -l)
if [ "$ERROR_COUNT" -gt 100 ]; then
    echo "⚠️ 错误数 $ERROR_COUNT——超过阈值!" | mail -s "告警" admin@company.com
fi

# 1.1.2 bash / zsh / sh 的区别

# 查看当前 Shell
echo $SHELL         # /bin/zsh(macOS 默认)或 /bin/bash(Linux 默认)

# 查看系统安装了哪些 Shell
cat /etc/shells

# 查看 bash 版本
bash --version

主流 Shell 对比:

Shell 全称 特点 默认系统
sh Bourne Shell 最古老、最基础——POSIX 标准的最小公共集 所有 Unix
bash Bourne Again Shell sh 的超集——功能丰富、最广泛使用 Linux
zsh Z Shell bash 的超集——更好的自动补全、主题、插件 macOS (10.15+)
dash Debian Almquist Shell 精简版——启动快,Debian 的 /bin/sh Debian/Ubuntu

🔑 写脚本用哪个? ——用 bash。它是 Linux/macOS/CI 环境的共同语言。zsh 交互体验最好(补全强),但脚本写 bash 兼容性最广。

# 每个脚本的第一行(§1.1.4 详解)
#!/bin/bash         # ← 明确声明:用 bash 解释执行

# 1.1.3 第一个 Shell 脚本

#!/bin/bash
# hello.sh —— 第一个 Shell 脚本

echo "Hello, Shell!"
echo "当前用户:$USER"
echo "当前目录:$(pwd)"
echo "当前时间:$(date '+%Y-%m-%d %H:%M:%S')"
# ① 赋予执行权限(只需一次)
chmod +x hello.sh

# ② 运行
./hello.sh

# 输出:
# Hello, Shell!
# 当前用户:yangchong
# 当前目录:/home/yangchong
# 当前时间:2025-06-12 14:30:00

🔑 chmod +x 干了什么?

ls -l hello.sh
# -rw-r--r--  1 user  group  123 Jun 12 14:30 hello.sh    ← 没有 x

chmod +x hello.sh
ls -l hello.sh
# -rwxr-xr-x  1 user  group  123 Jun 12 14:30 hello.sh    ← 有了 x(execute)

# 1.1.4 shebang (#!) 的奥秘

每个脚本第一行的 #!/bin/bash 叫 shebang——它告诉操作系统"用哪个程序来执行这个文件":

#!/bin/bash                # 用 bash 解释
#!/usr/bin/env bash        # 用 env 找到 bash(更可移植——推荐!)
#!/bin/zsh                 # 用 zsh 解释
#!/usr/bin/python3         # 用 Python 解释——Shell 脚本也可以是 Python!
#!/usr/bin/env python3
# hello.py —— 加 shebang 后可以直接 ./hello.py 执行
print("这是 Python 写的 'Shell 脚本'")

🔑 #!/bin/bash vs #!/usr/bin/env bash:

#!/bin/bash #!/usr/bin/env bash
bash 路径 写死 /bin/bash 由 env 在 PATH 中搜索
可移植性 macOS 可能没有 /bin/bash 最新版 ✅ 到处能用(只要 PATH 里有 bash)
推荐 绝对路径明确 ✅ 推荐——更可移植

# 1.1.5 四种执行方式对比

# ===== 方式 ①:./script.sh —— 子进程执行(最常用) =====
./hello.sh
# 开一个子 Shell,在子 Shell 里执行脚本
# 脚本里的变量不会污染当前 Shell

# ===== 方式 ②:bash script.sh —— 显式调用解释器 =====
bash hello.sh
# 和 ./hello.sh 效果相同——但不需要执行权限

# ===== 方式 ③:source script.sh 或 . script.sh —— 当前 Shell 执行 =====
source hello.sh      # 或
. hello.sh
# ⚠️ 在当前 Shell 中执行——脚本里的变量会留在当前环境!
# 常用于加载环境变量:source ~/.bashrc

# ===== 方式 ④:管道——把脚本内容塞给 bash =====
curl -s https://example.com/setup.sh | bash
# ⚠️ 危险——永远不要对不可信的脚本这样做!

四种方式对比表:

方式 开启新进程? 需要 x 权限? 变量污染? 场景
./script.sh ✅ ✅ ❌ 执行脚本
bash script.sh ✅ ❌ ❌ 调试/无权限
source script.sh ❌ ❌ ✅ 加载环境变量
curl ... \| bash ✅ ❌ ❌ 远程安装(危险!)

# 1.1.6 脚本调试三板斧

Shell 脚本难调试——没有 IDE 断点、没有漂亮的调用栈。但有三招能定位 90% 的问题:

#!/bin/bash

# ===== 第一招:set -x —— 打印每条执行命令 =====
set -x
name="张三"
echo "Hello, $name"
set +x                       # 关闭调试模式
# + name=张三
# + echo 'Hello, 张三'
# Hello, 张三
# + set +x

# ===== 第二招:set -e —— 遇错即停 =====
set -e                       # 任何一条命令返回非 0 就退出
cd /nonexistent               # ← 这条失败了——脚本立即退出,不会继续
echo "不会执行到这里"

# ===== 第三招:set -u —— 未定义变量报错 =====
set -u
echo "$UNDEFINED_VAR"        # ← bash: UNDEFINED_VAR: unbound variable

# ===== 生产级脚本开头 =====
#!/bin/bash
set -euo pipefail             # 三合一:遇错停 + 未定义报错 + 管道失败也报错
# -e: 命令失败停止
# -u: 使用未定义变量报错
# -o pipefail: 管道中任一命令失败都算失败
# 演示 pipefail 的必要性
set -e
cat nonexistent.txt | wc -l    # ❌ 不加 pipefail:cat 失败但 wc -l 成功,脚本继续
echo "这行仍然执行了——危险!"    # ← 不应该执行到这里

set -eo pipefail
cat nonexistent.txt | wc -l    # ✅ 加了 pipefail:cat 失败 → 整条管道失败 → 脚本退出
echo "这行不会执行"             # ← 正确行为

# 1.2 变量与替换

# 1.2.1 变量定义与引用

Shell 变量的核心规则——等号两边不能有空格(这是 Shell 最反直觉的规矩):

#!/bin/bash

# ===== 定义 =====
name="张三"                    # ✅ 等号两边无空格——Shell 铁律!
age=25
readonly PI=3.14159           # 只读变量——不能修改
# PI=3.14                     # ❌ bash: PI: readonly variable

# ===== 引用——必须加 $ =====
echo $name                    # 张三
echo "我的名字是 $name"       # 双引号内变量展开
echo '我的名字是 $name'       # 单引号内——不展开!输出「我的名字是 $name」

# ===== 删除变量 =====
unset name
echo "name = ${name:-未定义}"  # name = 未定义

# ===== 花括号——消除歧义 =====
animal="dog"
echo "I have 4 ${animal}s"     # I have 4 dogs(花括号告诉 Shell 变量名边界)
echo "I have 4 $animals"      # I have 4 ($animals 不存在——空)

🔑 单引号 vs 双引号——Shell 最重要的概念之一:

name="张三"
echo "Hello $name"            # Hello 张三——双引号:变量展开、转义生效
echo 'Hello $name'            # Hello $name——单引号:一切原样输出
echo Hello $name              # Hello 张三——无引号:变量展开,但空格导致分词

# 区别在这一点上最明显:
echo "$USER's home is $HOME"  # yangchong's home is /home/yangchong
echo '$USER'"'s home is $HOME" # $USER's home is /home/yangchong(拼接技巧)

# 1.2.2 环境变量 vs 局部变量

#!/bin/bash

# ===== 局部变量——仅当前 Shell 可见 =====
local_var="只有我能看见"

# ===== 环境变量——子进程也能看到 =====
export GLOBAL_VAR="我和我的子进程都能看见"
# 或两行:GLOBAL_VAR="..." ; export GLOBAL_VAR

# ===== 验证——在子 Shell 中查看 =====
bash -c 'echo "子进程中:local_var=$local_var, GLOBAL_VAR=$GLOBAL_VAR"'
# 输出:子进程中:local_var=, GLOBAL_VAR=我和我的子进程都能看见
#               ↑ 看不到!          ↑ 能看到!

# ===== 查看所有环境变量 =====
env                # 或 printenv
echo $PATH         # 最重要——命令搜索路径
echo $HOME         # 用户主目录
echo $USER         # 当前用户名

常用环境变量速查:

变量 含义 示例值
$HOME 用户主目录 /home/yangchong
$PATH 命令搜索路径 /usr/bin:/bin:/usr/local/bin
$USER 当前用户名 yangchong
$PWD 当前工作目录 /home/yangchong/project
$SHELL 当前 Shell /bin/zsh
$LANG 语言/区域 zh_CN.UTF-8
$PS1 命令提示符 \u@\h:\w$
$? 上条命令退出码 0 成功 / 非 0 失败

# 1.2.3 特殊变量速查

Shell 提供了一组以 $ 开头的特殊变量——脚本参数和状态的全部信息:

#!/bin/bash
# special_vars.sh

echo "脚本名:$0"              # ./special_vars.sh
echo "第 1 个参数:$1"         # 运行时 ./special_vars.sh a b c → a
echo "第 2 个参数:$2"         # b
echo "参数个数:$#"            # 3
echo "所有参数(空格分隔):$*"  # a b c
echo "所有参数(独立引号):$@"  # a b c(配合 for 用——见下)
echo "上一个命令的退出码:$?"   # 0
echo "当前 Shell PID:$$"      # 12345

# $* vs $@ ——在带引号时有本质区别
# $* = "a b c"(所有参数合并成一个字符串)
# $@ = "a" "b" "c"(每个参数独立——遍历安全)
# 实战:脚本参数处理
#!/bin/bash
set -euo pipefail

if [ $# -lt 2 ]; then
    echo "用法:$0 <输入文件> <输出目录>"
    exit 1
fi

input="$1"
output_dir="$2"

echo "正在处理 $input → $output_dir/"
# ...

# 1.2.4 命令替换 $() 与反引号

命令替换 = 把命令的输出当作字符串塞进变量:

#!/bin/bash

# ===== 方式 1:$() —— 推荐!支持嵌套 =====
today=$(date '+%Y-%m-%d')
file_count=$(ls | wc -l)
line_count=$(wc -l < /var/log/app.log)     # < 防止文件名也输出

# 嵌套——$() 天然支持
nested=$(echo "文件数:$(ls | wc -l)")

# ===== 方式 2:反引号 `` —— 不推荐——嵌套困难 =====
today=`date '+%Y-%m-%d'`                   # 和 $() 效果相同
# nested=`echo "文件数:\`ls | wc -l\`"`   # 嵌套需要转义——可读性差

echo "今天是 $today,当前目录有 $file_count 个文件"

🔑 $() vs 反引号:

$() 反引号 ``
嵌套 $(echo $(date))——自然 需要转义
可读性 ✅ 一眼看出起止 ❌ 容易漏看
所有 POSIX Shell sh 不支持(bash 支持) sh/bash 都支持

📌 新代码一律用 $()。

# 1.2.5 变量替换与默认值

Shell 的 ${} 远不止消除歧义——它有一整套变量替换语法:

#!/bin/bash
name="张三"

# ===== 1. 提供默认值(变量未定义/为空时使用)=====
echo "姓名:${name:-匿名}"       # name 有值 → 张三
unset name
echo "姓名:${name:-匿名}"       # name 无值 → 匿名

# ===== 2. 赋值默认值(:=——同时赋值给变量)=====
unset count
echo "数量:${count:=10}"        # 10——并且 count 被赋值为 10
echo "count=$count"             # count=10

# ===== 3. 变量必须存在(:?——否则报错退出)=====
# echo "${REQUIRED_VAR:?必须设置 REQUIRED_VAR}"  # bash: REQUIRED_VAR: 必须设置 REQUIRED_VAR

# ===== 4. 有值就用另一个值(:+——替代值)=====
name="张三"
echo "欢迎 ${name:+尊敬的 $name}" # name 有值 → 尊敬的 张三
unset name
echo "欢迎 ${name:+尊敬的 $name}" # name 无值 → (空)

# ===== 5. 字符串操作 =====
file="backup_2025.tar.gz"
echo "${file%.tar.gz}"           # backup_2025(从末尾删除最短匹配)
echo "${file%%.*}"               # backup_2025(从末尾删除最长匹配)
echo "${file#backup_}"           # 2025.tar.gz(从开头删除最短匹配)
echo "${file##*_}"               # 2025.tar.gz(从开头删除最长匹配)
echo "${#file}"                  # 19——字符串长度
echo "${file:0:6}"               # backup——子串(位置 0,长度 6)

# 1.2.6 算术运算

Shell 的算术运算不能直接写 x = 1 + 1——需要用 $(( )):

#!/bin/bash

# ===== $(( )) —— 算术运算(只支持整数!)=====
a=10
b=3
echo $((a + b))          # 13
echo $((a - b))          # 7
echo $((a * b))          # 30
echo $((a / b))          # 3(整数除法)
echo $((a % b))          # 1(取余)
echo $((a ** b))         # 1000(幂——a 的 b 次方)

# 变量前缀 $ 可省略
echo $((a + 5))          # 15

# 自增/自减
((a++))
echo $a                  # 11
((a += 5))
echo $a                  # 16

# if 里的条件判断——用 (( ))
if ((a > 10)); then
    echo "a 大于 10"
fi

# ===== let 命令(老语法——不推荐)=====
let "c = a + b"
echo $c                  # 和 $(()) 结果一样

⚠️ Shell 没有原生浮点运算——需要用外部工具:

# bc 计算器——最常用
echo "scale=2; 10 / 3" | bc       # 3.33
pi=$(echo "scale=4; 4*a(1)" | bc -l)  # 3.1416

# awk
awk 'BEGIN {print 10/3}'          # 3.33333

# 1.2.7 数组

Bash 支持一维数组——语法和 Python 差别很大:

#!/bin/bash

# ===== 定义 =====
fruits=("苹果" "香蕉" "橘子" "葡萄")       # 小括号,空格分隔
nums=(1 2 3 4 5)
mixed=("hello" 42 "world")                 # 可以混类型(但不推荐)

# 也可逐个赋值
fruits[4]="西瓜"                            # 追加到索引 4

# ===== 访问 =====
echo "${fruits[0]}"            # 苹果(第一个元素)
echo "${fruits[1]}"            # 香蕉
echo "${fruits[-1]}"           # 西瓜(最后一个——bash 4.2+)

# ===== 遍历 =====
echo "--- 遍历方式 1:取所有元素 ---"
echo "${fruits[@]}"            # 苹果 香蕉 橘子 葡萄 西瓜

echo "--- 遍历方式 2:for 循环 ---"
for fruit in "${fruits[@]}"; do
    echo "  水果:$fruit"
done

# ===== 数组长度 =====
echo "数组长度:${#fruits[@]}"  # 5

# ===== 切片 =====
echo "${fruits[@]:1:2}"        # 香蕉 橘子(从索引 1 开始,取 2 个)

# ===== 删除 =====
unset fruits[2]                # 删除索引 2(橘子)
echo "${fruits[@]}"            # 苹果 香蕉 葡萄 西瓜(索引不连续了)

🔑 关联数组(类似 Python dict)——bash 4.0+:

#!/bin/bash

# 声明关联数组——必须用 declare -A
declare -A student
student["name"]="张三"
student["age"]="25"
student["city"]="深圳"

echo "姓名:${student[name]}"        # 张三
echo "年龄:${student[age]}"         # 25

# 遍历关联数组
for key in "${!student[@]}"; do
    echo "  $key = ${student[$key]}"
done
#   name = 张三
#   age = 25
#   city = 深圳

# 1.2.8 Here Document 与交互

<< (Here Document)让你在脚本里嵌入多行文本——告别一堆 echo:

#!/bin/bash

# ===== Here Document:多行输出 =====
cat <<EOF
这是第一行
这是第二行
变量也会被展开:当前用户是 $USER
EOF

# 输出:
# 这是第一行
# 这是第二行
# 变量也会被展开:当前用户是 yangchong

# ===== 阻止变量展开——定界符加引号 =====
cat <<'EOF'
变量不会被展开:当前用户是 $USER
EOF
# 输出:变量不会被展开:当前用户是 $USER

# ===== <<- 可选:自动忽略行首 Tab =====
if true; then
	cat <<-EOF
		这行的 Tab 会被去掉
		这一行也是
	EOF
fi
# 输出(无缩进):
# 这行的 Tab 会被去掉
# 这一行也是

# ===== 实战:生成配置文件 =====
port=8080
db_name="myapp"
cat > config.yaml <<EOF
server:
  port: $port
database:
  name: $db_name
  host: localhost
EOF
# 直接写入 config.yaml 文件!

🔑 Here String <<<——把字符串当作 stdin 传给命令:

# 不用 echo + 管道
echo "hello world" | wc -c                   # 12

# 用 Here String——更简洁
wc -c <<< "hello world"                      # 12

# 实战:把变量传给命令
read -r first rest <<< "apple banana orange"
echo "first=$first, rest=$rest"              # first=apple, rest=banana orange

read 命令——带提示的输入:

#!/bin/bash

# 基础输入
echo -n "请输入你的名字:"
read -r name
echo "你好,$name!"

# 一行搞定——-p 提示
read -r -p "请输入年龄:" age
echo "你 ${age} 岁了"

# 限定最大字符数——密码输入
read -r -s -p "请输入密码:" password        # -s:输入不显示(silent)
echo ""                                      # 换行
echo "密码已接收(长度 ${#password})"

# 按分隔符拆分
read -r -p "输入姓名和年龄(空格分隔):" name age
echo "姓名:$name,年龄:$age"

# 1.2.9 综合案例:系统巡检脚本

把本章所有知识串联成一个生产级脚本——每天自动检查系统健康状态:

#!/bin/bash
# system_check.sh —— 系统巡检脚本
# 用法:./system_check.sh [输出文件名]
# 示例:./system_check.sh report_$(date +%Y%m%d).txt

set -euo pipefail

# ===== 1. 配置:变量 + 默认值 =====
OUTPUT_FILE="${1:-report.txt}"               # 输出文件名——默认 report.txt
HOSTNAME=$(hostname)                          # 主机名(命令替换)
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')       # 时间戳

# 阈值配置(环境变量可覆盖)
CPU_THRESHOLD="${CPU_THRESHOLD:-80}"          # CPU 使用率超过 80% 告警
MEM_THRESHOLD="${MEM_THRESHOLD:-85}"          # 内存使用率超过 85% 告警
DISK_THRESHOLD="${DISK_THRESHOLD:-90}"        # 磁盘使用率超过 90% 告警
LOG_KEEP_DAYS="${LOG_KEEP_DAYS:-30}"          # 日志保留天数

# ===== 2. 函数:带颜色输出 =====
# 颜色变量
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'                     # No Color——恢复默认

log_info()  { echo -e "${GREEN}[INFO]${NC}  $*"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC}  $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*"; }

# ===== 3. 检查函数 =====
check_cpu() {
    # 获取 CPU 使用率(取 100 - idle)
    local cpu_idle
    cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}' | cut -d'.' -f1)
    local cpu_usage=$((100 - cpu_idle))

    echo "CPU 使用率:${cpu_usage}%"
    if (( cpu_usage > CPU_THRESHOLD )); then
        log_warn "CPU 使用率 ${cpu_usage}% 超过阈值 ${CPU_THRESHOLD}%!"
    else
        log_info "CPU 使用率正常"
    fi
}

check_memory() {
    # 从 free 命令提取内存使用率
    local total used usage_pct
    read -r _ total used _ <<< "$(free -m | grep Mem)"
    usage_pct=$(( used * 100 / total ))

    echo "内存使用率:${usage_pct}%(已用 ${used}MB / 总量 ${total}MB)"
    if (( usage_pct > MEM_THRESHOLD )); then
        log_warn "内存使用率 ${usage_pct}% 超过阈值 ${MEM_THRESHOLD}%!"
    else
        log_info "内存使用率正常"
    fi
}

check_disk() {
    # 检查根分区使用率
    local usage_pct
    usage_pct=$(df -h / | tail -1 | awk '{print $5}' | sed 's/%//')

    echo "磁盘使用率:${usage_pct}%"
    if (( usage_pct > DISK_THRESHOLD )); then
        log_error "磁盘使用率 ${usage_pct}% 超过阈值 ${DISK_THRESHOLD}%——需要立即清理!"
    elif (( usage_pct > DISK_THRESHOLD - 10 )); then
        log_warn "磁盘使用率 ${usage_pct}% 接近阈值——建议清理"
    else
        log_info "磁盘空间充足"
    fi
}

check_services() {
    # 检查关键服务是否在运行
    local services=("sshd" "nginx" "cron")
    local failed=()

    for svc in "${services[@]}"; do
        if systemctl is-active --quiet "$svc" 2>/dev/null; then
            echo "  ✅ $svc"
        else
            echo "  ❌ $svc(未运行!)"
            failed+=("$svc")
        fi
    done

    if [ ${#failed[@]} -gt 0 ]; then
        log_error "以下服务未运行:${failed[*]}"
    else
        log_info "所有关键服务正常运行"
    fi
}

check_logs() {
    # 检查是否有异常日志(最近 1 小时)
    local recent_errors
    recent_errors=$(find /var/log -name "*.log" -mmin -60 \
        -exec grep -l "ERROR\|FATAL\|CRITICAL" {} \; 2>/dev/null | wc -l)

    echo "最近 1 小时出现 ERROR 的日志文件数:${recent_errors}"
    if (( recent_errors > 0 )); then
        log_warn "发现 ${recent_errors} 个日志文件包含 ERROR——请检查详情"
    else
        log_info "近期无 ERROR 日志"
    fi
}

cleanup_old_logs() {
    # 清理旧日志
    local count
    count=$(find /var/log -name "*.log" -mtime "+${LOG_KEEP_DAYS}" 2>/dev/null | wc -l)
    echo "超过 ${LOG_KEEP_DAYS} 天的日志文件:${count} 个"
}

# ===== 4. 主流程 =====
{
    echo "========================================"
    echo "  系统巡检报告"
    echo "  主机:${HOSTNAME}"
    echo "  时间:${TIMESTAMP}"
    echo "========================================"
    echo ""

    echo "【CPU 检查】"
    check_cpu
    echo ""

    echo "【内存检查】"
    check_memory
    echo ""

    echo "【磁盘检查】"
    check_disk
    echo ""

    echo "【服务检查】"
    check_services
    echo ""

    echo "【日志检查】"
    check_logs
    echo ""

    echo "【清理建议】"
    cleanup_old_logs
    echo ""

    echo "========================================"
    echo "  巡检结束"
    echo "========================================"
} | tee "$OUTPUT_FILE"   # tee:同时输出到屏幕和文件

echo ""
log_info "报告已保存至:$OUTPUT_FILE"

# 退出码:0=健康,1=有告警
if grep -q "WARN\|ERROR" "$OUTPUT_FILE"; then
    exit 1    # 有告警——退出码 1(CI 流水线可以根据这个判断失败)
else
    exit 0    # 一切正常
fi

案例知识融合:这个脚本覆盖了本章全部核心知识——shebang + set -euo pipefail 三板斧、变量定义/引用/默认值/环境变量/花括号、命令替换 $()、$1 参数处理、局部变量 local、数组定义/遍历/长度、算术运算 $(())、特殊变量 $? 退出码——100 行完成了一个可直接用于生产环境的系统巡检脚本。

🔑 学完这个案例你就掌握了 Shell 脚本的"骨架模式"——所有 Shell 脚本都是:配置变量 → 定义函数 → 主流程调用。

# 1.3 新手陷阱 Top 5

# 陷阱 说明
1 等号两边有空格 name = "张三" → Shell 把 name 当命令执行。必须是 name="张三"
2 变量引用忘加 $ echo name 打印的是字面量 name,不是变量值。echo $name 才对
3 [ 两边必须有空格 if [ $a == $b ]——[ 是一个命令![$a 会被当作一条命令
4 单引号阻止变量展开 echo '$USER' → $USER(原样输出)。变量展开用双引号
5 未加引号的变量被分词 for f in $files——如果文件名包含空格会被拆散。用 "$files" 或 "${array[@]}"

陷阱 3 详解——[ 是一个命令:

# ❌ 少空格——bash 报错
if [$a == $b ]; then   # bash: [a: command not found
    echo "相等"
fi

# ✅ 正确——[ 是 test 命令的别名,必须用空格和参数分开
if [ "$a" == "$b" ]; then
    echo "相等"
fi

# 验证:[ 真的是一个命令
which [                # /bin/[
ls -l /bin/[           # -rwxr-xr-x  /bin/[

陷阱 5 详解——空格分词:

# ❌ 没有引号——文件名包含空格就炸了
files=$(ls)                          # file1.txt  my doc.pdf  photo.jpg
for f in $files; do                  # 迭代:file1.txt → my → doc.pdf → photo.jpg(被拆散了!)
    echo "处理:$f"
done

# ✅ 加引号——正确处理空格
for f in *.pdf; do                   # 直接用 glob——原生支持空格
    echo "处理:$f"
done

# 1.4 综合思考题

  1. source vs ./ 的本质差异:source script.sh 不创建子进程——这意味着脚本可以修改当前 Shell 的环境变量(如 PATH)。为什么 ~/.bashrc 必须用 source 而不是 ./ 加载?如果反过来会怎样?

  2. set -e 的"假安全":set -e 会让脚本在命令失败时退出——但管道中最后一个命令失败不会触发(除非加 -o pipefail)。还有哪些命令不触发 set -e?(提示:if / while 条件、&& / || 左侧)

  3. $@ vs $* 的深度差异:"$@" 展开为每个参数分别引号括起、"$*" 展开为一个字符串——这导致在 for arg in "$@"; do 和 for arg in "$*"; do 中,引号内的空格会不会被分词。实验验证二者在处理 ./script.sh "a b" c 时的输出差异。

  4. Shell 脚本的 Python 替代:超过 200 行的 Shell 脚本通常难以维护——没有真正的数据结构、错误处理原始、语法反直觉。什么情况下应该用 Python 替代 Shell 脚本?什么情况下 Shell 仍然是最优选择?

  5. shebang 的历史与设计:#! 是 Dennis Ritchie 在 1980 年引入的——# 是注释符、! 是"魔法数"。同一行 #!/bin/bash,Shell 看到的是 # 注释行(不执行),内核看到 #! 就知道要用 /bin/bash 来解释这个文件。除了 #! 外,还有哪些"同一行、不同角色看到不同含义"的计算机黑魔法?(提示:HTTP 的 Content-Type、ELF 文件头)

#Shell#基础
上次更新: 2026/06/28, 17:55:19
Shell & Bash 从0到1实战专栏
流程控制与函数

← Shell & Bash 从0到1实战专栏 流程控制与函数→

最近更新
01
科学方法实践论法
06-28
02
辩证思维矛盾论法
06-28
03
毛选中的调查观念
06-28
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式