ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Shell条件判断与循环实战:退出码、单词分割与安全遍历

Shell条件判断与循环实战:退出码、单词分割与安全遍历

1. 项目概述:为什么条件判断和循环是Shell脚本真正的“开关”与“引擎”

你写过echo "Hello World",也用过ls -la加管道组合几个命令——这不算会Shell脚本。真正让脚本从“命令集合”跃升为“自动化程序”的临界点,就卡在条件判断语句和循环结构这两道门槛上。我带过不少刚转运维、测试或DevOps岗位的新人,他们普遍卡在同一个地方:能照着教程敲出if语句,但一到实际场景就懵——比如“检查某个服务进程是否存在,存在就重启,不存在就启动”,写出来的脚本要么永远不执行重启,要么每次运行都强行启动两次;再比如“遍历当前目录下所有.log文件并压缩”,结果把压缩包自己也塞进循环里反复处理,最后磁盘爆满。问题不在语法,而在对[ ]和[[ ]]的本质区别没吃透,对for变量作用域理解偏差,对while read读取换行符的陷阱毫无防备。

这个标题里的“快速入门”不是速成幻觉,而是指:用最贴近生产环境的真实逻辑,绕过教科书式抽象讲解,直击判断与循环在Shell中独有的行为特征。它不讲“if是条件语句”,而讲“为什么if [ $var = "ok" ]在$var为空时会报错,但if [[ $var == "ok" ]]不会”;不罗列for i in {1..5}的写法,而拆解“当文件名含空格、括号、中文时,for file in $(ls)为何必然崩溃,以及find -print0 | while IFS= read -r -d '' file才是唯一可靠解法”。关键词“条件判断语句”背后是Shell对退出状态码($?)的绝对依赖,“循环”背后是Bash对单词分割(Word Splitting)和路径名展开(Pathname Expansion)的不可回避机制。你不需要先学完POSIX标准,但必须清楚:Shell不是Python,它的“逻辑”长在系统调用的毛细血管里。适合谁?正在写部署脚本的开发、需要批量处理日志的运维、被CI/CD流水线里一堆.sh文件折磨的测试工程师——只要你的工作流里出现过“如果…就…”“对每个…做…”这类自然语言描述,这里就是你该停下来的路口。

2. 条件判断语句深度拆解:从语法表象到退出码本质

2.1 三类判断工具的本质差异:[ ]、[[ ]]与test

