资讯详情

资讯详情

Shell脚本从入门到实战:自动化运维与日志处理

半夜十二点你对着群里弹出的告警消息发呆日志目录快满了几十个文件堆在那里要按日期归档、要压缩、还要把三十天前的旧数据清理掉。手动敲命令不是不行但下一次、下一个月、下一年呢重复操作永远在消耗你的时间。这时候你就需要Shell脚本编程——把平时在终端里敲的一条条命令写进一个文件里让它按顺序、按条件自动执行。别把它想得多高深它本质就是Linux命令的“组合拳”只是加上了变量、判断、循环这些编程要素。几乎每一个接触过Linux的人都会在某个时刻和它相遇。这篇内容不是从“什么是操作系统”讲起的教科书而是从我真实使用Shell脚本的经验出发把手边的场景、踩过的坑、总结出的习惯一起梳理出来。看完你应该能做到三件事看懂别人写的Shell脚本自己动手写一个能解决实际问题的脚本遇到排查问题时知道往哪个方向查。无论你是运维、后端开发还是偶尔要处理服务器文件的数据分析师这都值得花半小时读一遍。1. Shell脚本到底是什么为什么值得学1.1 从一次手工操作说起Shell脚本解决的问题先还原一个我印象很深的场景。当时一台业务服务器上的应用日志是每天滚动生成的文件名带日期但运维要求保留最近30天更早的要压缩归档。最开始我每天晚上用一条find命令加上一条tar命令来处理过程大概是这样先查目录里有哪些文件按日期分批打包再把旧的删掉。前几次还行时间一长就发现麻烦——命令参数太长容易输错漏掉某个子目录没人知道赶上哪个月份有31天还会算错。后来我花20分钟写了一个几十行的脚本效果立刻不一样。我把“查日志、打包、归档、清理旧文件”这个完整流程固定下来每天定时执行一次再也没有出过错。这就是Shell脚本最核心的价值把不稳定的手工步骤变成稳定、可复现、可交接的自动化流程。它解决的不是某个高难度算法问题而是“重复、易错、耗时”这三大日常痛点。1.2 我理解的Shell脚本命令解释器与胶水语言的双重角色要理解Shell脚本先得理解Shell本身。Shell是Linux系统里的一层命令解释器它的工作很简单你输入ls它帮你调用对应的程序再把结果返回给你。而Shell脚本就是把这些你本来会手动输入的命令预先把执行顺序和判断逻辑写到一个文本文件里交给Shell逐行解释执行。它同时扮演两个角色。第一个角色是“翻译官”把人和操作系统之间的沟通变得流畅第二个角色是“胶水”把各种外部程序粘在一起。比如你用grep过滤内容用awk做统计用tar做压缩再用rsync传输到远程机器一条命令干一件事脚本则负责把它们按正确的顺序串起来中间还能做条件判断和错误处理。很多项目部署流程本质上就是Shell和Python这类外部工具配合完成的自动化流水线。所以我不建议把Shell脚本看作一门传统编程语言。它的优势不是计算和复杂数据结构而是文件管理、进程控制、系统状态获取和批量化处理。知道这一点你就不会在一个需要解析上万条JSON数据的场景里硬用Shell而是选择合适工具的“胶水层”。1.3 适用人群与典型场景Shell脚本适合谁我个人的判断是只要你的工作会用到Linux终端就应该掌握基础的那一小桶技能。运维工程师自然不用说是主力后端开发在部署服务、排查容器日志时会用数据分析师哪怕只是批量解压几个.tar.gz文件掌握它也能省下不少时间。它不像编程那样需要系统学习数据结构但它的知识密度高学完立刻能回本。典型场景我列几个最常见的批量文件管理按规则重命名、移动、复制、压缩、清理过期文件。定时任务配合cron实现每天备份、日志切割、监控检查。系统状态巡检检查CPU、内存、磁盘占用率异常时发告警。软件安装与部署一条命令完成依赖安装、配置生成、服务启动。日志处理从海量日志中提取关键字、统计错误数量、生成报表。这些场景的共同特点是重复频率高、规则固定、对实时交互要求低。一旦写成脚本收益是持续的。2. 写脚本之前先把环境与选型搞清楚2.1 默认Shell的差异bash、sh、dash与zsh不少初学者拿到Linux第一件事就是写脚本却很少留意自己用的到底是哪个Shell。实际上不同发行版默认Shell不一定相同。绝大多数情况下你遇到的是bash但系统里可能还有sh、dash、zsh。它们之间并不是完全兼容的。简单说bash是功能最全面的支持数组、关联数组、一些高级扩展语法绝大多数网上流传的脚本教程都以bash为主。sh在不少系统上只是一个指向POSIX兼容Shell的软链接它强调的是标准兼容性但很多bash专属写法它不认。dash启动速度快、资源占用小常在系统初始化脚本里出现但同样不支持数组和一堆扩展语法我在一个项目里就吃过亏在bash里调试正常的脚本切到dash后报错后来才发现用了数组。zsh交互体验好写脚本的人相对少但很多macOS用户的默认Shell就是它。所以在脚本第一行写着#!/bin/bash不是摆样子而是明确告诉系统“用bash解释执行”。如果写成#!/bin/sh请确保只使用POSIX兼容语法。我自己的习惯是新写脚本一律用bash除非明确要求兼容sh否则不去给自己挖坑。2.2 脚本文件的基本结构与执行方式一个最小的Shell脚本只需要两行#!/bin/bash echo hello shell第一行#!/bin/bash叫shebang它告诉内核用什么解释器来运行这个文件。第二行是实际命令。保存为test.sh后执行方式有几种bash test.sh直接以bash解释器运行不要求文件有执行权限。chmod x test.sh然后./test.sh这是最常见的方式前提是文件有执行权限且shebang正确。source test.sh或. test.sh在当前Shell进程里执行而不是新开子Shell。这种方式常用于加载环境变量或函数定义。这里有个容易混淆的概念直接执行脚本时系统会创建一个子Shell去跑脚本里的cd不会改变你当前终端所在目录而source执行时脚本里的变量和目录切换会直接影响当前Shell。写脚本时如果想在脚本里切换目录无所谓如果希望脚本对当前会话生效就必须用source。2.3 权限与路径问题容易踩的第一个坑很多第一次跑脚本的人会遇到Permission denied。原因很简单文件没有执行权限。我见过有人立刻加chmod 777这不是好习惯。正确做法是只给所有者加执行权限chmod x your-script.sh比权限更容易被忽略的是CRLF换行符。在Windows上编辑的脚本换行符是\r\nLinux下执行会在每行末尾多一个\r经常报奇怪的“找不到命令”错误。我当时第一次遇到这个问题时反复检查语法都没查出来最后用file命令才看到问题。解决办法很简单sed -i s/\r$// your-script.sh或者用dos2unix命令处理。这类问题遇到一次就长记性了。另一个常见问题是路径。脚本里如果用了相对路径它依赖的是“执行脚本时所在的目录”而不是脚本文件所在的目录。这会导致你在A目录执行能成功在B目录执行却找不到文件。稳妥的做法是脚本一开头就获取自身所在目录再基于它构造其他路径BASE_DIR$(cd $(dirname ${BASH_SOURCE[0]}) pwd)掌握了这三件事你就能避开新手期的绝大部分障碍放心往下写。3. 核心语法细节拆解照着用就行3.1 变量、引号与命令替换Shell脚本里的变量和编程语言里的变量不完全一样。它本质上是字符串但根据使用场景又可以当作数字、路径、“是/否”的标记。最简单的定义和引用是这样namehello echo $name注意等号两边不能有空格否则Shell会把name当成一条命令。引用变量时我强烈建议加上双引号特别是变量可能包含空格或特殊字符的时候。要是写成echo $name一旦name的值是a b c它会被拆成三个参数在很多场景下就出问题了。如果变量里包含通配符不加引号还会被展开成文件名列表后果很微妙。命令替换是另一个高频操作它把一条命令的输出保存到变量里today$(date %Y%m%d)用$()比老式的反引号更清晰而且支持嵌套。命令替换的空白字符会被自动吃掉一部分所以如果输出有空格尤其是文件路径还是要加双引号$today。还有一个小技巧变量赋值时用readonly声明只读变量防止后面误修改readonly LOG_DIR/var/log/myapp脚本规模一大这种保护很有价值。3.2 条件判断与循环条件判断是脚本“聪明”起来的关键。基本用法是if []、elif []、else和fi结尾。方括号左右都有空格这是Shell语法里最容易忽略的细节。if [ $count -gt 10 ]; then echo too many else echo ok fi这里$count是数字比较用的是-gt字符串比较用文件判断用-f、-d、-x等。新手经常把直接写进去结果被当成重定向。如果要写大于号要么用-gt要么在[[]]里使用。循环有两个常用形态。for循环适合遍历文件列表、数字范围for file in /var/log/myapp/*.log; do echo process $file donewhile循环适合按行读取文件while IFS read -r line; do echo $line done input.txtIFS和-r是两处容易踩坑的细节不设置IFS行首尾空格会被吃掉不加-r反斜杠会有特殊含义。很多批处理脚本处理日志时在这上面丢过数据。条件判断里还能组合多个条件用-a与和-o或但我更推荐用、||或[[]]可读性更好。3.3 函数与参数处理脚本写到一定程度逻辑会变复杂这时就该用函数把重复代码收拢起来。定义和调用很简单function log_info() { echo $(date %Y-%m-%d %H:%M:%S) INFO $1 } log_info start jobShell函数的参数用$1、$2这样的位置变量接收没有明确的return返回值最常见的做法是用echo输出结果调用方用$(...)捕获。函数里也可以访问全局变量但注意函数内部定义的变量默认也是全局的除非用local声明function calc() { local result0 ... }相对脚本整体的参数处理$0是脚本名$1到$9是参数$#是参数个数$是所有参数列表。配合shift可以逐个取出剩余参数。在写支持命令行选项的脚本时getopts是标准方案能解析-f xxx、-v这类参数虽然看起来很老派但很可靠。3.4 常用的文本处理三兄弟grep、sed、awkShell脚本里真正干“重活”的往往是文本处理命令。grep负责筛选sed负责按行替换和编辑awk负责按列提取和统计。三兄弟掌握了大部分日志和配置文件处理都不在话下。grep最基本的是按关键字过滤grep ERROR app.log要忽略大小写加-i要输出行号加-n要查找递归目录加-r。sed最常用的是替换操作sed -i s/old/new/g file.txt注意-i是原地修改不加它只是输出结果不写入文件。如果要把所有数字替换成空字符可以写sed -i s/[0-9]//g。在处理配置文件模板时我经常用sed把占位符替换成真实值。awk可以按列处理。默认按空白字符分割列用$1、$2引用awk {print $1, $NF} access.logNF表示当前行有多少列$NF就是最后一列。统计某列出现次数用awk {count[$1]} END {for(k in count) print k, count[k]} access.log这段代码读起来可能有点吓人但你可以把它当成一条固定套路记住随手就能解决不少统计需求。文本处理三兄弟的准确用法需要积累但一开始知道“筛选用grep、替换用sed、统计用awk”就够了。4. 一个完整实战批量日志归档脚本的诞生4.1 需求拆解与设计思路讲完基础用一个完整例子把技术点串起来。假设场景某服务器上有一个应用日志目录/opt/myapp/logs里面有多个子目录每个目录里都有一堆.log文件。我需要每天执行一次把所有超过1天的.log文件按当天日期打包压缩。打包文件放到归档目录/data/backup/logs/日期下。压缩完成后删除被归档的原始日志文件。保留15天内的归档数据更早的归档包清理掉。先别急着写命令。先把需求拆成几个模块找文件、打包、移动、清理。每个模块单独思考再组合。同时要考虑错误处理如果某个子目录没有新文件需要归档不能报错退出如果打包失败绝不能删除原始文件。这个设计原则很关键自动化脚本一旦误删数据比不执行还可怕。4.2 关键代码逐段讲解脚本我命名为archive_logs.sh核心骨架如下#!/bin/bash set -euo pipefail LOG_ROOT/opt/myapp/logs BACKUP_ROOT/data/backup/logs TODAY$(date %Y%m%d) KEEP_DAYS15 LOG_FILE/var/log/archive_logs.log log_message() { echo $(date %Y-%m-%d %H:%M:%S) $1 $LOG_FILE } mkdir -p $BACKUP_ROOT/$TODAY log_message start archive process, today$TODAY find $LOG_ROOT -type f -name *.log -mtime 1 -print0 | while IFS read -r -d file; do echo $file done /tmp/archive_file_list.txt if [ ! -s /tmp/archive_file_list.txt ]; then log_message no files to archive, exit 0 exit 0 fi while IFS read -r file; do base_dir$(dirname $file) base_name$(basename $file) archive_name$BACKUP_ROOT/$TODAY/${base_dir##*/}_${base_name}.tar.gz tar -czf $archive_name -C $base_dir $base_name if [ $? -eq 0 ]; then rm -f $file log_message archived and removed: $file - $archive_name else log_message failed to archive: $file fi done /tmp/archive_file_list.txt find $BACKUP_ROOT -type f -name *.tar.gz -mtime $KEEP_DAYS -delete log_message finished archive cleanup这段代码里set -euo pipefail放在开头是我推荐的习惯。-e表示遇到错误就退出-u表示使用未定义变量时报错pipefail表示管道中任何命令失败都会使整个管道失败。一开始你可能觉得它太严格但实际写自动化脚本时丑话说在前头比静默出错好得多。查找文件用了find加-print0这是为了处理文件名里有空格或换行的极端情况。配合while read -d 可以逐行安全读取。这里再把文件列表落盘是为了两步操作解耦先明确准备归档哪些文件再逐个处理。打包时我用了-C $base_dir $base_name先切换到文件所在目录再打包具体文件名。这样生成的压缩包里不会带着一长串绝对路径解压时也更干净。重点是“先打包成功再删除原文件”。这里用$?判断上一条tar命令的返回值只有返回0才删原文件。这个顺序在实践中救过我很多次之前有一次磁盘空间快满了tar中途失败但因为代码里先删后打包直接丢了一批日志。后来我强制要求自己所有涉及“移动/归档”的逻辑必须先确认新位置数据完整再动原文件。4.3 执行结果与验证方法把脚本保存后第一步不是直接扔给cron而是先手动跑一遍同时观察输出bash -x ./archive_logs.sh-x会打印脚本执行过程中的每一条实际命令对排查路径、变量值、逻辑分支非常有帮助。跑完之后我习惯按几个维度验证归档目录下是否出现日期目录打包文件名是否符合预期。打开一个压缩包确认里面内容完整且没有多余层级。原日志目录里符合条件的文件是否被删掉不符合条件的是否还在。用find检查归档目录里的老文件是否被清理。在测试阶段我会把KEEP_DAYS临时改成1天用几条历史文件快速验证清理逻辑确认没问题后再改回15天。这比直接等15天可靠得多。4.4 安全增强与异常处理运行环境不会总是那么理想。磁盘可能满掉目录权限可能不对源日志可能正在被其他进程写入。这些情况脚本都得有所准备。我后来给这个脚本加了几个增强disk_usage$(df /data | awk NR2 {print $5} | tr -d %) if [ $disk_usage -gt 90 ]; then log_message disk usage high, stop archive to avoid overflow exit 1 fi当目标磁盘使用率超过90%时暂停归档避免压缩包写到一半把磁盘塞满。这个判断我在另一个生产环境里确实触发过当时日志增长异常多亏提前停住否则后果挺麻烦。另外tar命令本身可以加入--exclude排除特定文件模式比如某些目录里的临时文件不需要打包。在文件查找阶段可以加更精确的-not -path条件让归档范围更可控。整个脚本执行完后我会从系统日志里检查LOG_FILE是否记录了正常结束标记确保不是静默挂掉。这样一个可调度、可观测、带基础防护的日志归档脚本就落地了。放入crontab之后每天自动运行几乎不用人管。5. 调试、排查与避坑指南5.1 脚本调试三板斧bash -x、set -e、管道行为写脚本时调试占了相当大一部分时间。最有用的工具是人类一问一答找逻辑但命令层面的几招也能大幅提速。第一招就是bash -x script.sh让脚本输出每条命令展开后的结果。这是最直观的。变量没有展开路径不对条件分支走错了一眼就能看出来。如果脚本太长还可以在局部用set -x和set x只调试某一段这样输出不至于刷屏。第二招是在脚本开头加set -e。虽然很多人对它又爱又恨但日常脚本我仍然建议打开。set -e遇到非零退出码就中断执行能避免程序在错误状态下继续跑把一个局部失败扩大成整体问题。当然它也有副作用某些命令的“失败”其实不是错误比如grep没匹配到内容会返回1这时set -e会让脚本中断。解决办法是在明确允许失败的命令后面补一段|| true或者把它放在if条件里。第三招是关于管道的。管道命令的退出码默认只看最后一个命令set -e对管道前面的命令失效。比如cat missing.txt | grep x即使cat失败了只要grep因为没输入而返回非零最终状态才可能是失败如果grep返回0整个管道就被当成成功。把set -euo pipefail一起用管道中任何一个环节失败都会暴露。这套组合是我现在写新脚本的默认配置。5.2 我踩过的5个Shell脚本坑这些坑不是我一个人踩过但每次我都在群里看到有人再问。整理出来希望能帮你直接避掉。第一坑忘记给变量加引号。路径含空格变量展开后就被拆成多个参数。轻则命令报错重则误删文件。所以我习惯了所有变量引用都带双引号尤其是$file、$1这种位置参数。第二坑find和文件名中的换行。如果只用find -exec rm {}文件名里的空格还行但换行和特殊字符会出问题。更安全的是find ... -print0加xargs -0或while read -d 。多写两个字符省掉无数麻烦。第三坑脚本里用cd不检查结果。如果cd到一个不存在的目录脚本还继续往下走后续所有相对路径操作就都作用在了错误目录上。稳妥做法是cd /path || exit 1否则宁可用BASE_DIR构造完整路径。第四坑crontab里的环境变量差异。在终端里能跑的脚本放进cron后可能找不到命令因为cron环境比交互Shell干净得多。解决方式在脚本里写全命令路径或者手动导入PATH比如export PATH$PATH:/usr/local/bin。这个坑我当年排查了很久最后发现是PATH没带上。第五坑忘了rm -f和tar之间的失败关联。前面说过压缩失败不能删原文件。不要用tar ... rm这种简单串联因为中间任何信号中断都可能让条件判断失真而是要用显式的返回值判断。5.3 性能考量什么时候不该用ShellShell脚本不是万能钥匙。它的主要成本在于每次执行命令都会启动子进程循环处理成千上万个文件时会明显变慢。假设你有一个包含五十万行日志的文本需要做复杂统计awk往往还能接受但如果要逐行执行外部命令就非常吃力。什么时候该换工具我的判断标准是复杂字符串处理尤其涉及转义、嵌套优先用Python。大量并发任务优先用Python或专门的调度工具。需要精确的错误堆栈和单元测试Shell写起来太脆弱。数据结构超过“二维文本表”比如JSON嵌套优先写Python或直接用jq。不过这不意味着Shell的地位低。在“快速处理一批文件、按顺序执行一串命令”这个领域它依然是最短路径。关键是别硬扛当逻辑复杂度超过脚本可维护的临界点及时换语言是明智的。6. 把脚本变成“工程”模块化、命名与注释6.1 函数划分与脚本骨架当脚本超过几十行我建议把它组织成几个功能区域变量定义区、函数区、主逻辑区。这样别人拿到代码时第一眼就能知道哪里该改、哪里不要动。我常用的骨架是这样#!/bin/bash set -euo pipefail # 配置区 LOG_DIR... BACKUP_DIR... KEEP_DAYS... # 函数区 log() { ... } check_env() { ... } archive_one_file() { ... } cleanup_old_files() { ... } # 主逻辑区 check_env ...函数命名尽量体现动作check_env、archive_one_file一目了然。每个函数只做一件事如果函数里出现三个以上的局部阶段就该考虑继续拆分。这个原则和写其他语言是一样的。6.2 命名规范、日志输出与退出码命名规范让人少做无用功。变量用全大写表示只读配置函数名用蛇形命名临时文件统一放到/tmp并加$$进程号避免冲突。脚本里每一个rm、mv这种破坏性操作之前我都留有明确日志。日志输出对排查问题太重要了。我习惯把日志写到固定文件同时带时间和级别关键词log_info() { echo [INFO] $(date %F %T) $* $LOG_FILE; } log_error() { echo [ERROR] $(date %F %T) $* $LOG_FILE; }脚本退出码也要刻意设计。0表示成功非0表示失败不同失败原因可以用不同码区分比如1是环境问题2是逻辑错误3是资源不足。定时任务调度工具对退出码非常敏感设计好了监控报警才准确。6.3 如何管理自己的脚本库脚本会越写越多不好好管理就会变成一锅粥。我现在的做法是把所有常用脚本放在一个统一目录比如~/bin并同步到版本管理里。每个脚本都带同样的头部注释作用、用法、参数、依赖命令、更新日期。#!/bin/bash # 功能: 日志归档 # 用法: ./archive_logs.sh [--dry-run] # 依赖: tar, find, date # 最后修改: 2025-01-01另外写脚本时我会尽量保持“幂等”。所谓幂等就是同一个脚本重复执行多次结果不会叠加出问题。比如归档脚本如果第一次执行到一半失败第二次重新跑不会把已经归档过的文件再打包成重复内容。具体做法是打包前先检查目标压缩包是否已存在存在就跳过或覆盖前先警告。这个习惯在定时任务里极其重要因为任务可能被手动重跑。最后再分享一点个人的体会我从最早只会写三行的echo脚本到后来接手过上千行的部署脚本中间最大的转折点是意识到Shell脚本不只是命令的堆叠它的价值在于“把一个人的操作经验固化成一整套可复用的流程”。你每把一个手工步骤变成脚本就多释放一份精力和注意力去处理真正需要人判断的事情。如果你刚入门我的建议也很简单别追求一开始就写特别复杂的脚本先把手边最重复、最烦人的那个操作自动化哪怕只有五行。写完以后加个日志放一句注释让它跑起来。等你用上一周你会自然地想给它加参数、加判断、加异常处理。那时候你就不是在“学Shell”而是在用它解决自己的问题了。希望这篇内容能让你少走一些弯路也欢迎你踩过哪些Shell脚本的坑在评论区一起聊聊。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →