Shell 入门与变量
# 第 1 章 Shell 入门与变量
# 目录介绍
# 1.1 Shell 简介与类型
# 1.1.1 Shell 是什么?
Shell 是操作系统最外层的"壳"——它接受你输入的命令,翻译给内核执行,然后把结果返回给你。
用户 (你)
│ 输入命令
▼
┌──────────────┐
│ Shell │ ← 命令解释器——翻译人类语言 → 系统调用
└──────────────┘
│ 系统调用
▼
┌──────────────┐
│ Kernel │ ← 内核——管理硬件、进程、文件
└──────────────┘
2
3
4
5
6
7
8
9
10
11
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
2
3
4
5
6
7
8
9
10
11
12
# 1.1.2 bash / zsh / sh 的区别
# 查看当前 Shell
echo $SHELL # /bin/zsh(macOS 默认)或 /bin/bash(Linux 默认)
# 查看系统安装了哪些 Shell
cat /etc/shells
# 查看 bash 版本
bash --version
2
3
4
5
6
7
8
主流 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 解释执行
2
# 1.1.3 第一个 Shell 脚本
#!/bin/bash
# hello.sh —— 第一个 Shell 脚本
echo "Hello, Shell!"
echo "当前用户:$USER"
echo "当前目录:$(pwd)"
echo "当前时间:$(date '+%Y-%m-%d %H:%M:%S')"
2
3
4
5
6
7
# ① 赋予执行权限(只需一次)
chmod +x hello.sh
# ② 运行
./hello.sh
# 输出:
# Hello, Shell!
# 当前用户:yangchong
# 当前目录:/home/yangchong
# 当前时间:2025-06-12 14:30:00
2
3
4
5
6
7
8
9
10
11
🔑 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)
2
3
4
5
6
# 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!
2
3
4
#!/usr/bin/env python3
# hello.py —— 加 shebang 后可以直接 ./hello.py 执行
print("这是 Python 写的 'Shell 脚本'")
2
3
🔑 #!/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
# ⚠️ 危险——永远不要对不可信的脚本这样做!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
四种方式对比表:
| 方式 | 开启新进程? | 需要 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: 管道中任一命令失败都算失败
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
# 演示 pipefail 的必要性
set -e
cat nonexistent.txt | wc -l # ❌ 不加 pipefail:cat 失败但 wc -l 成功,脚本继续
echo "这行仍然执行了——危险!" # ← 不应该执行到这里
set -eo pipefail
cat nonexistent.txt | wc -l # ✅ 加了 pipefail:cat 失败 → 整条管道失败 → 脚本退出
echo "这行不会执行" # ← 正确行为
2
3
4
5
6
7
8
# 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 不存在——空)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
🔑 单引号 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(拼接技巧)
2
3
4
5
6
7
8
# 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 # 当前用户名
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
常用环境变量速查:
| 变量 | 含义 | 示例值 |
|---|---|---|
$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"(每个参数独立——遍历安全)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 实战:脚本参数处理
#!/bin/bash
set -euo pipefail
if [ $# -lt 2 ]; then
echo "用法:$0 <输入文件> <输出目录>"
exit 1
fi
input="$1"
output_dir="$2"
echo "正在处理 $input → $output_dir/"
# ...
2
3
4
5
6
7
8
9
10
11
12
13
14
# 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 个文件"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
🔑 $() 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)
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
# 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 # 和 $(()) 结果一样
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
⚠️ 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
2
3
4
5
6
# 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[@]}" # 苹果 香蕉 葡萄 西瓜(索引不连续了)
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
🔑 关联数组(类似 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 = 深圳
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 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 文件!
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
🔑 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
2
3
4
5
6
7
8
9
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"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 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
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
案例知识融合:这个脚本覆盖了本章全部核心知识——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/[
2
3
4
5
6
7
8
9
10
11
12
13
陷阱 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
2
3
4
5
6
7
8
9
10
# 1.4 综合思考题
sourcevs./的本质差异:source script.sh不创建子进程——这意味着脚本可以修改当前 Shell 的环境变量(如PATH)。为什么~/.bashrc必须用source而不是./加载?如果反过来会怎样?set -e的"假安全":set -e会让脚本在命令失败时退出——但管道中最后一个命令失败不会触发(除非加-o pipefail)。还有哪些命令不触发set -e?(提示:if/while条件、&&/||左侧)$@vs$*的深度差异:"$@"展开为每个参数分别引号括起、"$*"展开为一个字符串——这导致在for arg in "$@"; do和for arg in "$*"; do中,引号内的空格会不会被分词。实验验证二者在处理./script.sh "a b" c时的输出差异。Shell 脚本的 Python 替代:超过 200 行的 Shell 脚本通常难以维护——没有真正的数据结构、错误处理原始、语法反直觉。什么情况下应该用 Python 替代 Shell 脚本?什么情况下 Shell 仍然是最优选择?
shebang 的历史与设计:
#!是 Dennis Ritchie 在 1980 年引入的——#是注释符、!是"魔法数"。同一行#!/bin/bash,Shell 看到的是#注释行(不执行),内核看到#!就知道要用/bin/bash来解释这个文件。除了#!外,还有哪些"同一行、不同角色看到不同含义"的计算机黑魔法?(提示:HTTP 的 Content-Type、ELF 文件头)