新手最容易栽跟头的地方,就是把[ ]当成万能括号。其实[是个外部命令(通常位于/usr/bin/[),而[[ ]]是Bash内置关键字。这意味着什么?看一个真实案例:某次线上巡检脚本要求判断配置文件是否包含特定参数,原始写法是:

if [ grep -q "timeout=30" /etc/app.conf ]; then echo "配置正常" fi

运行直接报错:[: missing]'。原因?[ ]命令要求**最后一个参数必须是]**,而grep -q "timeout=30" /etc/app.conf被整个当作一个字符串参数传给[,导致]`缺失。正确写法应是:

if grep -q "timeout=30" /etc/app.conf; then echo "配置正常" fi

或者用[[ ]]:

if [[ $(grep -o "timeout=30" /etc/app.conf) ]]; then echo "配置正常" fi

提示:[[ ]]内部支持命令替换$(),且不强制要求]作为独立参数,这是它比[ ]更安全的核心原因之一。

再看字符串比较的坑。当变量可能为空时:

user_name="" if [ $user_name = "admin" ]; then # 危险!实际执行的是 [ = "admin" ],语法错误 echo "是管理员" fi

[ ]会将空变量展开为空白,导致命令变成[ = "admin" ],[命令报错unary operator expected。而[[ ]]对此有容错:

if [[ $user_name == "admin" ]]; then # 安全,即使$user_name为空也不报错 echo "是管理员" fi

test命令与[ ]功能完全等价([ ]其实是test的符号别名),所以test "$user_name" = "admin"和[ "$user_name" = "admin" ]效果相同——关键在双引号保护。这才是[ ]的正确用法:永远给变量加双引号!

工具类型空变量安全正则匹配通配符匹配命令替换支持是否POSIX兼容
[ ]外部命令❌(需手动加引号)❌❌❌✅
test外部命令❌(需手动加引号)❌❌❌✅
[[ ]]Bash内置✅✅(=~)✅(==)✅❌

注意:[[ ]]的==支持通配符(如[[ $file == *.log ]]),而=仅字面匹配;=~支持正则(如[[ $ip =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]),但正则语法遵循Bash规则,非PCRE。

2.2 退出状态码:Shell逻辑的底层货币

所有条件判断的根基,是命令的退出状态码(Exit Status)。Shell中,0代表成功(true),非0代表失败(false)。if语句根本不关心命令输出什么,只看$?:

# 这个if判断的是touch命令的退出码,不是文件内容 if touch /tmp/testfile; then echo "文件创建成功(touch返回0)" else echo "文件创建失败(touch返回非0)" fi

实操中常见误区是混淆“命令输出”和“退出状态”。比如检查端口是否监听:

# 错误示范:用echo输出判断 if echo "$(netstat -tuln | grep ':8080')" | grep -q "LISTEN"; then echo "端口在监听" fi # 问题:即使netstat无输出,echo仍返回0,整个管道返回0,判断永远为真 # 正确做法:让grep决定退出码 if netstat -tuln | grep -q ":8080.*LISTEN"; then echo "端口在监听" else echo "端口未监听" fi

这里grep -q的关键是:匹配成功返回0,失败返回1。if直接捕获这个值,无需中间echo干扰。我见过太多脚本用if [ "$(command)" != "" ]来判断,这多了一层子shell开销,且当command输出含换行符时,[ ]会因参数过多报错。直接让原命令的退出码说话,是最高效、最可靠的判断方式。

2.3 文件测试与复合条件:生产环境高频场景

文件操作是脚本最常用场景。[ ]和[[ ]]都支持丰富的文件测试操作符,但用法细节决定成败:

# 检查文件存在且可读(推荐用-a连接,但注意优先级) if [ -f "/etc/passwd" -a -r "/etc/passwd" ]; then echo "passwd文件存在且可读" fi # 更清晰的写法:用&&连接多个测试(推荐) if [ -f "/etc/passwd" ] && [ -r "/etc/passwd" ]; then echo "passwd文件存在且可读" fi # [[ ]]支持更简洁的写法 if [[ -f "/etc/passwd" && -r "/etc/passwd" ]]; then echo "passwd文件存在且可读" fi

注意:[ ]中-a(and)、-o(or)已被POSIX标记为废弃,应优先使用&&/||。[[ ]]中必须用&&/||,-a/-o不被支持。

另一个高频需求:判断文件是否为空。-s测试非空,但要注意:

# 如果文件不存在,-s会返回false(即退出码1),这符合预期 if [ -s "/var/log/app.log" ]; then echo "日志文件有内容" else echo "日志文件为空或不存在" fi

但若需区分“空”和“不存在”,得用嵌套:

if [ ! -f "/var/log/app.log" ]; then echo "文件不存在" elif [ -s "/var/log/app.log" ]; then echo "文件非空" else echo "文件存在但为空" fi

2.4 实战避坑:那些让你深夜加班的隐藏雷区

  • 变量未定义导致语法错误:
    if [ $UNDEFINED_VAR = "yes" ]; then→ 实际执行[ = "yes" ],报错。永远用[ "${UNDEFINED_VAR:-}" = "yes" ]或[[ ${UNDEFINED_VAR:-} == "yes" ]],${VAR:-}提供默认空值。

  • 数字比较用错操作符:
    if [ $count = 5 ]; then→ 字符串比较!count=05时返回false。数字比较必须用-eq:if [ "$count" -eq 5 ]; then。

  • [[ ]]中正则匹配的锚点陷阱:
    [[ "abc123" =~ [0-9]+ ]]匹配成功,但[[ "abc123def" =~ [0-9]+ ]]也成功——因为正则默认不锚定。要精确匹配整行数字,必须加^和$:[[ "123" =~ ^[0-9]+$ ]]。

  • if后接命令组的括号选择:
    if cmd1 && cmd2; then ... fi和if { cmd1; cmd2; }; then ... fi效果不同。前者cmd1失败则cmd2不执行;后者{ }是命令分组,cmd1失败cmd2仍会执行(除非用&&显式连接)。生产脚本中,用&&明确依赖关系,避免{ }引入意外执行。

3. 循环结构实战解析:如何安全遍历一切“可迭代对象”

3.1for循环:表面简单,内藏玄机

for循环最常被滥用在文件遍历上。经典反模式:

# ❌ 危险!文件名含空格时崩溃 for file in $(ls *.log); do gzip "$file" done

$(ls *.log)执行后,输出被单词分割(Word Splitting):app log.txt error log.txt被切分为app、log.txt、error、log.txt四个参数,$file依次取值,导致gzip app报错。正确解法只有两个:

方案1:用glob直接遍历(最推荐)

# ✅ 安全!Bash自动处理空格、特殊字符 for file in *.log; do if [ -e "$file" ]; then # 防止无匹配时字面量*.log gzip "$file" fi done

方案2:用find+while(处理复杂条件)

# ✅ 安全!-print0和read -d ''配合处理任意文件名 find /var/log -name "*.log" -print0 | while IFS= read -r -d '' file; do gzip "$file" done

注意:find的-print0用ASCII 0分隔文件名,read -d ''以0为分隔符读取,彻底规避换行符、空格、制表符问题。IFS=防止read自身进行单词分割,-r禁用反斜杠转义。

for循环还支持C风格语法,适合计数:

# 等价于 for ((i=1; i<=10; i++)) for i in {1..10}; do echo "第$i次" done

但{1..10}是大括号扩展(Brace Expansion),在for之前就展开为10个参数,内存占用大;for ((i=1; i<=10; i++))是纯算术运算,更高效:

for ((i=1; i<=1000; i++)); do echo "$i" done

3.2while循环:事件驱动与流处理的基石

while的核心是条件持续为真时重复执行。最易错的是while read的变量作用域:

# ❌ 变量count在子shell中修改,外部不可见 count=0 cat list.txt | while read line; do ((count++)) echo "处理第$count行: $line" done echo "总计处理$count行" # 输出0!

管道|会创建子shell,count++只在子shell中生效。解决方案:

  • 重定向代替管道(推荐):

    count=0 while read line; do ((count++)) echo "处理第$count行: $line" done < list.txt echo "总计处理$count行" # 输出正确值
  • 用< <(command)进程替换(Bash特有):

    count=0 while read line; do ((count++)) echo "处理第$count行: $line" done < <(grep "ERROR" /var/log/syslog)

while的另一个杀手级应用是等待条件满足,比如等待服务端口就绪:

# 等待MySQL端口3306可连接,超时30秒 timeout=30 while ! nc -z localhost 3306; do ((timeout--)) if [ $timeout -le 0 ]; then echo "MySQL启动超时" exit 1 fi sleep 1 done echo "MySQL已就绪"

这里! nc -z利用nc的退出码:连接成功返回0(!使其为假,循环退出),失败返回1(!使其为真,继续循环)。

3.3until循环:逆向思维的优雅表达

until是while的逻辑反面:条件为假时执行,为真时停止。它让“等待失败”类逻辑更直观:

# 等待某个临时文件消失(表示任务完成) until [ ! -f "/tmp/task.running" ]; do echo "任务仍在运行..." sleep 2 done echo "任务已完成"

对比while [ -f "/tmp/task.running" ],语义相同,但until更贴近自然语言“直到文件不存在”。

3.4 循环控制:break、continue与信号处理

break和continue的行为与多数语言一致,但需注意嵌套层级:

for service in nginx mysql redis; do echo "检查服务: $service" for port in 80 3306 6379; do if nc -z localhost $port; then echo " $service在端口$port上运行" break 2 # 跳出两层循环,找到即停 fi done done

break 2跳出for port和外层for service,避免无谓检查。continue 2同理跳过当前外层循环迭代。

更关键的是循环中的信号处理。默认情况下,Ctrl+C会终止整个脚本。若需优雅退出循环:

trap 'echo "收到中断信号,正在清理..."; rm -f /tmp/lockfile; exit 1' INT for i in {1..100}; do if [ -f "/tmp/lockfile" ]; then echo "检测到锁文件,跳过" continue fi touch /tmp/lockfile # 执行耗时操作... sleep 1 rm -f /tmp/lockfile done

trap捕获INT信号(Ctrl+C),执行清理后退出,避免锁文件残留。

4. 综合实战:一个生产级日志轮转脚本的完整实现

4.1 需求分析:从模糊描述到精确逻辑

假设需求:“每天凌晨自动压缩/var/log/app/下所有.log文件,保留最近7天的压缩包,删除更早的”。这看似简单,但隐含多个技术点:

  • 安全遍历含空格的文件名;
  • 按修改时间排序并筛选最近7天;
  • 压缩后验证完整性;
  • 错误时发送告警(模拟);
  • 避免并发执行冲突。

4.2 脚本骨架与核心逻辑设计

#!/bin/bash # 日志轮转脚本:log_rotate.sh # 作者:某运维工程师 # 功能:安全压缩日志,按时间保留,异常告警 # ===== 配置区 ===== LOG_DIR="/var/log/app" ARCHIVE_DIR="/var/log/app/archive" DAYS_TO_KEEP=7 LOCK_FILE="/tmp/log_rotate.lock" # ===== 函数定义 ===== # 获取当前日期(用于归档名) get_date() { date +"%Y%m%d_%H%M%S" } # 发送告警(此处简化为写日志) send_alert() { local msg="$1" echo "[$(date)] ALERT: $msg" >> /var/log/log_rotate.log } # ===== 主流程 ===== main() { # 1. 检查锁文件,防止并发 if [ -f "$LOCK_FILE" ]; then send_alert "脚本已在运行,PID $(cat $LOCK_FILE)" exit 1 fi echo $$ > "$LOCK_FILE" # 2. 创建归档目录 mkdir -p "$ARCHIVE_DIR" # 3. 安全遍历所有.log文件(用find -print0) find "$LOG_DIR" -maxdepth 1 -name "*.log" -print0 | while IFS= read -r -d '' logfile; do # 跳过归档目录自身 if [[ "$logfile" == "$ARCHIVE_DIR"/* ]]; then continue fi # 生成归档名:原文件名+时间戳 basename=$(basename "$logfile") archive_name="${basename%.log}_$(get_date).tar.gz" archive_path="$ARCHIVE_DIR/$archive_name" # 4. 压缩并验证 if tar -czf "$archive_path" "$logfile" >/dev/null 2>&1; then # 验证压缩包 if tar -tzf "$archive_path" >/dev/null 2>&1; then echo "已归档: $logfile -> $archive_name" # 成功后删除原日志 rm -f "$logfile" else send_alert "归档验证失败: $archive_path" rm -f "$archive_path" fi else send_alert "归档失败: $logfile" fi done # 5. 清理旧归档(保留最近DAYS_TO_KEEP天) find "$ARCHIVE_DIR" -name "*.tar.gz" -type f -mtime +$DAYS_TO_KEEP -delete # 6. 清理锁 rm -f "$LOCK_FILE" }

4.3 关键环节详解与参数推演

锁文件机制:echo $$ > "$LOCK_FILE"写入当前进程PID,后续检查存在即阻塞。为什么不用flock?因为flock在某些精简系统(如Alpine)中可能未预装,而[ -f ]是POSIX通用方案。

时间筛选逻辑:-mtime +7表示“修改时间超过7天”,但注意-mtime基于24小时周期计算,非精确到秒。若需精确到小时,用-newermt(GNU find特有):

# 删除7天前的文件(精确到秒) find "$ARCHIVE_DIR" -name "*.tar.gz" -type f ! -newermt "$(date -d '7 days ago' '+%Y-%m-%d %H:%M:%S')" -delete

压缩验证必要性:tar -czf即使失败也可能生成损坏文件。tar -tzf尝试列出内容,成功说明压缩包结构完整。我曾在线上遇到tar因磁盘满写入截断,-czf返回0但文件损坏,-tzf立即暴露问题。

错误处理粒度:脚本对单个文件失败不中断整体流程(send_alert后continue),确保其他日志正常处理。这是生产脚本与玩具脚本的核心区别——失败隔离,而非全局崩溃。

4.4 实操部署与验证步骤

  1. 权限设置:

    chmod +x log_rotate.sh # 测试执行(不删原文件) ./log_rotate.sh
  2. 添加到crontab(每天凌晨2点):

    # 编辑crontab crontab -e # 添加行 0 2 * * * /path/to/log_rotate.sh >> /var/log/log_rotate_cron.log 2>&1
  3. 验证锁机制:
    手动运行脚本后,在另一终端执行./log_rotate.sh,应看到告警并退出。

  4. 模拟日志文件测试:

    # 创建测试文件(含空格) touch "/var/log/app/test log.log" "/var/log/app/error.log" # 运行脚本,检查归档目录 ls -l /var/log/app/archive/
  5. 检查日志:
    tail -f /var/log/log_rotate.log观察归档记录与告警。

5. 常见问题与排查技巧实录:来自真实战场的教训

5.1 “语法错误:意外的文件结尾” —— 引号与括号的隐形战争

现象:脚本执行报错line xx: syntax error: unexpected end of file。
根因:最常见的原因是Windows换行符(CRLF)。在Windows编辑器中保存的脚本,每行末尾是\r\n,而Linux只认\n,\r被解释为字符,导致if、for等结构的then、do找不到匹配的fi、done。
排查:

# 查看文件结尾符 file script.sh # 输出:script.sh: Bourne-Again shell script, ASCII text executable, with CRLF line terminators # 或用hexdump hexdump -C script.sh | tail # 查找0d 0a(CRLF)

解决:

# 方法1:dos2unix命令 dos2unix script.sh # 方法2:sed替换 sed -i 's/\r$//' script.sh # 方法3:vim中 :set ff=unix :wq

5.2 “命令未找到” —— PATH陷阱与Shebang失效

现象:脚本中python3 --version报错command not found,但在终端直接运行正常。
根因:脚本执行时的PATH环境变量与交互式Shell不同。cron执行时PATH通常极简(如/usr/bin:/bin),而python3可能在/usr/local/bin/python3。
排查:

# 在脚本开头添加调试 echo "PATH=$PATH" >&2 which python3 >&2

解决:

  • 显式指定绝对路径:/usr/local/bin/python3 --version
  • 在脚本开头重置PATH:export PATH="/usr/local/bin:/usr/bin:/bin"
  • 使用env命令(推荐):env python3 --version,env会搜索完整PATH

5.3 “循环只执行一次” —— 管道导致的子shell迷局

现象:cat data.txt | while read line; do echo $line; count=$((count+1)); done; echo $count输出0。
根因:管道左侧的while在子shell中运行,count变量修改不回传父shell。
终极解决方案对比:

方案代码示例优点缺点适用场景
重定向while read line; do ...; done < data.txt简单,变量有效需提前知道输入源文件输入
进程替换while read line; do ...; done < <(command)可接任意命令输出Bash特有命令输出流
临时文件command > /tmp/tmpfile; while read ...; done < /tmp/tmpfile兼容性最好磁盘IO开销严格POSIX环境

5.4 “文件名乱码” —— 字符编码与locale的无声冲突

现象:脚本处理中文文件名时,ls显示正常,但for file in *.log中$file打印为??.log。
根因:Shell的LC_COLLATE或LC_CTYPE环境变量未设为UTF-8,导致glob匹配失败。
排查:

locale # 若输出中LC_CTYPE="POSIX",则问题确认

解决:

# 在脚本开头强制设置 export LC_ALL=C.UTF-8 # 或更精准 export LC_CTYPE=en_US.UTF-8

5.5 “性能瓶颈” —— 为什么find比for file in *.log慢10倍?

现象:遍历10万个文件,for file in *.log秒级完成,find . -name "*.log"耗时数分钟。
真相:*.log是Shell自身glob扩展,在for循环前一次性展开为10万个参数,内存占用高但速度快;find是外部命令,需fork进程、遍历目录树、逐个匹配,CPU和IO开销大。
优化策略:

  • 小规模(<1000文件):用for file in *.log,简洁高效;
  • 大规模(>1000文件):用find -maxdepth 1 -name "*.log" -print0 | while IFS= read -r -d '',避免参数列表过长(ARG_MAX限制);
  • 极端规模(百万文件):改用find -printf '%p\0' | ...,-printf比-print0更快。

实测数据:在10万文件目录中,for耗时0.3s,find -print0耗时1.2s,find -printf耗时0.8s。差异源于-printf是find内置动作,无需额外格式化开销。

6. 进阶思考:从脚本到工程化实践的跨越

6.1 参数化与配置分离:告别硬编码

生产脚本必须支持灵活配置。将LOG_DIR、DAYS_TO_KEEP等移到外部配置文件:

# config.conf LOG_DIR="/var/log/app" DAYS_TO_KEEP=7 ARCHIVE_DIR="/var/log/app/archive" # 脚本中加载 if [ -f "./config.conf" ]; then source "./config.conf" elif [ -f "/etc/log_rotate.conf" ]; then source "/etc/log_rotate.conf" else echo "配置文件未找到" exit 1 fi

source(或.)命令在当前shell中执行配置文件,变量立即生效。比export更安全,避免污染全局环境。

6.2 日志分级与结构化输出

简单echo无法满足审计需求。添加日志级别:

log_info() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] INFO: $*" >> "$LOG_FILE"; } log_warn() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] WARN: $*" >> "$LOG_FILE"; } log_error() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] ERROR: $*" >> "$LOG_FILE"; } # 使用 log_info "开始轮转" log_warn "文件$logfile过大,跳过压缩"

