7天掌握Shell脚本:日志清理自动化实战与踩坑指南

7天掌握Shell脚本:日志清理自动化实战与踩坑指南 如果你也是一名运维开发或者只是偶尔管几台 Linux 服务器大概率经历过这样的深夜手机告警连着响磁盘使用率刷到 95% 以上。你睡眼惺忪地爬起来先df -h看一眼再du -sh /data/logs/*一层层找过去终于定位到某个业务目录下的老日志然后手动rm -f删掉几十个文件。运气好十分钟解决运气差误删了正在写入的日志第二天业务方准来找你。我就是从这种“手动删日志”的日常里走出来的。原本每周至少要花半天时间重复这件事后来我用 7 天掌握了 Shell 脚本把日志清理做成定时任务工作日里能腾出大约 30% 的时间去做更有价值的事。这篇内容不讲虚的我把自己真实的踩坑过程、脚本代码、排查方法和学习路线全部整理出来。无论你是第一次接触 Shell 脚本还是已经在写一点自动化脚本但经常出问题这篇文章应该都能给你一些可直接拿走的东西。1. 为什么我从“删日志”开始搞自动化运维而不是一上来就上平台很多人一提到自动化运维第一反应就是上一套 Ansible、SaltStack或者干脆研究各种监控告警平台觉得不搞点“大平台”就不好意思说自己做过自动化。我的经验恰恰相反绝大多数人的第一个自动化项目应该选一个足够疼痛、足够简单、失败代价可控的场景而不是从底层工具链开始卷。日志清理就是这种“天选场景”。首先它是每个用 Linux 的人都会碰到的真问题。日志文件会持续增长磁盘不会无限扩容尤其是跑 Java 应用或 NGINX 的服务器一个logs目录下可能有几十个按天滚动的文件没人管的话两三个月不到磁盘就满了。其次清理过期日志这个动作本身逻辑很清晰找出 N 天前的文件然后删除它们。没有复杂的业务耦合也不会因为误操作把数据库搞挂。删错了顶多丢几份历史日志这种低风险高复用的场景拿来练手再合适不过。当时我给自己做了一个粗略的时间统计每天会固定看几次磁盘剩余量、登录服务器点进日志目录、数一数有哪些大文件遇到磁盘告警还要临时手动清理。零零散散加起来一周至少花 4 到 6 个小时在这类琐事上。以每周 40 小时工作时间计算这就是 10% 到 15% 的工时。但手动操作的最大成本还不只是时间本身而是它会频繁打断你的工作流。你刚写两行代码告警弹出来你得切换上下文去处理处理完再切回来重新回忆刚才的思路这个切换成本往往比操作本身还高。所以实现自动化后真正省下的不只是那半小时的清理时间还有整个工作日的连贯性。对比维度手动删日志阶段Shell 脚本自动化阶段单次耗时5 到 15 分钟还要排查定位定时任务自动运行耗时趋近于 0误删风险高夜间操作易看错文件名低脚本带试运行模式可提前验证时间占用每周 4 到 6 小时且碎片化严重写脚本一次投入后续维护极少可复用性每台服务器都要重新手工操作拷贝脚本改参数即可批量部署这个对比也让“解放 30% 工作时间”这件事变得不那么夸张因为在自动化平台上投入的 7 天学习时间实际上是在给自己买“持续性的时间折扣”。更重要的是通过这个很小的项目我熟悉了 Shell 的基本语法、find和cron的用法、脚本调试方式这些能力后来直接迁移到了更多自动化运维场景里。换句话说我这个入门不是从语法书开始的而是从真实痛点反推回去的学到的每一样东西都能立刻派上用场。2. 7 天时间线拆解我不是按语法书硬啃 Shell我见过很多人的 Shell 学习路径是这样的先买一本六百页的《Shell 脚本编程》从第一章“Shell 是什么”开始看看到变量和运算符还能坚持到sed、awk就逐渐放弃最后书签停留在第三章。我不打算这么学。我的方法很简单就是把“写一个日志清理脚本”当成一个完整的项目去倒推自己需要什么然后只学够用的命令边做边补。2.1 第 1 到 2 天先学会用命令“盘”一遍日志目录头两天我几乎没有新建脚本文件做的事情是在命令行里反复敲命令目的是搞清楚下面几个问题日志目录下到底有哪些文件它们的体积和时间分布长什么样有哪些字段和用法是清理时要用的整个过程就是一边查man帮助一边敲。比如用du -sh *看目录整体占用用ls -lh看清楚文件大小用find /data/logs -type f -name *.log -mtime 7 -ls找出 7 天前的日志。我也会顺手统计某一天的错误日志数量比如grep -c ERROR /data/logs/business.log.2024-12-01。这些操作看起来很简单但它们正是脚本语法之外的“手感”。这里我想强调一个容易被新手忽略的点Shell 脚本本质上是若干命令的组合而不是一门独立语言。很多教程上来就讲for循环、if判断让学生误以为要学的是编程语法但实际上 Shell 的强项是把现成的命令行工具用管道和变量串起来。如果一条命令本身都写不熟练到了脚本里只会更懵。所以我前两天的主要目标是“命令优先”不求会写循环只求能靠一条命令完成一个日志分析任务比如按大小排序、筛选 N 天前的文件、统计错误数量等。同时我也鼓励大家用一个笨办法做练习把自己的命令行历史history导出来看看平时手动操作时重复输入了哪些命令。那些重复三次以上的命令将来都应该变成脚本里的内容。我就是这样锁定了几个高频场景——登录后先看磁盘占用、进入日志目录找大文件、确认服务进程还在不在——这些就是第一批自动化目标。2.2 第 3 到 5 天从最小可用的清理脚本开始边写边补到第三天我已经不满足于一条条敲命令了因为我意识到操作的完整链路每次都要重新输入太蠢了。于是我开始把之前的命令固化到一个脚本文件里。这个阶段的原则是“先让它能被跑起来”不追求优雅和泛化。我写的第一版清理脚本非常简单大概长这样#!/usr/bin/env bash find /data/logs -type f -name *.log.* -mtime 7 -delete一行命令能有什么技术含量但它解决了一个真实问题几秒内删掉了所有超过 7 天的历史日志。不过很快我就发现了几个问题日志目录不只/data/logs一个有些服务和 Nginx 的日志在别的地方每次想改保留天数都要进脚本改代码很麻烦如果哪天看错参数直接删了不该删的文件连后悔药都没有。于是接下来的两天我就是在给这个脚本做“增量升级”。为了复用我把目录和天数改成了脚本参数为了安全我加了试运行模式DRY_RUN为了让执行过程可追踪我必须把每次清理的文件名和数量写到日志文件里。变量、函数、循环、条件判断这些语法就是在这个过程中逼着自己去学的。比如要统计删除了多少个文件我就必须了解for循环怎么写要判断某个目录是否存在就要学会if [ ! -d $dir ]。一个印象很深的细节是那时我对find -delete很不放心总觉得它“删得太沉默”所以我改成先用find把文件名列出来存进一个变量再for循环遍历执行rm -f。这种写法在性能上当然不如直接-delete但它能让我打印出每一个被删除的文件名对当时的我来说可视化带来的安全感远比那点性能更重要。这个“先看效果再优化性能”的思路我到现在都在用。2.3 第 6 到 7 天加保护、加日志、让它自己定时跑起来当脚本能在手动执行的情况下稳定运行我就开始考虑“自动化”的最后一个环节怎么让它不需要人天天盯着执行。第六天我主要做的是加固脚本。比如在脚本开头加上set -u避免变量没定义时被 Shell 悄悄当成空字符串继续跑这种错误很隐蔽尤其当你把目录名写错成空变量时可能直接变成对整个文件系统操作。那两天我浏览了很多危险案例意识到必须在脚本里显式判断入参是否为有效目录否则遇到错误参数时脚本可能“安静地失败”这是最坑人的。第七天我把脚本接入了crontab并仔细验证了定时任务的执行环境、输出日志、权限问题。到这一步我才敢说自己真正“掌握”了 Shell 脚本在运维里的基本用法学会用命令分析问题学会把命令组装成脚本学会给脚本加参数和日志最后学会用定时任务让它脱离人工。整个过程是滚动向前的每次只解决眼前一个具体问题而不是先把语法学完再上机。3. 日志清理脚本的完整实现每个细节都值得较真下面给出一个我后来稳定运行了很久的脚本模板相比最初的版本它已经把容错、试运行、日志记录这些关键点都涵盖了。我会把关键参数拆开解释并说明为什么这样写。3.1 需求与前置约定先明确目标清理/data/logs下超过 7 天的历史日志。这里的“历史日志”我定义为带时间戳或编号的归档文件比如app.log.2025-01-01、app.log.1而不包括当前正在写入的app.log。之所以这样区分是因为在logrotate或应用框架的滚动机制下当前活动日志通常是不带后缀的那个直接按时间删归档文件风险最小。我还希望脚本支持传入目录和保留天数两个参数这样遇到多个日志目录时就不用改代码。为了保证首次使用安全脚本默认开启DRY_RUN模式只打印“将删除哪些文件”而不真正执行删除操作。确认无误后再通过环境变量关闭试运行这个习惯帮我避开了很多误删问题。3.2 脚本代码与关键参数说明#!/usr/bin/env bash # # clean_old_logs.sh - 清理指定目录下的过期日志文件 # # 用法: # ./clean_old_logs.sh [日志目录] [保留天数] # # 示例: # ./clean_old_logs.sh /data/logs 7 # # 环境变量: # DRY_RUN1 时只模拟执行不真正删除默认开启 # DRY_RUN0 时执行实际删除 # set -u # 参数赋值允许用户传入未传时使用默认值 LOG_DIR${1:-/data/logs} RETENTION_DAYS${2:-7} DRY_RUN${DRY_RUN:-1} # 检查目录是否有效避免变量为空时误操作 if [ ! -d ${LOG_DIR} ]; then echo [ERROR] 目录不存在: ${LOG_DIR} exit 1 fi STAMP$(date %Y-%m-%d %H:%M:%S) OUTPUT_LOG/var/log/clean_old_logs/clean.log # 找出所有满足条件的归档日志 FILES$(find ${LOG_DIR} -type f \( -name *.log.* -o -name *.log \) -mtime ${RETENTION_DAYS} 2/dev/null) # 没有过期文件时直接退出 if [ -z ${FILES} ]; then echo ${STAMP} [INFO] 目录 ${LOG_DIR} 下没有需要清理的过期日志 exit 0 fi # 逐条删除并打印/记录日志 COUNT_DELETED0 for f in ${FILES}; do if [ ${DRY_RUN} 1 ]; then echo [DRY_RUN] 将删除: ${f} else rm -f ${f} echo ${STAMP} [DELETE] ${f} ${OUTPUT_LOG} fi COUNT_DELETED$((COUNT_DELETED 1)) done if [ ${DRY_RUN} 1 ]; then echo ${STAMP} [DRY_RUN] 发现 ${COUNT_DELETED} 个过期文件未实际删除 else echo ${STAMP} [INFO] 已清理 ${COUNT_DELETED} 个文件目录: ${LOG_DIR} ${OUTPUT_LOG} fi exit 0这段脚本里每个部分都有明确的意图。先说LOG_DIR${1:-/data/logs}这是 Shell 中非常常用的参数默认值写法意思是如果第一个参数没有传就使用/data/logs。配合RETENTION_DAYS${2:-7}可以让脚本适配多个场景比如想清理 Nginx 日志时直接调用./clean_old_logs.sh /usr/local/nginx/logs 15不需要改任何内部代码。再说find的条件部分-type f限定只处理普通文件不会把目录本身删除\( -name *.log.* -o -name *.log \)用来匹配归档日志和当前日志。如果你的滚定日志还包含.gz压缩包比如app.log.2025-01-01.gz可以在-name条件里追加-o -name *.gz。-mtime ${RETENTION_DAYS}是找出修改时间早于“7 天前”的文件注意这里的加号表示“大于”如果写成7则表示“恰好等于 7 天这个时间窗口”两者含义不同非常容易搞混。我实际用了很长时间之后才发现一个细节find -mtime是按“天”为粒度取整的如果你在凌晨 00:10 执行脚本而文件是在 7 天前的 23:50 修改的-mtime 7可能会放过它一小段时间。如果业务日志增长非常快希望精确到分钟可以改用-mmin 1008010080 分钟等于 7 天或者用-newermt 7 days ago做更精确的范围判断。这里不再展开但你要知道这个精度差异是真实存在的。DRY_RUN设计是整个脚本最重要的安全阀。我第一次写这个脚本时没有试运行模式直接模拟删除后发现把自己需要保留的 30 天前的一份报表日志删掉了虽然那份日志不是核心业务但教训很深刻。后来我给所有涉及删除或覆盖操作的脚本都加上了同样的开关默认只打印将要做什么看清楚没问题后才真正执行。尤其是给crontab部署自动化任务前一定要先以试运行模式跑 3 天观察输出是否完全符合预期。3.3 常见循环写法的适用场景上面脚本里我用的是for f in ${FILES}循环这是初学者最容易理解的写法。但 Shell 里循环至少有三种常见风格对应不同场景写法示例适用场景for...in遍历列表for f in /data/logs/*.log; do echo $f; done文件名列表来自路径展开时最自然C 风格循环for ((i1; i10; i)); do echo $i; done明确需要数字序号时while read逐行遍历find ... | while read f; do ...; done处理文件名内包含空格、换行符等特殊字符时对于日志清理脚本如果文件名都比较规范for...in够用。但如果日志文件由人为上传生成文件名可能包含空格比如user report 2025.log问题就来了Shell 默认按空格做分词${FILES}会被拆成多个字段导致一个完整文件名被拆成好几个部分不仅删除不了文件还可能报错。我遇到过一位同事上传了一个叫v2.0 final backup.log的文件脚本跑完后它还在原地而命令日志里全是No such file or directory。第五章我会重点讲这个坑的排查过程。这里顺便提一下 C 风格循环的用处。它不只可以输出数字还能在你需要“把保留 7 天的日志按天压缩成一个 tar 包并删除原文件”时用来拼接日期。比如从 7 天前的那一天开始循环到今天每天执行一次归档逻辑这在备份场景里非常实用。3.4 交给 crontab 之前必须做的三件事手动执行脚本已经稳定之后就要让它自动跑。我的计划是每天凌晨 02:00 执行一次这时业务流量最低清理动作对线上影响最小。于是添加了这个定时任务条目0 2 * * * /usr/local/bin/clean_old_logs.sh /data/logs 7 /var/log/clean_old_logs/clean.log 21这里必须注意几个细节。第一脚本路径和日志输出路径都要写绝对路径。crontab 执行时的环境和你手动登录 shell 时的环境不同如果在脚本里依赖相对路径十有八九会执行失败。第二 ... 21是标准输出和错误输出都追加到同一个日志文件否则脚本出错时你收不到任何反馈排查起来非常麻烦。第三脚本文件本身要有执行权限需要先执行chmod x /usr/local/bin/clean_old_logs.sh。如果之后发现定时任务根本没跑优先检查脚本开头有没有写#!/usr/bin/env bash这一行。有些脚本是在 Windows 上编辑后传上服务器的可能带着看不见的\r回车字符导致系统在解析第一行时找不到解释器。用file clean_old_logs.sh命令可以快速查看脚本的字符编码和换行类型能识别出很多类似问题。crontab 相关的问题排查我在第五章会再展开这里先记一句话配置完定时任务后不要只等第二天看结果最好先手动执行一次脚本确认路径和权限都能用。4. 一个脚本远远不够把 Shell 能力复制到其他运维场景学会日志清理只是一个开始。真正让我觉得“自动化运维”不是空话的是脚本能力和 Shell 思维方式被复制到其他场景之后。后面这几段我会按我实际落地的顺序讲每个都保留了最核心的代码骨架你可以直接改编成自己的工具。4.1 磁盘水位监控脚本从“回收”升级到“预警”日志清理脚本解决的是“日志已经占用太多空间”的问题但我很快意识到应该在磁盘还没满的时候就收到预警而不是等到日志把磁盘塞满了再被动清理。于是我用 Shell 写了一个磁盘水位监控脚本本质上是对df命令输出的二次加工。#!/usr/bin/env bash THRESHOLD${1:-80} WATCH_DIR${2:-/data} ALERT_TO${3:-opsexample.com} USAGE$(df -P ${WATCH_DIR} | awk NR2 {print $5} | tr -d %) if [ ${USAGE} -gt ${THRESHOLD} ]; then echo $(date %Y-%m-%d %H:%M:%S) [WARN] ${WATCH_DIR} 使用率: ${USAGE}% | mail -s 磁盘空间告警: ${USAGE}% ${ALERT_TO} fi你可以看到这段脚本和日志清理脚本的核心逻辑很像接受参数、利用命令输出、做分支判断。这就是 Shell 脚本最迷人的一点——它没有太多神秘的新概念你只是把 Linux 命令组合得更有结构而已。df -P中的-P参数是保证输出格式不带换行方便awk提取NR2表示取第二行也就是目标文件系统的统计行tr -d %的作用是把百分号删掉方便在if里做数值比较。我把它加进 crontab每 5 分钟检查一次磁盘。这样每次日志清理脚本执行之前系统已经为“磁盘管理”建立了预警机制磁盘就算突然暴涨我也能提前收到邮件而不是等用户反馈服务写不进日志才反应过。4.2 服务挂了自动拉起比告警更省心日志和磁盘之外另一个高频手动操作是重启服务。有一段时间业务服务偶尔会因内存溢出退出我总在半夜接到“页面打不开”的通知然后登录服务器敲systemctl restart或sh start.sh。重复几次后很自然地想能不能让 Shell 在发现服务不在线时自动重启它存活检测可以分两种思路一种是看进程在不在一种是主动探测一个健康接口。对 Java 服务这种单进程场景第一种思路最简单直接。#!/usr/bin/env bash PROCESS_NAMEjava START_CMD/opt/app/start.sh LOG_FILE/var/log/app_auto_restart.log if ! pgrep -f ${PROCESS_NAME} /dev/null 21; then echo $(date %Y-%m-%d %H:%M:%S) [INFO] 服务进程不存在尝试拉起 ${LOG_FILE} nohup ${START_CMD} /dev/null 21 fi这个脚本的核心是pgrep -f-f参数表示匹配完整命令行。如果不加-fpgrep java可能连脚本自身都匹配不到导致检测失效。执行拉起时我用nohup确保服务不会因为 shell 退出被附带杀掉并把输出重定向到/dev/null避免日志文件被nohup.out撑大。写这个脚本时我犯过一个低级错误就是没有加条件判断就直接pkill -f java结果把服务器上其他 Java 进程也带崩了。后来我改为只匹配特定端口或特定 jar 包的名字比如pgrep -f app.jar并在拉起前先记录当前时间、进程不存在的原因这样每次自动拉起都有痕迹可查。类似的“带监控的自动动作”才是可靠的自动化运维而不是简单粗暴地“看到没了就拉”。4.3 批量操作场景文件打包与清理的 for 循环实战Shell 脚本在批量处理场景里的优势也很明显。比如日志目录下每天都有大量归档文件我需要把 7 天前的.log文件打包成.tar.gz再删除原文件或者要把/backup下超过 30 天的临时目录清理干净。这类操作如果靠手动一条条执行非常容易出错但用脚本批量做就很稳。#!/usr/bin/env bash # 按天归档把旧日志打包后删除原文件 LOG_DIR/data/logs ARCHIVE_DIR/data/archive DAYS_AGO7 # 计算 7 天前的日期如 2025-01-01 OLD_DATE$(date -d ${DAYS_AGO} days ago %Y-%m-%d) find ${LOG_DIR} -type f -name *.log.* -mtime ${DAYS_AGO} -print0 | while read -r -d f; do BASE_NAME$(basename ${f}) # 把每个旧日志单独打包文件名带上日期 tar -czf ${ARCHIVE_DIR}/${BASE_NAME}.${OLD_DATE}.tar.gz -C ${LOG_DIR} $(basename ${f}) if [ $? -eq 0 ]; then echo $(date %Y-%m-%d %H:%M:%S) [INFO] 已归档 ${f} /var/log/archive.log rm -f ${f} else echo $(date %Y-%m-%d %H:%M:%S) [ERROR] 归档失败 ${f} /var/log/archive.log fi done这段脚本比前面几个复杂的地方在于find的输出经过了-print0它能让分隔符不再是换行而是空字符\0对应的while read -r -d 就是按空字符读取每一行。这样即使文件名里有空格也不会被拆开。这也提前预告了第五章里文件名带空格的处理方式。这段脚本同时展示了for/while循环结合文件操作、命令执行结果判断、错误日志采集的完整套路。等你能熟练写出这种小工具运维日常里的“重复性动作”基本上都能被脚本接管。你先别急着学 Ansible 那套批量下发Shell 脚本先把单机上的痛解决了再谈大规模管理才有底。5. Shell 实战中的高频坑位与排查方法写 Shell 脚本这种事语法不难真正的门槛都在“运行环境和边界条件”上。下面几个坑我全踩过有些至今还在影响我的脚本风格。把它们整理成一张速查表遇到问题可以对照着看。5.1 脚本文件编码与中文乱码问题Shell 脚本文件本身是纯文本但编辑环境千奇百怪最常见的两个坑是字符编码不对和换行符是 Windows 的\r\n。如果你在 Windows 上写脚本再传到 Linux 上运行大概率会看到类似bad interpreter: /bin/bash^M: no such file or directory的报错。这个^M就是 Windows 换行留下的\r字符。第一种定位方式是直接执行file clean_old_logs.sh它会显示文件的换行类型比如CRLF line terminators。如果显示是CRLF用dos2unix clean_old_logs.sh转一下即可。没有dos2unix时也可以执行sed -i s/\r$// clean_old_logs.sh效果是一样的。关于字符编码Shell 脚本最好统一使用 UTF-8且在脚本文件开头声明不要使用中文注释时不带-的话会引入 BOM 头可能导致第一行解析失败。查看脚本当前编码可以用file -bi script.sh输出里会标注charsetutf-8或charsetus-ascii等信息。如果文本里混入中文乱码可以先确认系统当前locale再确认编辑器默认保存编码。有一个经验法则生产环境服务器上的脚本和配置文件注释尽量用英文或者确保团队统一用 UTF-8 编辑否则容易因为不同开发环境之间的编码差异导致莫名其妙的解析错误。5.2 for 循环按“空格”拆词文件里带空格怎么办for f in ${FILES}看起来没问题实际上它遵循了 Shell 的默认分词规则把变量内容按空格、制表符和换行符拆成多个字段。目录名或文件名一旦包含空格就会出问题。比如/data/logs/user report 2025.log会被拆成/data/logs/user、report、2025.log三个词导致文件根本找不到。解决方式很多我按实用性排序。第一优先不要用for遍历find的输出改用while read。第二优先把所有文件名用引号包裹。稳妥方案是下面的写法可以处理绝大多数特殊字符find ${LOG_DIR} -type f -name *.log.* -mtime ${RETENTION_DAYS} -print0 | while read -r -d f; do echo 处理文件: ${f} rm -f ${f} done这里使用零字节分隔符传递到while read配合-r防止反斜杠转义。如果你看到read -r -d 觉得费解就记住这句话-print0配合-d 是 Shell 处理“文件名不可预测”时的黄金组合。我写这类脚本时还发现一个附加问题while管道中的代码会运行在一个子 Shell 里如果你在循环里修改变量值循环结束后主脚本里是拿不到最新值的。比如想统计总共删了多少个文件并打印直接在循环里做累加会失效。解决办法是使用进程替换或临时文件尽可能将结果输出到临时文件后读取或者把统计逻辑直接放进循环内 echo。我通常选择最简单的方式循环里不要依赖全局变量每条处理结果都即时输出到日志文件。典型问题可能原因快速排查方法bad interpreterWindows 换行符\rfile script.sh再用dos2unix转换中文注释乱码文件编码不对file -bi script.sh统一改 UTF-8文件名带空格删不掉for按空格分词改用while read -d crontab定时不执行路径或环境变量不一致手动执行一次并检查/var/log/cron脚本不输出任何结果set -e遇到非零退出码用bash -x script.sh逐行调试5.3 crontab 执行不生效的排查清单几乎每个接触定时任务的人都会遇到“明明手动执行脚本没问题放进 crontab 就不跑”的情况。这类问题我总结成下面四个步骤按顺序排查基本都能解决。第一先确认 crond 服务本身在运行systemctl status crond或service cron status如果服务没启动后面全是白搭。第二确认脚本有执行权限chmod x /path/to/script.sh同时确认脚本第一行的路径正确且对应的解释器存在。第三最好把 crontab 条目里的输出重定向到日志文件比如 /var/log/clean_old_logs/clean.log 21这样即使脚本报错也至少能看到一份错误输出。第四检查crontab -e编辑的内容有没有被某些编辑器自动加了奇怪的字符比如用 Windows 记事本保存后上传每行结尾带着\r会导致 cron 解析不出来。环境变量是另一个特别容易忽略的坑。你手动登录时Shell 会加载/etc/profile、~/.bashrc等其中定义了你的PATH但 cron 执行命令时用的 PATH 通常非常精简可能只有/usr/bin:/bin。如果你的脚本里写死了/usr/local/bin/clean_old_logs.sh而脚本内部又直接调用了一个位于/usr/local/sbin的工具就会提示 command not found。解决方式是在脚本开头检查并设置关键命令的全路径或者把PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin这一行写到脚本或 crontab 环境变量的上方。我看过太多人排查半天最后发现只是mail命令的路径在 cron 环境里没有被找到。6. 写给想复制这条路的人工具链和思维习惯建议前面讲了很多具体的踩坑案例最后这个部分我想分享几个对提升 Shell 上手效率最有帮助的工具和习惯有些是我后来接触更专业的脚本时才知道的如果早点用前 7 天能少绕很多弯。6.1 三个值得提前装的工具第一个是 ShellCheck一个 Shell 脚本静态检查工具。你写完后执行一条命令shellcheck clean_old_logs.sh它会直接帮你指出代码里的潜在问题比如未加引号的变量、可能被空格影响的展开、循环里的误用等。我记得第一次对自己的旧脚本运行 ShellCheck看到输出里几十条 warning瞬间明白了之前为什么总在奇怪的位置出错。在很多发行版里一条命令就能安装yum install shellcheck或apt install shellcheck。在 CI 流水线里也可以加一道shellcheck检查能挡住不少低级错误。第二个是编辑器插件。如果你用 VSCode安装 ShellCheck 扩展和 shell-format 扩展后脚本的高亮、格式化、错误提示都有了和写 Python/Java 的体验差不多。很多人觉得 Shell 脚本“很原始”其实不是 Shell 本身的问题是缺了趁手的编辑器支持。第三个是bash -x调试模式。我刚写脚本时不知道有这种用法遇到问题只会往里塞一堆echo。执行bash -x clean_old_logs.sh /data/logs 7系统会把每一条命令在展开后的真实内容打印出来变量值是多少、有没有被正确赋值一眼就能看清。特别是排查变量为空、条件判断不符合预期的时候bash -x比任何教程都管用。6.2 建议培养的几个 Shell 习惯从我的经验看脚本写得好不好很多时候不是语法问题而是习惯问题。第一个习惯是“先试运行再动真格”。只要脚本里涉及删除、覆盖、远程执行尝试在逻辑里加入试运行模式哪怕只是一个DRY_RUN1的环境变量开关也能让你在业务高峰期多一层底气。第二个习惯是“命令路径能写全就写全”。虽然这样看起来啰嗦一点但在定时任务环境中可以避免因为 PATH 变化导致“明明手动执行没问题”的灵异事件。第三个习惯是“每条操作都留日志”。删除文件的脚本一定要输出谁在什么时间删了哪些文件否则将来排查问题寸步难行。第四个习惯是“刻意减少一个命令能完成的操作”。能用find -delete就绝不在管道里反复读写能用sort加uniq就不写多层循环Shell 的哲学是让工具替你干活而不是把工具拼成人肉 CPU。还有一个更具体但很实用的建议学会在给别人看脚本之前先把自己的变量名、注释写清楚。我早期脚本里全是a、b、x这类无意义变量三个月后自己都看不懂最崩溃的是明明代码报错但看不出是哪一段逻辑出了问题。后来我给自己定了一条规矩每个变量名都表达用途每个关键步骤都留一行注释尤其是当时觉得“这就没啥好解释的”的地方反而更要写清楚。真实世界的脚本往往不是写给别人看的而是写给你自己未来那个正在排障、脑子一团乱麻的时刻看的。这次从手动删日志到自动化运维的经历让我最大的改变不是学会了多少条命令而是处理问题时开始问一句这是一次性动作还是每天都要做的重复工作如果是后者就应该把它变成脚本。日志清理只是我工作上第一个成熟的自动化小项目后来我又把同样的思路用在了磁盘监控、服务自愈和批量备份上才慢慢觉得自动化运维离我并不远。你说 30% 有没有水分我觉得没有。毕竟自动化的收益从来不只省下那 30% 的操作时间更重要的是我做这些事的时候再也不用半夜从床上爬起来看磁盘告警了。