Skip to content

Shell 脚本基础 ​

S3-035建议学进阶约 60 分钟JDK 21企业开发基础 · Linux 与 Shell

1. 本节定位 ​

前面学的命令都是"一次一条"。脚本把它们串起来,变成"可重复执行的一件事" ✓

text
命令:  tail -f app.log            (每次都要手敲)
脚本:  ./deploy.sh start          (把 20 个步骤固化成一个动作)★

脚本的价值不只是"省事":

价值说明
可重复 ★每次执行都一样,不会因为"这次忘了第 7 步"而出错 ✓
可版本化脚本进 Git,团队共享、可评审 ✓
可自动化能被 CI / 定时任务调用 ✓

2. 学习目标 ​

完成本节后,你应该能够:

  • 写出可执行的第一行脚本(#!/usr/bin/env bash);
  • 正确使用变量与引号(这是最容易错的地方)★★;
  • 使用位置参数($1、$#、$@)与默认值(${1:-default})★;
  • 用退出码表达成败,并用 if 判断 ★★★;
  • 写 for / while 循环与函数;
  • 理解 set -euo pipefail 为什么该成为默认开头 ★★★;
  • 会用 bash -n / bash -x / shellcheck 调试;
  • 写出"工程化"的脚本(参数校验、日志、幂等)★★。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一个"手工操作"的典型事故:

text
发布步骤(文档里写着):
  1. 备份  2. 停服务  3. 替换 jar  4. 启动  5. 检查
某次执行时,第 5 步忘了 → 线上跑着一个启动失败的版本 ✗

如果写成脚本:第 5 步不可能被跳过 ✓

脚本把"应该做什么"从"文档里的文字"变成"可执行的事实" ✓

这与 《Git Flow 与团队协作》 的"分支保护"、《代码规范与自动校验》 的"规范检查"是同一条原则: 把约定变成机制 ✓

5. 核心概念 ​

5.1 第一行与执行方式 ​

bash
#!/usr/bin/env bash        # ★ shebang:告诉系统用哪个解释器

两种执行方式:

方式命令需要执行权限吗
作为解释器参数bash script.sh❌ 不需要 ✓
直接执行./script.sh✅ 需要(chmod +x)★

⚠️ 新手最常问的"Permission denied":

bash
./deploy.sh        # ✗ Permission denied
chmod +x deploy.sh # ✓ 加执行位([S3-033](./S3-033))
./deploy.sh

5.2 变量与引号 ★★ ​

赋值(等号两边不能有空格 ⚠️):

bash
name="Alice"        # ✓
name = "Alice"      # ✗ 会被当成命令 name

使用:

bash
echo "$name"
echo "${name}"      # ★ 推荐:带花括号,边界明确

★ 引号的三条规则(这是本节最容易错的地方):

引号变量会展开吗用在
双引号 " " ★✅ 会默认用这个 ✓
单引号 ' '❌ 不会原样输出(如正则里的 $)✓
无引号✅ 会(但会做分词 ✗)不要用(除非你明确想要分词)⚠️

无引号的危险(真实 bug 来源):

bash
file="my report.txt"
rm $file            # ✗ 被分词成 rm "my" "report.txt" → 删了两个文件 ✗✗
rm "$file"          # ✓

★ 规则:引用变量时,几乎总是加双引号 ✓

命令替换(把命令的输出赋给变量):

bash
today=$(date '+%F')            # ★ 推荐 $( )
count=$(grep -c ERROR app.log)
# 反引号 `date` 也行,但不推荐(嵌套困难、易混淆)

5.3 参数 ​

bash
./deploy.sh prod v1.2.0
变量含义
$0脚本自己
$1 $2第 1、2 个参数 ✓
$#参数个数 ★
$@所有参数(保持各自独立)★
$*所有参数(合并成一个字符串,⚠️ 一般不用)
$?上一个命令的退出码 ★★

默认值与必填校验 ★:

bash
ENV="${1:-dev}"                        # 没传就用 dev ✓
JAR="${2:?用法:$0 <环境> <jar 路径>}"   # ★ 没传就报错并退出 ✓

${var:?msg} 非常实用 ——它让脚本"参数不对就拒绝执行",而不是带着空值跑下去 ✗

5.4 退出码与条件 ★★★ ​

这是脚本的"控制流基础" ✓

bash
if grep -q "ERROR" app.log; then
    echo "有错误"
else
    echo "没有错误"
fi

★ if 判断的就是"命令的退出码"(0 = 真,非 0 = 假)✓

不需要 [ ] 的常见写法:

bash
if command; then ... fi                  # ✓ 直接判断命令
if [ -f "$file" ]; then ... fi           # 判断文件存在
if [ -z "$var" ]; then ... fi            # 判断字符串为空
if [ "$a" = "$b" ]; then ... fi          # 字符串相等(注意空格 ★)

⚠️ [ ] 里的空格不能省:

bash
if [$a = $b]; then    # ✗
if [ "$a" = "$b" ]; then   # ✓

文件测试(部署脚本常用)★:

表达式含义
-f是普通文件 ✓
-d是目录 ✓
-e存在(任何类型)✓
-s存在且非空 ★(判断"打包成功"时用)✓
-r / -w / -x可读 / 可写 / 可执行 ✓

主动设置退出码 ★:

bash
exit 0      # 成功 ✓
exit 1      # 失败 ✗

★ 这就是脚本能被 CI / 监控判断成败的原因(《代码规范与自动校验》)✓

5.5 循环与函数 ​

bash
# for
for file in *.log; do
    echo "处理 $file"
done

# 计数循环
for i in $(seq 1 10); do sleep 1; done        # ★ 等待/重试时常用

# while(常用于"最多等 N 次"的重试)★
attempt=0
while [ $attempt -lt 10 ]; do
    curl -fsS "$URL" && break
    attempt=$((attempt + 1))
    sleep 3
done

函数(让脚本可读):

bash
log() { echo "[$(date '+%F %T')] $*"; }       # ★ 带时间戳的日志函数

start() {
    log "启动..."
    # ...
}

start                                          # 调用(不加括号 ✓)

★ 一个习惯:每个脚本都定义一个 log() ——带时间戳的输出对事后复盘极有价值 ✓

5.6 set -euo pipefail ★★★ ​

这是本节最该记住的一行 ✓

bash
#!/usr/bin/env bash
set -euo pipefail

三个选项分别做什么:

选项作用为什么重要
-e任何命令失败就退出 ★避免"第 3 步失败了,脚本还继续跑第 4、5 步" ✗✗
-u用了未定义变量就报错 ★避免 rm -rf $DIR/* 在 DIR 为空时变成 rm -rf /* ✗✗✗
-o pipefail管道中任一环节失败,整体算失败 ★默认只看最后一个命令的退出码 ✗

★ -o pipefail 的例子:

bash
grep ERROR app.log | wc -l
# 如果 grep 失败(文件不存在),默认整体退出码 = wc 的(0) → "看起来成功" ✗
# 加了 pipefail → 整体非 0 ✓

⚠️ 什么时候"不用" -e:

当你想"收集所有问题再一起报告"时(如本节的 check-project.sh)——那时你会自己检查每个命令的结果 ✓

但即使这样,-u 和 pipefail 通常仍要保留 ✓

5.7 调试 ​

方法作用
bash -n script.sh ★只检查语法(不执行)——改完脚本先跑这个 ✓
bash -x script.sh ★★打印每一条执行的命令(逐行跟踪)——排查"为什么这样"的神器 ✓
shellcheck script.sh ★★★静态检查(能发现引号、变量、语法等各类问题)——强烈推荐安装 ✓
set -x / set +x在脚本里临时开关跟踪 ✓

★ 本节的脚本就自带了"语法自检"(bash -n "$0")✓ ——这是一个值得模仿的习惯 ✓

shellcheck 能发现什么(举例):

问题提示
变量没加引号SC2086: Double quote to prevent globbing ★
$? 用法不对SC2181
用了 [ 而不是 [[SC2292(bash 里推荐 [[)

5.8 脚本的"工程化" ​

三个让脚本从"能跑"变成"可交付"的习惯 ★:

习惯做法为什么
① 参数校验${1:?用法...} + [ -f "$file" ]错的参数要在第一步就拒绝 ✓
② 日志log() 带时间戳 + 关键步骤都输出事后能复盘 ✓
③ 幂等 ★重复执行结果一致(mkdir -p、先判断是否已运行)失败后重跑是常态 ✓

幂等为什么重要:

脚本执行到一半失败了,你一定会再跑一次 ✓ ——如果脚本不幂等,第二次可能造成新的破坏 ✗

例子:mkdir dir 第二次会报错 ✗ → 用 mkdir -p ✓

6. 递进示例 ​

6.1 最小示例 ​

bash
#!/usr/bin/env bash
set -euo pipefail
name="${1:-world}"
echo "hello, $name"

6.2 业务示例:项目健康检查 ​

见第 7 节(check-project.sh 的完整实测)✓

6.3 反例:没有 set -e 的部署脚本 ​

bash
cd /opt/app             # 如果目录不存在,失败
cp new.jar app.jar      # ✗ 但脚本继续跑:把 new.jar 拷到了"当前目录"
systemctl restart app   # ✗ 重启了一个没更新的版本
echo "发布完成 ✓"        # ✗ 报告成功 ✗✗

★ 这正是 set -e 要防的:一步失败,后续全部在错误的前提上执行,最后还报告成功 ✗✗

7. 完整可运行代码 ​

文件:labs/engineering/check-project.sh

运行(Windows 用 Git Bash):

powershell
& "C:\Program Files\Git\bin\bash.exe" -c 'cd <仓库根目录> && bash labs/engineering/check-project.sh .'

预期输出(真实运行结果)★:

text
===== 1. 基本信息 =====
目录     : <仓库根目录>
主机名   : dev-host
当前用户 : testuser
时间     : 2026-09-21 16:34:50

===== 2. 文件统计 =====
Markdown 文件 : 93
Java 文件     : 135

===== 3. 检查大文件(> 500KB 的文档)=====
docs/.vitepress/cache/deps/chunk-C2KGWKLW.js.map
  [提示] 以上文件偏大,考虑拆分

===== 4. 检查技术债标记 =====
含 TODO/FIXME 的文件数 : 9

===== 5. 检查构建产物是否被误提交 =====
构建产物未被提交 ✓

===== 6. 检查脚本自身的语法 =====
语法检查通过 ✓

===== 结果 =====
全部通过 ✓

脚本退出码=0

脚本里用到的技术点对照 ★:

位置技术点本节章节
set -uo pipefail安全选项(未用 -e,因为要收集所有问题)5.6 ★
ROOT="${1:-$(pwd)}"默认值 + 命令替换5.2 / 5.3
problems=0 / problems=$((problems + 1))变量与算术5.2
section() { ... }函数5.5
if [ -n "$large" ]条件判断5.4
bash -n "$0"语法自检 ★5.7
exit 0 / exit 1退出码 ★5.4

★ 一个值得注意的设计:这个脚本"刻意不用 set -e" ——因为它要检查 6 项并报告所有问题,而不是第 1 项失败就停 ✓

这说明:规则是工具,不是教条 ——知道为什么用,才知道什么时候不用 ✓

8. Debug 观察点 ​

  1. 去掉引号试试:把 cd "$ROOT" 改成 cd $ROOT,传一个带空格的路径 —— 观察报错 ✗(体会引号的价值)✓;
  2. 验证 -u:在脚本里引用一个没定义的变量 —— 观察它报错退出 ✓(对比不加 -u 时"静默用空值")✗;
  3. 验证 pipefail:grep x 不存在的文件 | wc -l; echo $? —— 加与不加 pipefail 的对比 ✓;
  4. bash -x:用它跑一遍 check-project.sh —— 观察每一行实际执行的样子(变量被展开后的真实命令)★;
  5. 故意制造语法错误:删掉一个 fi —— 用 bash -n 观察它报错 ✓。

9. 常见错误 ​

错误 1:变量不加引号 ​

见 5.2。文件名带空格时会导致灾难 ✗

错误 2:赋值时加空格 ​

bash
name = "x"      # ✗ 被当成命令
name="x"        # ✓

错误 3:忘了 set -e,一步失败后还继续 ​

见 6.3。最危险的一类 ✗✗

错误 4:$? 被"覆盖" ​

bash
command
if [ $? -eq 0 ]; then ...      # ⚠️ 能工作但啰嗦
# ★ 更简洁:
if command; then ...            # ✓ 直接判断

更隐蔽的坑:

bash
command
echo "执行完了"                  # ✗ echo 覆盖了 $?
if [ $? -eq 0 ]; then ...       # 这里判断的是 echo 的退出码 ✗✗

修:先把 $? 存到变量,或直接用 if command ✓

错误 5:脚本没有执行权限就 ./x.sh ​

见 5.1。chmod +x ✓

错误 6:在脚本里用相对路径 ​

bash
cd /opt/app
cat config/app.yml      # 如果 cd 失败,就在"当前目录"找 ✗

修:用绝对路径,或 cd ... || exit 1(配合 set -e 更安全)✓

错误 7:脚本里硬编码密码 ​

bash
mysql -uroot -p123456      # ✗ 密码进 Git ✗✗

修:从环境变量或配置文件读(《Spring Boot 配置管理与多环境》 的"敏感配置")✓

10. 动手实验 ​

目标:写一个"能交付"的脚本(不只是能跑)。

步骤:

  1. 跑第 7 节的 check-project.sh,核对输出;
  2. bash -x 跑一遍:观察变量展开后的真实命令 ✓;
  3. 故意删掉一个 fi,用 bash -n 观察报错 ✓;
  4. 给脚本加一个 --help:当参数是 -h / --help 时打印用法并 exit 0 ✓;
  5. 加一个"严格模式"开关:传 --strict 时把 TODO 也当成失败(体会"规则可配")★;
  6. 用 shellcheck(如果能装)跑一遍,看看它会报什么 ✓。

验收方式:能说出第 2 步 bash -x 的用处、第 5 步"为什么要可配"。

扩展任务:把 《Linux 服务部署与运维》 的发布步骤写成脚本:

  • 备份(带时间戳)→ 优雅停止 → 替换 → 启动 → 健康检查(重试) → 失败回滚;
  • 要求:set -euo pipefail、log() 函数、参数校验、幂等;
  • 说明:其中"健康检查重试"用哪个循环结构最合适?为什么?

11. 练习题 ​

基础题

  1. 判断:set -e 会让脚本在"任何命令返回非 0"时立即退出。(对 / 错)
  2. 填空:引用变量时应该加 ______ 引号;$(cmd) 表示 ______;${1:-dev} 表示 ______。
  3. 简答:为什么"脚本要幂等"?

代码阅读题

bash
#!/usr/bin/env bash
set -euo pipefail

DIR="/opt/${APP}/logs"
rm -rf "$DIR"/*
echo "清理完成"
  1. 这段脚本有什么问题?
  2. 怎么改才安全?

编程题

  1. 写一个"日志分析脚本":
    • 输入:一个日志文件路径 + 一个可选的"起始时间";
    • 输出:错误数、最多的 5 种错误、最慢的 10 个请求;
    • 要求:
      • 参数校验(文件必须存在)★;
      • 用函数组织(统计、输出分离);
      • set -euo pipefail;
      • 全路径引用都加引号 ✓;
    • 回答:为什么"日志文件路径"这个参数必须校验存在性?

综合任务

  1. 为团队设计"脚本规范":
    • 背景:团队里脚本越来越多(部署、备份、清理),但质量参差——有的会误删、有的失败不退出;
    • 要求:
      • 写一份脚本规范(至少 8 条),每条给出理由与示例;
      • 说明哪些规范能被工具检查(shellcheck)、哪些只能靠评审;
      • 设计脚本的存放与评审方式(放哪个目录?谁来 review?);
      • 说明"脚本算不算代码"(要不要测试、要不要进 Git、要不要 review)★;
    • 额外要求:说明"如果一个脚本会 rm -rf,应该额外加哪些保护"。
分级提示

提示 1(方向):第 1 题基本是对的,但要注意"某些位置(如 if 条件里)不受 -e 影响"。

提示 2(关键点):第 4 题——APP 如果没定义会怎样?rm -rf "$DIR"/* 会变成什么?

提示 3(接近答案):第 7 题的"脚本算不算代码"——想想脚本出错的代价,以及它是否会被重复执行。

12. 参考答案 ​

基础题
  1. 基本对,但有三个重要例外 ⚠️

set -e 不生效的位置:

位置例子为什么
if / while 的条件里if grep -q x f; then ...条件就是"允许失败"的语义 ✓
**`/&&` 的左边**
取反! cmd同上 ✓

还有一个隐蔽的例外 ⚠️:

bash
set -e
count=$(grep -c ERROR app.log)     # grep 没匹配到时返回 1,但——
# 在"命令替换 + 赋值"里,某些情况 -e 不生效(取决于 bash 版本与上下文)

所以更稳的写法:

bash
count=$(grep -c ERROR app.log || true)      # ★ 明确表示"失败也算 0"

★ || true 是一个常用技巧:告诉 set -e:"这条命令失败是预期的,别退出" ✓

  1. 双引号 ";命令替换(把命令输出赋给变量);"如果 $1 没传(或为空),就用 dev" ✓

三个都是日常高频用法 ✓

补充:

写法含义
${var:-default}未定义或为空 → 用 default ✓
${var:=default}同上,并且赋值回 var
${var:?msg} ★未定义或为空 → 打印 msg 并退出 ✓
${var:+other}已定义 → 用 other(少见)
  1. 因为"脚本执行到一半失败,你一定会再跑一次" ✓

不幂等的例子:

操作第一次第二次
mkdir dir✓✗ 报错
mkdir -p dir ★✓✓
echo "x" >> conf✓✗ 又追加了一行
ln -s target link✓✗ 报错(已存在)
ln -sfn target link ★✓✓

★ 记住这两个:mkdir -p 与 ln -sfn ✓

幂等还有一层含义:"重复执行不会产生副作用" —— 如"启动前先检查是否已在运行"(本节的 deploy-demo.sh 就是这么做的 ✓)

代码阅读题(第 4、5 题)

第 4 题:两个严重问题

#问题后果
1APP 未定义(且 set -u 会拦截 ✓)如果没有 -u:DIR="/opt//logs" → rm -rf /opt//logs/* ✗
2rm -rf "$DIR"/* ✗✗如果 DIR 因某种原因为空 → rm -rf /* ✗✗✗

★ 第 2 条是"删库跑路"级别的隐患 ✓

为什么会变成空:

bash
DIR="/opt/${APP}/logs"
# 如果 APP 未定义 → /opt//logs(还不是空)
# 但如果写成:DIR="${APP_DIR}/logs" 且 APP_DIR 未定义 → "/logs"
# 或者:DIR="${1}" 没传 → ""
rm -rf ""/*     # = rm -rf /*     ✗✗✗

注意:**即使 set -u 能拦截"未定义变量",它拦截不了"变量被定义为空字符串" ⚠️

bash
APP=""              # 定义了,但是空
DIR="/opt/${APP}/logs"     # 不报错(因为 APP 已定义)

所以 -u 不是万能的 ✓ ——还需要显式检查 ★

第 5 题:安全版本

bash
#!/usr/bin/env bash
set -euo pipefail

APP="${1:?用法:$0 <应用名>}"

# ★ ① 显式校验非空(不只是"已定义")
if [ -z "$APP" ]; then
    echo "✗ 应用名不能为空" >&2
    exit 1
fi

# ★ ② 校验目录存在且在预期范围内(白名单思路)★
DIR="/opt/$APP/logs"
if [ ! -d "$DIR" ]; then
    echo "✗ 目录不存在:$DIR" >&2
    exit 1
fi

# ★ ③ 再做一次"安全网":确认路径以 /opt/ 开头
case "$DIR" in
    /opt/*) ;;
    *)
        echo "✗ 拒绝清理非 /opt 下的路径:$DIR" >&2
        exit 1
        ;;
esac

# ★ ④ 用 -mtime 限定"只删旧的",而不是删全部
find "$DIR" -type f -name '*.log' -mtime +7 -delete
echo "清理完成:7 天前的日志"

五层保护 ★:

#保护防什么
1set -u未定义变量 ✗
2${1:?}参数缺失 ✗
3[ -z ] 检查变量为空 ⚠️
4case 白名单 ★★路径不在预期范围 ✗✗
5-mtime +7 而不是 * ★误删还在用的文件 ✗

★ 第 4 条最重要:"白名单"思路又一次出现 ✓

与其"检查路径有没有问题",不如"确认它在允许的范围内" ✓

这与 《Git Flow 与团队协作》 的"默认拒绝 + 白名单"、《Filter、Listener 与会话管理》 的鉴权策略完全一致 ✓

第 5 条的额外价值:"只删 7 天前"比"删全部"安全得多 ——即使误执行,损失也有限 ✓

(这也是"降低影响面"的思路,呼应 《Linux 文件、用户与权限管理》 综合任务的"假定失守") ✓

编程题(第 6 题)
bash
#!/usr/bin/env bash
# analyze-log.sh —— 日志分析
set -euo pipefail

LOG="${1:?用法:$0 <日志文件> [起始时间,如 2026-09-21]}"
SINCE="${2:-}"

# ★ 参数校验:文件必须存在
if [ ! -f "$LOG" ]; then
    echo "✗ 文件不存在:$LOG" >&2
    exit 1
fi

log() { echo "[$(date '+%F %T')] $*" >&2; }

# ① 错误数
count_error() {
    grep -c 'ERROR' "$LOG" || true        # ★ || true:没匹配到返回 0 而不是失败
}

# ② 最多的 5 种错误(按第 4 列归类,可按实际日志格式调整列号)
top_errors() {
    awk '/ERROR/ {count[$4]++} END {for (k in count) print count[k], k}' "$LOG" \
        | sort -rn | head -5
}

# ③ 最慢的 10 个请求(假设最后一列是耗时 ms)
slowest() {
    awk '{print $NF, $0}' "$LOG" | sort -rn | head -10 | cut -d' ' -f2-
}

echo "===== 日志分析:$LOG ====="
echo ""
echo "--- 错误总数 ---"
count_error
echo ""
echo "--- 最多的 5 种错误 ---"
top_errors
echo ""
echo "--- 最慢的 10 个请求 ---"
slowest
echo ""
log "分析完成"

要点:

#做法说明
①${1:?...} + [ -f ]参数校验(5.3)★
②三个函数可读、可复用(5.5)✓
③grep -c ... || true配合 set -e(没匹配到不算失败)★
④所有 "$LOG" 加引号路径带空格也不怕(5.2)✓
⑤awk 一次做完筛选+聚合比一串管道更快(《Linux 常用命令与部署》)✓

为什么"日志文件路径必须校验存在性"

理由说明
① 快速失败 ★参数错了要在第一秒就说,而不是跑到一半才报"找不到文件" ✗
② 避免"空结果"被误读 ✗✗如果文件不存在,grep 报错、awk 输出空 → 脚本可能输出"错误数 0" → 你以为"没问题" ✗✗✗
③ 保护下游如果分析结果被别的脚本使用,空结果会传播 ✗

★ 第 ② 条最关键:

"脚本没报错"不等于"脚本做对了" ✓

如果文件不存在,很多命令会"安静地输出空" ——而 set -e 未必能拦截(因为命令本身可能返回 0)✗

所以显式校验是唯一可靠的方式 ✓

这条经验可以推广:脚本里"输入"都要先校验(文件存在、参数合法、依赖命令可用)✓

"入口校验"与 《参数校验与全局异常处理》 的"参数校验"、《代码规范与自动校验》 的"规范检查"是同一件事: 在边界处拦住错误,而不是让它流到后面 ✓

综合任务(第 7 题)

《Shell 脚本规范》(8 条)

#规范理由示例
1开头 set -euo pipefail ★★★防止"失败后继续跑"set -euo pipefail
2所有变量引用加双引号 ★★防分词(文件名带空格)rm "$file"
3参数用 ${n:?} 校验 ★错参数要立刻拒绝ENV="${1:?用法:...}"
4幂等 ★失败重跑是常态mkdir -p / ln -sfn
5log() 带时间戳 ★事后可复盘log() { echo "[$(date '+%F %T')] $*"; }
6用绝对路径(或先 cd 再校验)相对路径依赖"当前目录" ✗/opt/app/start.sh
7禁用 rm -rf $VAR/* ★★变量为空 = 灾难 ✗✗✗见第 8 条
8危险操作要"白名单 + 确认 + 备份" ★★★不可逆操作必须多重保护见下
9不硬编码密码进 Git 就泄漏 ✗从环境变量读
10用函数组织,主流程清晰可读 = 可维护main() { ... }

哪些能被工具检查

规范shellcheck 能查吗
变量加引号✅ 能(SC2086)★
$? 用法✅ 能(SC2181)
语法错误✅ 能
set -euo pipefail⚠️ 部分(能提示"未设置"的某些情况,但不能强制)
幂等❌ 不能(需要人判断)✗
危险 rm⚠️ 部分(能提示"变量可能为空",但判断不了业务意图)
不硬编码密码❌ 不能 ✗

★ 结论:工具能查"形式",查不了"意图" ✓ ——所以"规范检查(CI)+ 人工 review"都要有 ✓

存放与评审

项做法
存放进 Git(如 scripts/ 目录),不放在服务器某个人的家目录 ✗
命名动词开头(deploy.sh / backup.sh),带 .sh 后缀 ✓
README每个脚本顶部注释:用途、用法、作者、注意事项 ★
评审脚本与代码一样要 PR + review(见下)
权限服务器上脚本属主正确、权限 750,不要 777 ✗

"脚本算不算代码" ★

答:算 ✓

理由说明
① 出错代价一样大 ✗✗一个 rm -rf 的破坏性不亚于一段有 bug 的业务代码
② 会被反复执行与"一次性命令"不同 —— 脚本的错误会重复发生 ✗
③ 会被他人使用别人不知道你的"隐含前提"(如"必须在 /opt 下运行")✗
④ 需要演进环境变了、需求变了 → 脚本也要改 ✓

所以脚本应该:

要说明
进 Git ✓版本化、可追溯
要 review ★尤其是含 rm / kill / 数据库操作的 ✓
要有测试(至少"干跑"验证)bash -n(语法)+ 在测试环境执行 ✓
要有 --dry-run ★★危险脚本应该支持"只打印将要做什么" ✓✓

★ --dry-run 是危险脚本的标配 ✓

bash
DRY_RUN="${DRY_RUN:-0}"
run() {
    if [ "$DRY_RUN" = "1" ]; then
        echo "[dry-run] $*"
    else
        "$@"
    fi
}
run rm -f "$file"        # ★ 真正执行前,可以 DRY_RUN=1 先看看会删什么

这与 《Git 分支、合并与回退》 的"--force-with-lease"、《Linux 文件、用户与权限管理》 的"打包成功再删"是同一种谨慎: 在不可逆操作前,给自己一个"看看会怎样"的机会 ✓

会 rm -rf 的脚本要加哪些保护 ★★★

#保护说明
1白名单校验路径 ★★case "$DIR" in /opt/*) ;; *) exit 1 ;; esac
2--dry-run ★★先看会删什么
3限定条件(-mtime +7、-name '*.log')不删"全部" ✓
4先备份 / 先移入待删区mv 到 /tmp/trash 而不是直接删 ★
5打印将要删的清单并确认read -p "确认删除?[y/N]" ⚠️(自动化脚本里要允许跳过确认)
6记录日志删了什么、什么时候、谁执行的 ✓
7set -u防变量未定义 ✓

★ 第 4 条最实用:用 mv 到一个"待删目录",过几天再真正清理 ✓

这样即使删错了,也有几天窗口能捞回来 ✓

这是"回收站"的思路 ——而 rm -rf 没有回收站 ✗

一句话总结本任务:

脚本的规范、review、保护机制,与业务代码是同一套工程纪律 ✓

因为它们的出错代价相同,而"这只是个脚本"这种想法,恰恰是事故的开始 ✗

这与 《代码规范与自动校验》 的"规范检查接 CI"、《Git Flow 与团队协作》 的"分支保护"、《Linux 文件、用户与权限管理》 的"最小权限"共享同一条设计哲学: 用机制代替注意力 ✓

13. 掌握标准 ​

  • [ ] 能写出带 shebang 的可执行脚本(知道 chmod +x);
  • [ ] 引用变量时总是加双引号 ★;
  • [ ] 会用 $1 / $# / ${1:-默认} / ${1:?报错};
  • [ ] 理解"if 判断的是退出码" ★★★;
  • [ ] 会用 $(cmd)、$((算术));
  • [ ] 能写 for / while 循环与函数;
  • [ ] 知道 set -euo pipefail 每一项的作用,以及什么时候可以不用 -e ★★★;
  • [ ] 会用 bash -n / bash -x / shellcheck;
  • [ ] 知道脚本要"参数校验 + 日志 + 幂等";
  • [ ] 知道含 rm -rf 的脚本需要哪些额外保护 ★★。

14. 扩展资源 ​

15. 下一步学习 ​

《进程与端口排查》:服务"起不来"或"连不上"时怎么查。

后续知识

暂无

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引