结构化日志便于ELK等系统采集,$(date '+%Y-%m-%d %H:%M:%S')提供ISO 8601格式时间戳。

6.3 可测试性设计:让脚本能被单元测试

Shell脚本也能单元测试!关键在于函数化与依赖注入:

# 将核心逻辑抽为函数 rotate_file() { local logfile="$1" local archive_dir="$2" # 实现压缩逻辑... } # 测试时可mock依赖 mock_tar() { echo "MOCK: tar -czf $2 $1" > /tmp/mock_output } # 替换原命令 tar() { mock_tar "$@"; } # 调用函数测试 rotate_file "/tmp/test.log" "/tmp/archive"

通过shunit2框架,可编写断言验证函数行为,提升脚本可靠性。

6.4 安全加固:最小权限原则落地

脚本不应以root运行所有操作。最佳实践:

  • 日志轮转本身用普通用户:chown appuser:appgroup /var/log/app,脚本以appuser运行;
  • 仅需root的操作单独提权:如systemctl restart app,用sudo并配置/etc/sudoers限定命令:
    appuser ALL=(root) NOPASSWD: /bin/systemctl restart app
  • 敏感操作加确认:echo "即将删除$old_file,确认?(y/N)"; read -r confirm; [[ $confirm == "y" ]] || exit 0

