第 4 章 · 03 练习与调试最佳实践 语法学完只算"认得字",动手写题才算"会作文"。本节把经典题库中的 12 道脚本练习按难度分成五组逐题精讲,随后转入工程化三件套:set -e 失败即停、bash -x 逐行追踪、bats 自动化测试,最后汇总一份"新手的坑"清单。 学习目标 独立完成 12 道练习,覆盖变量、算术、条件、循环、函数、case、文件与进程 理解 set -e 的语义边界,用 || true 等手法精确控制失败策略 用 bash -x 定位脚本执行路径 认识 bats 测试框架的用例骨架 避开等号空格、漏引号、忘权限、CRLF 换行等高频坑 一、练习全景:12 题五组 组别 | 练习 | 主题 | 难度 | 考点 入门三连 | Hello World | 变量 |
语法学完只算"认得字",动手写题才算"会作文"。本节把经典题库中的 12 道脚本练习按难度分成五组逐题精讲,随后转入工程化三件套:set -e 失败即停、bash -x 逐行追踪、bats 自动化测试,最后汇总一份"新手的坑"清单。
| 组别 | 练习 | 主题 | 难度 | 考点 |
|---|---|---|---|---|
| 入门三连 | Hello World | 变量 | 基础 | 定义与引用 |
| Basic Date | 变量 | 基础 | 命令替换 | |
| Great Day | 变量 | 基础 | 参数缺省值 | |
| 算术与条件 | Factors | 算术 | 基础 | 取模与 if |
| Argument Check | 条件 | 基础 | 参数校验 | |
| 循环与输入 | Files Size | For 循环 | 基础 | 遍历与统计 |
| Count Chars | 输入+While | 基础 | read 逐行 | |
| 函数与分支 | Sum | 函数 | 基础 | 参数累加 |
| Num of Args | Case | 基础 | 多路分派 | |
| 杂项进阶 | Empty Files | 杂项 | 基础 | 空文件检测 |
| Directories Comparison | 杂项 | 基础 | 目录对比 | |
| Host Status | 杂项 | 进阶 | 主机探活 |
从"入门三连"到"主机探活",难度与考察范围逐组攀升。下面按组精讲,每题给出思路与参考答案。
第一题 Hello World 只考一件事——变量定义与引用:
#!/bin/bash HW="Hello World" echo $HW
写对它的前提是记住"等号两侧无空格"。第二题 Basic Date 引入命令替换:
#!/bin/bash DATE=$(date) echo $DATE
把 date 命令的输出装进变量再打印,是 $() 的最小完整用例。第三题 Great Day 升级为"有参数打印参数、无参数打印默认值":
#!/bin/bash echo "${1:-yay}"
一行代码同时考察了位置参数 $1、缺省值展开 :- 与双引号安全引用三个知识点,是面试常考的最小健壮脚本。
Factors 题要求判断"给定数字是否有 4 这个因数",取模运算登场:
#!/bin/bash if [ $(($1 % 4)) -eq 0 ]; then echo "4 的倍数" else echo "不是 4 的倍数" fi
$(( )) 算术替换取余,-eq 数值比较,三处语法环环相扣。Argument Check 题转向"参数是否存在"的校验:
#!/bin/bash if [ $# -eq 0 ]; then echo "请传入参数" exit 1 fi echo "收到 $# 个参数"
$# 统计参数个数,exit 1 用非零退出码向调用方报错——脚本的"契约"意识从这里开始。
Files Size 题遍历一批文件并报告大小:
#!/bin/bash for f in "$@"; do stat -c '%s' "$f" # 或 ls -l "$f" | awk '{print $5}' done
循环体可以换成任意处理逻辑,比如累加出"总大小"。Count Chars 题反向操作——从标准输入逐行读、逐行数字符数:
#!/bin/bash while read line; do echo "${#line}" done
${#var} 求长度配合 while+read 逐行处理,正是文本管道的标准姿势。
Sum 题把"参数求和"封装成函数:
#!/bin/bash sum() { total=0 for n in "$@"; do total=$((total + n)) done echo $total } sum "$@"
函数负责纯计算、用 echo 输出结果,主流程调用并接住输出。Num of Args 题按参数个数分派输出,是 case 的考题:
#!/bin/bash case $# in 0) echo "没有参数" ;; 1) echo "只有一个参数" ;; *) echo "有多个参数" ;; esac
Empty Files 题找出空文件,组合 find 与 test:
#!/bin/bash find . -type f -empty # 或循环版: for f in "$@"; do [ -s "$f" ] || echo "$f 是空文件" done
-s 判断非空,|| 短路实现"为空则报告"。Directories Comparison 题对比两个目录的差异:
#!/bin/bash diff <(ls -1 dir1) <(ls -1 dir2)
这题正是上一节讲的进程替换的用武之地——diff 需要两个"文件",而数据源是两条命令输出。最后一道进阶题 Host Status 探活主机:
#!/bin/bash if ping -c 2 -W 1 "$1" > /dev/null 2>&1; then echo "$1 在线" else echo "$1 不可达" fi
> /dev/null 2>&1 吞掉噪音输出、只留退出码判断,是探活脚本的标准写法;进阶版可换成 curl 探 HTTP 服务并检查 HTTP 状态码。
Shell 脚本的默认行为反直觉:某条命令失败,脚本不会停下,会继续往下跑。这意味着一个 rm 失败后,后续假设文件已删除的逻辑仍会执行,错误被层层放大。
set -e(errexit)改变这个行为:任何命令返回非零退出码,脚本立即退出。它是脚本的第一道防线,通常与 set -u(变量未定义即报错)搭配:
set -euo pipefail
三件套含义:-e 失败即停、-u 未定义变量即错、pipefail 让管道中任一环节失败都算失败(默认只看最后一段)。这是现代 Bash 脚本的"标准安全头"。
但 set -e 有边界,需要精确控制的地方要显式豁免:
|| true 兜底;定位脚本问题,第一利器是 bash -x script.sh(或脚本内 set -x):每条命令执行前打印展开后的完整形式(带 + 前缀),变量值一目了然:
$ bash -x hello.sh + HW='Hello World' + echo 'Hello World' Hello World
从输出能直接看出:变量是否展开、引号是否生效、参数是否切分。老派但同样有效的是在关键路径打 echo 断点("old good way"),确认"执行到了这里"和"此时的值是什么"。
调试纪律:先 -x 看执行路径,再在可疑点加 echo,修复后务必移除调试输出。
| 坑 | 错误写法 | 正确写法 |
|---|---|---|
| 赋值带空格 | x = 2 |
x=2 |
| 引用不加引号 | echo $1 | echo "$1" |
|
| 忘了执行权限 | ./hello.sh 报拒绝 |
chmod +x hello.sh |
| Windows 换行 | 报 $'\r': command not found | 用 LF 换行(脚本内 sed -i 's/\r$//') |
|
| 混用 * 与 @ | "$*" 传参被拼碎 | "$@" 保持参数完整 |
|
| 正则放引号内 | [[ $x =~ "^[0-9]+$" ]] 失配 |
模式存变量或用无引号字面量 |
其中 CRLF 换行是 Windows 用户写 Linux 脚本的头号杀手:编辑器保存了 \r\n,Bash 把 \r 当成参数内容,报错信息莫名其妙,务必以 LF 保存。
脚本是代码,代码就该有测试。Bats(Bash Automated Testing System)是 Bash 生态的事实标准测试框架,用例骨架极简:
#!/usr/bin/env bats @test "无参数时输出 yay" { result="$(echo "${1:-yay}")" [ "$result" = "yay" ] } @test "有参数时输出参数" { result="$(echo "hello")" [ "$result" = "hello" ] }
每个 @test 块是一个用例,块内用 [ ] 断言,全部通过则测试绿。把练习题的输出抓进变量、与期望值比对,12 道练习就变成了 12 组回归用例——以后改脚本不怕改坏旧功能。
本章从词法(变量与参数)到语法(条件、循环、函数)再到工程化(练习、set -e、bash -x、bats),完成了 Shell 脚本的闭环。12 道练习覆盖了脚本编程的绝大多数基础模式,set -euo pipefail 与 bats 则让脚本从"能跑"走向"敢交付"。Shell 是 DevOps 的底层母语,但它处理的是"单机上的命令流"——当应用要跨机器、跨环境交付时,容器化是下一站。
第 5 章《容器化与 Docker》将把"可执行环境"打包成镜像:镜像分层、Dockerfile 指令、容器与虚拟机的关系,以及镜像构建与分发的最佳实践。