📚前言
📒FDE系列内容总纲:
🚄前置课程列表:
见文档结尾附录。
🚀阶段2·Day 38:Shell 脚本 --- 把命令串起来自动跑
FDE 学习系列教程 · 第二阶段 · 第 4 周 · Day 3 预计时长:3 小时 | 难度:★★★☆☆ | 前置知识:Day 36-37 文件操作、权限、grep
📌 一句话目标:掌握 Shell 脚本的变量、参数、条件、循环、函数和退出码,写出一个带参数校验、自动打包、日期命名的备份脚本。
🧑🤝🧑 开场:从"手敲命令"到"写程序指挥命令"
前两天你已经能在终端里呼风唤雨了。但发现没有------有些操作你在反复敲:
-
每天备份:
tar -czf ...、ls -lh、记日志,五条命令敲一遍 -
每次部署:拉代码、停服务、装依赖、启服务、健康检查,八条命令
-
每周巡检:查磁盘、查进程、查端口......
人不怕一次干得多,就怕同样的事干一百遍。漏敲一条、顺序错一步,就要排障半天。
Shell 脚本就是解决这个的:把一串命令写进一个文件,配上变量、判断、循环,以后敲一次脚本名,全套动作自动跑完。
没有脚本: 有脚本:
───────── ──────
$ tar -czf ... $ ./backup.sh ~/fde_project
$ ls -lh ... ✅ 备份完成
$ echo 时间 >> log.log 文件:fde_project_backup_20260918.tar.gz
$ 检查目录在不在... 大小:2.1M
(10 个动作全靠脑子记) (一条命令,脚本自己全干了)
今天难度比前两天高一些,语法细节多,别想着一次全背下来------先跟着敲通,理解"脚本能干嘛",细节以后随用随查。
📖 一、Shell 脚本长什么样
第一个脚本:hello.sh
在 ~/fde_lab 里新建文件(可以用 nano hello.sh,或在 VS Code 里编辑):
#!/bin/bash
# ==========================================
# 我的第一个 Shell 脚本
# # 开头的行都是注释,不会执行
# ==========================================
echo "Hello, FDE!"
echo "当前用户:$(whoami)"
echo "当前目录:$(pwd)"
echo "今天是:$(date '+%Y-%m-%d %H:%M')"
逐行解释:
| 代码 | 含义 |
|---|---|
#!/bin/bash |
shebang:告诉系统"用 /bin/bash 来解释执行这个脚本"。脚本第一行固定这么写 |
# 注释 |
井号开头是注释,给人看的,机器忽略 |
echo "..." |
往屏幕打印一行文字(相当于 Python 的 print) |
$(命令) |
命令替换:先执行括号里的命令,把输出结果塞回字符串里 |
运行脚本的两种方式
# 方式一:加执行权限后直接运行(推荐,正式姿势)
chmod +x hello.sh
./hello.sh
# Hello, FDE!
# 当前用户:fde
# 当前目录:/home/fde/fde_lab
# 今天是:2026-09-18 14:30
# 方式二:用 bash 解释器直接跑(不需要 x 权限,临时测试方便)
bash hello.sh
⚠️ 为什么是
./hello.sh而不是hello.sh? 因为 Linux 默认不会在"当前目录"里找命令(只在 PATH 环境变量指定的系统目录找)。加./等于明确告诉系统"我要运行当前目录下这个文件"。
date 命令:备份脚本的"日期生成器"
今天的实战脚本要用,先单独练熟:
date '+%Y-%m-%d' # 2026-09-18
date '+%Y%m%d_%H%M%S' # 20260918_143052(备份文件命名用这个)
date '+%Y-%m-%d %H:%M:%S' # 2026-09-18 14:30:52(日志时间戳用这个)
格式符:%Y年 %m月 %d日 %H时 %M分 %S秒。
📖 二、变量:Shell 里存东西
Shell 变量语法和 Python 很不一样,等号两边绝对不能有空格,这是新手第一大坑:
# ===== 定义变量 =====
NAME="工单系统" # ⭐ 等号两边紧贴,写 NAME = "x" 会报错
VERSION=1.0
# ===== 使用变量:加 $ =====
echo "$NAME v$VERSION" # 工单系统 v1.0
echo "${NAME}_backup" # 加花括号明确变量边界:工单系统_backup
# ===== 命令替换:把命令输出存进变量 =====
TODAY=$(date '+%Y%m%d')
FILE_COUNT=$(ls *.sh | wc -l)
echo "今天是 $TODAY,当前目录有 $FILE_COUNT 个脚本"
❌ 错误写法 ✅ 正确写法
NAME = "工单系统" NAME="工单系统"
(Shell 会以为 NAME 是个命令)
echo "我的名字是 NAME" echo "我的名字是 $NAME"
(没加 $,打印出原文 NAME)
echo "$NAME_backup" echo "${NAME}_backup"
(系统找名为 NAME_backup 的变量) (花括号划清边界)
引号的门道(双引号 vs 单引号)
NAME="工单系统"
echo "你好 $NAME" # 你好 工单系统 ← 双引号:变量会被替换
echo '你好 $NAME' # 你好 $NAME ← 单引号:原样输出,不替换
echo "今天 $(date +%Y)" # 双引号里命令替换也生效
💡 经验法则:字符串一律用双引号包起来(尤其含变量、含空格、含路径时)。单引号只在你明确想"原样输出、啥都不解析"时用。
给变量设默认值
BACKUP_DIR="${2:-./backups}" # 如果第2个参数没给,就用 ./backups
echo "${LEVEL:-普通}" # LEVEL 没定义时显示"普通"
# ${变量:-默认值} 这个写法今天备份脚本就会用
📖 三、位置参数:让脚本接收外部输入
脚本想复用,就得能接收参数,就像 Python 脚本里的 sys.argv。
新建 args.sh:
#!/bin/bash
echo "脚本自己叫:$0"
echo "第 1 个参数:$1"
echo "第 2 个参数:$2"
echo "第 3 个参数:$3"
echo "参数总个数:$#"
echo "所有参数:$@"
chmod +x args.sh
./args.sh /home/fde /backup 强制模式
# 脚本自己叫:./args.sh
# 第 1 个参数:/home/fde
# 第 2 个参数:/backup
# 第 3 个参数:强制模式
# 参数总个数:3
# 所有参数:/home/fde /backup 强制模式
特殊参数速查:
| 符号 | 含义 |
|---|---|
$0 |
脚本自己的名字 |
$1 $2 ... |
第 1、2...个参数 |
$# |
参数个数 |
$@ |
所有参数的列表(循环时用) |
$? |
上一条命令的退出码(下面细讲) |
$$ |
当前脚本的进程号 |
📖 四、退出码:命令在"说"自己成功没有
每条命令执行完都会留一个数字,存在 $? 里,表示执行结果:
ls /home # 成功
echo $? # 0 ← 0 表示成功
ls /一个不存在的目录
echo $? # 2 ← 非 0 表示失败(不同数字代表不同错误)
grep "不存在的字" hello.sh
echo $? # 1 ← grep 没匹配到也算非0
┌──────────────────────────────────────┐
│ 退出码约定: │
│ 0 = 成功 │
│ 非 0 = 失败(1/2/126/127...) │
│ │
│ 脚本用法: │
│ exit 0 正常结束 │
│ exit 1 异常退出(参数错等) │
└──────────────────────────────────────┘
这给了脚本"判断成败"的能力------比如健康检查脚本就是根据 curl 的退出码判断服务死活。明天会大量用到。
📖 五、条件判断:if
基本结构
if [ 条件 ]; then
条件成立时做的事
elif [ 另一个条件 ]; then
另一个条件成立时
else
都不成立时
fi
🚨 头号语法坑 :
[后面和]前面必须有空格 !if ["$1" = ""]会直接报语法错误。因为[在 Shell 里其实是个命令(test 的简写),它需要空格把自己和参数分开。
字符串判断
if [ "$1" = "" ]; then
echo "你没传参数!"
exit 1
fi
if [ "$1" = "start" ]; then
echo "准备启动"
fi
if [ "$1" != "start" ]; then
echo "参数不是 start"
fi
# 判空更专业的写法
if [ -z "$1" ]; then # -z = zero length,字符串长度为0
echo "参数为空"
fi
if [ -n "$1" ]; then # -n = 非空
echo "收到参数:$1"
fi
文件/目录判断(备份脚本的核心校验)
if [ -d "$DIR" ]; then # -d directory:存在且是目录
echo "$DIR 是个目录"
fi
if [ -f "$FILE" ]; then # -f file:存在且是普通文件
echo "$FILE 是个文件"
fi
if [ ! -e "$PATH" ]; then # -e exists:存在;! 表示"非"
echo "$PATH 不存在"
fi
if [ -r "$FILE" ]; then # -r 可读 / -w 可写 / -x 可执行
echo "文件可读"
fi
| 测试符 | 含义 |
|---|---|
-d |
是目录 |
-f |
是普通文件 |
-e |
存在(不管类型) |
-r -w -x |
可读/可写/可执行 |
-z |
字符串为空 |
-n |
字符串非空 |
= != |
字符串相等/不等 |
数字比较(注意不是 > <)
if [ "$COUNT" -gt 5 ]; then ... # -gt greater than 大于
if [ "$COUNT" -ge 5 ]; then ... # -ge 大于等于
if [ "$COUNT" -lt 5 ]; then ... # -lt 小于
if [ "$COUNT" -le 5 ]; then ... # -le 小于等于
if [ "$COUNT" -eq 5 ]; then ... # -eq 等于
if [ "$COUNT" -ne 5 ]; then ... # -ne 不等于
⚠️ 数字比较用
-eq -lt -gt这套,不能直接写><(那俩在 Shell 里是重定向符号)。字符串相等才用=。
用 && 和 || 写简单条件
[ -d /home/fde ] && echo "目录在" # 前面成功才执行后面
[ -f a.txt ] || echo "文件不存在" # 前面失败才执行后面
mkdir -p /tmp/test && cd /tmp/test # 建目录成功后才进去(链式保证)
📖 六、循环:for 和 while
for:遍历一个清单
# 遍历文件
for file in *.sh; do
echo "找到脚本:$file"
done
# 遍历给定列表
for name in 李工 王工 赵工; do
echo "通知:$name 你好"
done
# 遍历数字序列
for i in 1 2 3 4 5; do
echo "第 $i 次巡检"
done
# 更灵活的数字序列用 {起始..结束}
for i in {1..5}; do
echo "编号 $i"
done
# 遍历脚本收到的所有参数
for arg in "$@"; do
echo "处理参数:$arg"
done
while:条件为真就一直转
COUNT=0
while [ $COUNT -lt 5 ]; do
COUNT=$((COUNT + 1)) # $(( )) 是整数运算
echo "计数:$COUNT"
done
整数运算语法:
echo $((3 + 2)) # 5
NUM=10
NUM=$((NUM + 1)) # 自增
echo $((NUM * 2)) # 22
实战小例子:批量改名
# 给当前目录所有 .txt 文件复制一份 .bak
for f in *.txt; do
cp "$f" "${f}.bak"
echo "已备份 $f → ${f}.bak"
done
📖 七、函数:把重复动作打包
函数让脚本有结构、能复用,和 Python 的函数思想一样:
# 定义函数(function 关键字可省略)
log_info() {
echo "[$(date '+%H:%M:%S')] [INFO] $1"
}
log_error() {
echo "[$(date '+%H:%M:%S')] [ERROR] $1" >&2 # >&2 输出到错误流
}
# 调用(函数内部也用 $1 $2 接参数)
log_info "脚本开始"
log_info "正在处理数据"
log_error "设备无响应"
# [14:30:52] [INFO] 脚本开始
# [14:30:52] [INFO] 正在处理数据
# [14:30:52] [ERROR] 设备无响应
函数要点:
定义: 函数名() { 命令... }
调用: 函数名 参数1 参数2 (不加括号!)
取参: 函数内部用 $1 $2,$@ 是全部参数
局部变量:local x=1(不加 local 就是全局变量)
返回值:return 数字(退出码);想返回字符串用 echo + $()
# 函数返回字符串的标准套路
get_size() {
du -h "$1" | cut -f1
}
SIZE=$(get_size "hello.sh") # 用 $() 接住函数的 echo 输出
echo "hello.sh 大小:$SIZE"
🖥️ 八、实战:自动备份脚本(本课重头戏)
把今天学的全部用上。新建 backup.sh:
#!/bin/bash
# ==========================================
# 自动备份脚本 v1.0
# 功能:把指定目录打包压缩,按日期命名,输出报告
# 用法:./backup.sh <要备份的目录> [备份存放目录]
# 例子:./backup.sh ~/fde_project ./backups
# ==========================================
set -euo pipefail
# ↑ 严格模式:出错即停、未定义变量报错、管道失败即失败
# Day 40 会细讲,今天先照抄,它能让脚本更可靠
# ---------- 1. 接收并校验参数 ----------
SOURCE_DIR="${1:-}" # 源目录(必填)
BACKUP_DIR="${2:-./backups}" # 备份目录(选填,默认 ./backups)
if [ -z "$SOURCE_DIR" ]; then
echo "❌ 错误:没有指定要备份的目录"
echo "用法:$0 <源目录> [备份目录]"
echo "例子:$0 ~/fde_project ./backups"
exit 1
fi
if [ ! -d "$SOURCE_DIR" ]; then
echo "❌ 错误:源目录不存在 → $SOURCE_DIR"
exit 1
fi
# ---------- 2. 准备备份目录 ----------
mkdir -p "$BACKUP_DIR"
# ---------- 3. 生成备份文件名(目录名 + 时间戳)----------
TIMESTAMP=$(date '+%Y%m%d_%H%M%S')
DIR_NAME=$(basename "$SOURCE_DIR") # 从完整路径里抠出最后一段目录名
ARCHIVE="${BACKUP_DIR}/${DIR_NAME}_backup_${TIMESTAMP}.tar.gz"
# ---------- 4. 打包压缩 ----------
echo "📦 正在备份:$SOURCE_DIR"
echo " 目标文件:$ARCHIVE"
# tar 参数:c 创建 z gzip压缩 f 指定文件
# -C 先切到源目录的父目录,保证压缩包里不带一长串绝对路径
tar -czf "$ARCHIVE" -C "$(dirname "$SOURCE_DIR")" "$DIR_NAME"
# ---------- 5. 输出结果报告 ----------
SIZE=$(du -h "$ARCHIVE" | cut -f1)
echo "✅ 备份完成"
echo " 文件:$ARCHIVE"
echo " 大小:$SIZE"
echo " 时间:$(date '+%Y-%m-%d %H:%M:%S')"
跑起来
chmod +x backup.sh
# 正常备份
./backup.sh ~/fde_project
# 📦 正在备份:/home/fde/fde_project
# 目标文件:./backups/fde_project_backup_20260918_143052.tar.gz
# ✅ 备份完成
# 文件:./backups/fde_project_backup_20260918_143052.tar.gz
# 大小:12K
# 时间:2026-09-18 14:30:52
# 不传参数 → 校验拦截
./backup.sh
# ❌ 错误:没有指定要备份的目录
# 用法:./backup.sh <源目录> [备份目录]
# 源目录不存在 → 校验拦截
./backup.sh /not/exist
# ❌ 错误:源目录不存在 → /not/exist
# 验证压缩包内容
tar -tzf ./backups/fde_project_backup_*.tar.gz | head
脚本逻辑流程图
./backup.sh ~/fde_project ./backups
│
▼
┌─── 参数为空?───┐
│ 是 → 打印用法,exit 1
└───────┬─────────┘
│ 否
▼
┌── 源目录存在?──┐
│ 否 → 报错,exit 1
└───────┬────────┘
│ 是
▼
mkdir -p 备份目录
│
▼
生成时间戳文件名(tar.gz)
│
▼
tar -czf 打包压缩
│
▼
du -h 算大小,打印报告 → 完成
关键命令拆解:tar
tar -czf 备份包.tar.gz 要打包的东西
│││
││└─ f file:指定生成的文件名
│└── z gzip:压缩(不加 z 只打包不压缩,文件大)
└─── c create:创建新压缩包
tar -tzf 备份包.tar.gz # t list:列出包里有啥(不释放)
tar -xzf 备份包.tar.gz # x extract:解包释放
💡 为什么要
-C "$(dirname ...)"?如果直接tar -czf x.tar.gz /home/fde/fde_project,解压时会还原出完整的home/fde/fde_project/...层级;用-C切到父目录再打包,解压出来就是干净的fde_project/一个文件夹。这是备份脚本的专业细节。
📝 本课小结
| 知识点 | 语法 | 一句话 |
|---|---|---|
| 脚本头 | #!/bin/bash |
指定 bash 解释器 |
| 输出 | echo |
打印文字 |
| 命令替换 | $(命令) |
拿命令输出 |
| 变量 | NAME="x",取值 $NAME |
等号两边无空格 |
| 默认值 | ${1:-默认} |
参数没给就用默认 |
| 参数 | $0 $1 $# $@ |
脚本名/第N参/个数/全部 |
| 退出码 | $?,exit 1 |
0 成功非0失败 |
| 条件 | if [ ... ]; then |
括号两边有空格 |
| 文件判断 | -d -f -e -z |
目录/文件/存在/空串 |
| 数字比较 | -eq -lt -gt |
不能用 > < |
| 循环 | for x in 列表; do / while [ ]; do |
遍历 / 条件循环 |
| 运算 | $((a+1)) |
整数运算 |
| 函数 | f() {...},调用 f 参数 |
内部用 $1 |
| 打包 | tar -czf/-tzf/-xzf |
建/看/解 |
🧠 核心认知 :Shell 脚本 = 命令 + 编程逻辑 。前两周学的命令是"砖块",今天学的变量/条件/循环/函数是"水泥和图纸"。写脚本的思路永远是:先手动把命令跑通 → 再把固定值换成变量/参数 → 加上校验和日志 → 存成 .sh 复用。
📋 课后练习
练习 1:参数与判断(约 20 分钟)
写 check_path.sh,接收一个路径参数:
-
没传参数 → 提示用法并
exit 1 -
传的是目录 → 输出"这是目录,里面有 N 个文件"(提示
ls "$1" \| wc -l) -
传的是文件 → 输出"这是文件,大小为 XX"(提示
du -h) -
都不是 → 输出"路径不存在"
练习 2:循环批量处理(约 20 分钟)
写 batch_backup.sh,接收多个文件 参数(用 $@),把每个文件复制成 文件名.bak,并用 for 循环逐个打印"已备份 xxx"。测试:./batch_backup.sh a.txt b.txt hello.sh。
练习 3:升级备份脚本(约 40 分钟,三选二实现)
在 backup.sh 基础上加功能:
-
日志功能 :所有输出同时显示在屏幕并 追加写入
backup.log(提示:把报告部分用tee -a backup.log,或定义 log 函数) -
保留份数 :脚本开头加第三个参数 K(默认 7),备份完成后只保留最新的 K 份,更老的自动删(提示:
ls -t 目录/*.tar.gz | tail -n +$((K+1)) | xargs -r rm) -
备份后校验 :用
tar -tzf "$ARCHIVE" > /dev/null验证压缩包没损坏,失败就报错退出(用$?判断)
🔭 下节预告
备份脚本能跑了。但有个问题:你的 FastAPI 服务一关终端就停,总不能一直开着窗口守着它。
明天综合实战,解决服务"常驻"问题:
-
nohup:关掉终端服务照样跑
-
screen:给服务开一个"关不掉的房间",随时能回去看
-
tail -f:实时盯日志
-
crontab:让备份脚本每天凌晨 2 点自动执行,不用人管
-
健康检查脚本:服务挂了自动拉起来
学完明天,你的 API 就有了一台"无人值守服务器"的雏形。明天见!
🌍附录:前置课程列表
阶段一:认知启蒙(AI 认知与 FDE 角色)
AI 认知
【FDE系列】阶段1Day 1:AI 层级关系 --- 四个嵌套的圈-CSDN博客
【FDE系列】阶段1Day 2:AI 三阶段发展史 --- 会认 → 会判断 → 会创造-CSDN博客
【FDE系列】阶段1Day 3:符号 AI vs 机器学习 --- 两条路线的本质区别-CSDN博客
【FDE系列】阶段1Day 4:Transformer 的历史意义 --- 2017 年的分水岭-CSDN博客
【FDE系列】阶段1Day 5:本周复习与自测 --- 检验你的 AI 认知地基-CSDN博客
【FDE系列】阶段1Day 6:Transformer 架构 --- 一张图纸盖出千千万万栋楼-CSDN博客
【FDE系列】阶段1Day 7:LLM 本质 --- 文字接龙机器-CSDN博客
【FDE系列】阶段1Day 8:Token --- 模型眼中的最小单位-CSDN博客
【FDE系列】阶段1Day 9:AI 幻觉 --- 为什么会一本正经地胡说八道-CSDN博客
【FDE系列】阶段1Day 10:上下文窗口 --- 模型的记忆力上限 + 本周复习-CSDN博客
【FDE系列】阶段1Day 11:Prompt --- 给模型立规矩-CSDN博客
【FDE系列】阶段1Day 12:Memory --- 让模型记住上下文
【FDE系列】阶段1Day 13:RAG --- 给模型配图书管理员-CSDN博客
【FDE系列】阶段1Day 14:Tool Use --- 让模型动手操作-CSDN博客
【FDE系列】阶段1Day 15:MCP --- 统一的工具接口标准 + 第三周复习-CSDN博客
FDE 基础概念
【FDE系列】阶段1Day 16:什么是 FDE --- 把 AI 变成客户结果的人-CSDN博客
【FDE系列】阶段1Day 17:FDE vs 传统实施 --- 三大本质区别-CSDN博客
【FDE系列】阶段1Day 18:FDE 三重身份 + C6 胜任力模型-CSDN博客
【FDE系列】阶段1Day 19:七阶段行动路径 + 行业经验的价值-CSDN博客
【FDE系列】阶段1Day 20:阶段总结与产出物 --- 第一阶段收官-CSDN博客
阶段二:技术地基(Python + FastAPI + SQL + Docker + API 集成)
Python基础
【FDE系列】阶段2:Day 21:Python 环境搭建 --- 写出你的第一行代码-CSDN博客
【FDE系列】阶段2:Day 22:变量、数据类型、条件判断 --- Python 的"记忆"和"判断"-CSDN博客
【FDE系列】阶段2:Day 23:循环与函数 --- 让代码跑 100 遍、把逻辑打包复用-CSDN博客
【FDE系列】阶段2:Day 24:数据结构 --- 列表、字典、集合、元组-CSDN博客
【FDE系列】阶段2:Day 25:文件读写与 JSON --- 让程序连通外部数据(第一周收官)-CSDN博客
【FDE系列】阶段2:Day 26:模块化编程 --- 把代码拆成"抽屉柜"-CSDN博客
【FDE系列】阶段2:Day 27:异常处理与日志 --- 让程序"摔不烂、查得到"-CSDN博客
FastAPI入门到进阶
【FDE系列】阶段2:Day 28:FastAPI 入门 --- 把你的函数变成 API 服务-CSDN博客
【FDE系列】阶段2:Day 29:FastAPI 进阶 --- Pydantic 模型与完整 CRUD 实战-CSDN博客
【FDE系列】阶段2:Day 30:生产代码规范 --- 测试、类型注解、配置管理(第二周收官)-CSDN博客
SQL基础
【FDE系列】阶段2:Day 31:SQL 基础 --- 增删改查一把梭-CSDN博客
【FDE系列】阶段2:Day 32:多表查询 --- JOIN 与聚合-CSDN博客
【FDE系列】阶段2:Day 33:进阶查询 --- 窗口函数与 CTE-CSDN博客
【FDE系列】阶段2:Day 34:数据清洗 --- 把脏数据捋干净-CSDN博客
【FDE系列】阶段2:Day 35:Python + SQL --- 工单接入 MySQL + 本周收官-CSDN博客