我在某金融客户项目中,曾因脚本以root运行导致误删核心配置。后来所有脚本强制遵循“能不用root,绝不用root;必须用时,只授最小必要权限”原则,再未发生类似事故。

7. 个人经验总结:那些没人告诉你的“潜规则”

写完这个系列第三篇,回头看看最初那个“快速入门”的标题,突然觉得它像一句温柔的提醒:入门不是抵达终点,而是看清脚下每一颗石子的形状与纹路。Shell脚本的条件与循环,从来不是语法题,而是系统哲学的入口。我踩过的最深的坑,往往不在if写错括号,而在忽略了一个事实:Shell是Unix哲学的活体标本——它不做假设,只响应事实;它不保证安全,只提供工具;它不隐藏复杂,只等待你亲手解开。

比如[[ ]]的正则匹配,文档说它支持=~,但没人告诉你BASH_REMATCH数组会捕获分组,[[ "abc123" =~ ([0-9]+) ]] && echo "${BASH_REMATCH[1]}"能直接提取123——这比sed或awk在单行处理中更轻量。再比如while read的-r选项,手册写“do not allow backslashes to escape any characters”,但实际意义是:没有-r,read会把行尾反斜杠当作续行符,导致echo "line1\和line2"被合并为line1line2,这在处理密钥文件时是致命的。

还有个血泪教训:永远在脚本开头加set -euo pipefail。-e让任何命令失败立即退出,-u禁止未定义变量,-o pipefail让管道中任一命令失败整个管道失败。这四行代码,省去我80%的调试时间。它不是银弹,但像安全带——你不觉得它重要,直到出事。

最后分享个小技巧:当你不确定某个命令在脚本中如何行为时,别猜,用set -x打开调试模式。它会在执行前打印每条命令及展开后的参数,像X光一样照出变量的真实面目。关掉它只需set +x。这招救过我无数个凌晨三点。

Shell脚本的魅力,正在于它的“不完美”——它不试图掩盖系统的粗糙,反而邀请你直面。当你能从容写出find /path -print0 | while IFS= read -r -d '' file; do ...; done,你写的不再是脚本,而是与操作系统的一次精密对话。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进