7天掌握Shell脚本:日志清理自动化实战与踩坑指南
2026/9/8 15:22:23 网站建设 项目流程

如果你也是一名运维开发,或者只是偶尔管几台 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 的基本语法、findcron的用法、脚本调试方式,这些能力后来直接迁移到了更多自动化运维场景里。换句话说,我这个入门不是从语法书开始的,而是从真实痛点反推回去的,学到的每一样东西都能立刻派上用场。

2. 7 天时间线拆解:我不是按语法书硬啃 Shell

我见过很多人的 Shell 学习路径是这样的:先买一本六百页的《Shell 脚本编程》,从第一章“Shell 是什么”开始看,看到变量和运算符还能坚持,到sedawk就逐渐放弃,最后书签停留在第三章。我不打算这么学。我的方法很简单,就是把“写一个日志清理脚本”当成一个完整的项目去倒推自己需要什么,然后只学够用的命令,边做边补。

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-01app.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_RUN=1 时只模拟执行,不真正删除(默认开启) # DRY_RUN=0 时执行实际删除 # 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_DELETED=0 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 +10080(10080 分钟等于 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 ((i=1; i<=10; 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 2>&1

这里必须注意几个细节。第一,脚本路径和日志输出路径都要写绝对路径。crontab 执行时的环境和你手动登录 shell 时的环境不同,如果在脚本里依赖相对路径,十有八九会执行失败。第二,>> ... 2>&1是标准输出和错误输出都追加到同一个日志文件,否则脚本出错时你收不到任何反馈,排查起来非常麻烦。第三,脚本文件本身要有执行权限,需要先执行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:-ops@example.com}" USAGE=$(df -P "${WATCH_DIR}" | awk 'NR==2 {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提取;NR==2表示取第二行,也就是目标文件系统的统计行;tr -d '%'的作用是把百分号删掉,方便在if里做数值比较。

我把它加进 crontab:每 5 分钟检查一次磁盘。这样每次日志清理脚本执行之前,系统已经为“磁盘管理”建立了预警机制,磁盘就算突然暴涨,我也能提前收到邮件,而不是等用户反馈服务写不进日志才反应过。

4.2 服务挂了自动拉起,比告警更省心

日志和磁盘之外,另一个高频手动操作是重启服务。有一段时间业务服务偶尔会因内存溢出退出,我总在半夜接到“页面打不开”的通知,然后登录服务器敲systemctl restartsh start.sh。重复几次后很自然地想:能不能让 Shell 在发现服务不在线时自动重启它?

存活检测可以分两种思路:一种是看进程在不在,一种是主动探测一个健康接口。对 Java 服务这种单进程场景,第一种思路最简单直接。

#!/usr/bin/env bash PROCESS_NAME="java" START_CMD="/opt/app/start.sh" LOG_FILE="/var/log/app_auto_restart.log" if ! pgrep -f "${PROCESS_NAME}" > /dev/null 2>&1; then echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] 服务进程不存在,尝试拉起" >> "${LOG_FILE}" nohup "${START_CMD}" > /dev/null 2>&1 & 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_AGO=7 # 计算 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,输出里会标注charset=utf-8charset=us-ascii等信息。如果文本里混入中文乱码,可以先确认系统当前locale,再确认编辑器默认保存编码。有一个经验法则:生产环境服务器上的脚本和配置文件,注释尽量用英文,或者确保团队统一用 UTF-8 编辑,否则容易因为不同开发环境之间的编码差异导致莫名其妙的解析错误。

5.2 for 循环按“空格”拆词:文件里带空格怎么办

for f in ${FILES}看起来没问题,实际上它遵循了 Shell 的默认分词规则:把变量内容按空格、制表符和换行符拆成多个字段。目录名或文件名一旦包含空格,就会出问题。比如/data/logs/user report 2025.log会被拆成/data/logs/userreport2025.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 crondservice cron status,如果服务没启动,后面全是白搭。第二,确认脚本有执行权限:chmod +x /path/to/script.sh,同时确认脚本第一行的路径正确且对应的解释器存在。第三,最好把 crontab 条目里的输出重定向到日志文件,比如>> /var/log/clean_old_logs/clean.log 2>&1,这样即使脚本报错也至少能看到一份错误输出。第四,检查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 shellcheckapt 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_RUN=1的环境变量开关,也能让你在业务高峰期多一层底气。第二个习惯是“命令路径能写全就写全”。虽然这样看起来啰嗦一点,但在定时任务环境中可以避免因为 PATH 变化导致“明明手动执行没问题”的灵异事件。第三个习惯是“每条操作都留日志”。删除文件的脚本,一定要输出谁在什么时间删了哪些文件,否则将来排查问题寸步难行。第四个习惯是“刻意减少一个命令能完成的操作”。能用find -delete就绝不在管道里反复读写,能用sortuniq就不写多层循环,Shell 的哲学是让工具替你干活,而不是把工具拼成人肉 CPU。

还有一个更具体但很实用的建议:学会在给别人看脚本之前先把自己的变量名、注释写清楚。我早期脚本里全是abx这类无意义变量,三个月后自己都看不懂,最崩溃的是明明代码报错但看不出是哪一段逻辑出了问题。后来我给自己定了一条规矩:每个变量名都表达用途,每个关键步骤都留一行注释,尤其是当时觉得“这就没啥好解释的”的地方,反而更要写清楚。真实世界的脚本,往往不是写给别人看的,而是写给你自己未来那个正在排障、脑子一团乱麻的时刻看的。

这次从手动删日志到自动化运维的经历,让我最大的改变不是学会了多少条命令,而是处理问题时开始问一句:这是一次性动作,还是每天都要做的重复工作?如果是后者,就应该把它变成脚本。日志清理只是我工作上第一个成熟的自动化小项目,后来我又把同样的思路用在了磁盘监控、服务自愈和批量备份上,才慢慢觉得自动化运维离我并不远。你说 30% 有没有水分?我觉得没有。毕竟自动化的收益从来不只省下那 30% 的操作时间,更重要的是我做这些事的时候,再也不用半夜从床上爬起来看磁盘告警了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询