编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • 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 入门与变量
      • 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    │  ← 内核——管理硬件、进程、文件
└──────────────┘
1
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
1
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
1
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 解释执行
1
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')"
1
2
3
4
5
6
7
# ① 赋予执行权限(只需一次)
chmod +x hello.sh

# ② 运行
./hello.sh

# 输出:
# Hello, Shell!
# 当前用户:yangchong
# 当前目录:/home/yangchong
# 当前时间:2025-06-12 14:30:00
1
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)
1
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!
1
2
3
4
#!/usr/bin/env python3
# hello.py —— 加 shebang 后可以直接 ./hello.py 执行
print("这是 Python 写的 'Shell 脚本'")
1
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
# ⚠️ 危险——永远不要对不可信的脚本这样做!
1
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: 管道中任一命令失败都算失败
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
# 演示 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
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 不存在——空)
1
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(拼接技巧)
1
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         # 当前用户名
1
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"(每个参数独立——遍历安全)
1
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/"
# ...
1
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 个文件"
1
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)
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

# 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                  # 和 $(()) 结果一样
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

⚠️ 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
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[@]}"            # 苹果 香蕉 葡萄 西瓜(索引不连续了)
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

🔑 关联数组(类似 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
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 文件!
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

🔑 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
1
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"
1
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
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
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/[
1
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
1
2
3
4
5
6
7
8
9
10

# 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/17, 12:47:39
Shell & Bash 从0到1实战专栏
流程控制与函数

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

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