
周一早上我习惯先做三件事登录服务器看日志备份昨天的数据清理临时文件。前两周我还觉得挺充实到第三周就有点受不了了——明明是重复到闭着眼睛都能敲完的命令为什么每天还要花掉将近一个小时然后我开始认真学 shell 编程。学完之后回头看我最大的感受是shell 编程并不是一门多高深的语言它真正的价值是把那些你每天都在重复敲的命令沉淀成一套可以反复执行、可以检查、可以交接的流程。一个运维人员会不会写 shell 脚本差别不是“会不会编程”而是“有没有把手工操作变成自动化工具的意识”。这篇文章不打算做语法大全也不会堆几十个冷门命令。我会从一个实际使用者的角度把 shell 编程从认知、最小流程、自动化实战到稳定性排查讲清楚。看完之后你至少能写出第一个能用的脚本并知道怎么让它长期稳定地跑下去。1. 先想清楚一件事Shell 脚本真正解决的是重复劳动1.1 为什么很多新手学会了命令却写不出脚本很多人学过 Linux 常用命令ls、cd、cp、grep、awk都会用但一到“写脚本”就卡住了。原因是他们把命令理解成了“独立的动作”而不是“流程中的一个环节”。ls是看一眼目录里有什么grep是从一堆文本里筛出内容tar是打包文件。单独用都没问题但真实运维工作中几乎没有哪件事是单条命令能完成的。你要登录服务器、查看状态、判断结果、执行操作、记录日志这是一个多步骤流程。而 shell 脚本的本质就是把这些步骤按顺序写下来让机器替你按顺序执行。新手写不出脚本通常不是语法不懂而是缺少一个意识**把一次手工操作翻译成一条按顺序执行的命令流。**比如你每天手动做的事是“进入备份目录、把日志文件夹打包、删除三天前的压缩包”这就是三段步骤翻译成脚本就是cd /data/backup || exit 1 tar -czf logs_$(date %F).tar.gz /var/log/myapp/ find /data/backup -name *.tar.gz -mtime 3 -delete不需要任何复杂的编程技巧只要你会敲这三条命令你就能写出脚本。区别只在于你愿不愿意把它们写进一个文件里让电脑每天替你重复。1.2 一个判断标准你在“敲命令”还是在“造工具”我见过两种运维风格的同事。一种人命令行敲得很熟但每次做同一件事都要重新敲一遍一旦中间被打断可能还会忘记进行到哪一步另一种人写脚本时可能没那么快但会把所有重复操作沉淀成脚本下次执行只需一条命令。判断标准很简单同一件事你做了第二次之后还打算做第三次吗如果答案是“会”那这件事就值得写成一个脚本。哪怕脚本只有三行它也是在帮你做时间积累。更关键的是脚本可以被检查、被修改、被别人接手。三个月后你再处理同一类问题不用回忆当时敲了什么直接看脚本就好。手工命令是记在脑子里的脚本是写在文件里的后者在团队协作和故障复盘里会可靠得多。但也要说清楚边界并不是所有场景都适合写脚本。一次性操作、临时排查、交互性很强的调试直接敲命令往往更快。我的建议是先手敲一遍确认流程能走通再决定要不要固化成脚本。既不要一上来就写脚本也不要永远都在手敲。2. 从小白到能干活最小可用的脚本怎么搭2.1 环境准备与最小脚本先让一件小事自动化在开始之前先确认你的环境里有哪些可用的 shell。Linux 发行版大多自带 BashmacOS 也自带 Bash 或 Zsh。Windows 用户如果要在本机练习常见做法是装 WSL 或使用虚拟机但如果你是想学运维更推荐直接在 Linux 环境里练。打开终端执行echo $SHELL bash --version如果能看到bash相关输出说明环境已经可以用了。新建一个脚本文件比如first.sh#!/usr/bin/env bash echo 当前时间是$(date) echo 当前用户是$(whoami) echo 当前目录是$(pwd)第一行#!/usr/bin/env bash叫 shebang作用是告诉系统用哪个解释器来运行这个脚本。用env bash而不是直接写死/bin/bash可以在不同系统间获得更好的兼容性。然后给脚本执行权限并运行chmod x first.sh ./first.sh这里最容易踩的第一个坑是执行方式。./first.sh表示用当前目录下的脚本文件脚本需要可执行权限。如果你直接写first.sh系统会去PATH环境变量里找大概率找不到。所以要么用./first.sh要么用bash first.sh显式指定解释器。2.2 变量、参数和判断让脚本开始“长脑子”一个只按顺序执行的脚本本质上还是命令的堆叠真正让它变得灵活的是变量、参数和判断。变量在 shell 里的写法很简单BACKUP_DIR/data/backup KEEP_DAYS7注意两个细节等号两边不能有空格引用变量时要加$写成$BACKUP_DIR或者更严谨的${BACKUP_DIR}。位置参数是脚本外部传给脚本的值。$1是第一个参数$2是第二个$0是脚本自身路径。比如写一个重命名文件的脚本#!/usr/bin/env bash # rename.sh 用法./rename.sh 旧名字 新名字 mv $1 $2这里不加引号会出问题。如果文件名带空格比如my report.txtmv $1 $2会被解释成多个参数结果完全是意料之外。所以凡是涉及路径和文件名的变量建议一律加双引号。判断语句是让脚本“有脑子”的关键。比如备份之前先确认目标目录存在不存在就创建BACKUP_DIR/data/backup if [ ! -d $BACKUP_DIR ]; then mkdir -p $BACKUP_DIR echo 目录不存在已自动创建$BACKUP_DIR fi新手在if上最常见的报错是[和变量之间没有留空格。if [ -d $dir ]中[是一个命令后面必须有空格$dir和]之间也要有空格。少了空格shell 会直接报语法错误。2.3 循环和函数从单条任务走向批量任务循环解决的是“一批东西”的问题。比如要把当前目录下所有.log文件重命名加上日期后缀for f in *.log; do mv $f ${f%.log}_$(date %F).log done${f%.log}是 shell 的字符串截取语法表示从变量f的末尾去掉.log。这类语法记不住没关系用到时查一下就行但要知道有这种能力。函数的作用是复用。如果脚本里有一大段逻辑需要在多处出现就可以封装成函数log() { echo [$(date %F %T)] $1 } log 开始执行备份调用函数时可以传参函数内部用$1、$2接收。这里的$1是函数的第一个参数和脚本的位置参数是两套东西。到这一步你已经具备写“能用脚本”的能力了顺序执行、变量、判断、循环、函数。剩下的问题只有一个拿它去解决什么实际问题。3. 自动化运维实战用三个场景把脚本派上用场与其背语法不如直接看三个最常见的运维场景。这三个场景都不复杂但覆盖了变量、判断、循环、定时、日志处理这些核心能力。1.1 场景一自动备份目录并只保留最近 7 天备份备份是运维里最基础也最不能出错的场景。手工备份通常包含三个动作打包、按日期命名、清理过期备份。#!/usr/bin/env bash SOURCE_DIR/var/www/html BACKUP_DIR/data/backup KEEP_DAYS7 TODAY$(date %F) if [ ! -d $BACKUP_DIR ]; then mkdir -p $BACKUP_DIR fi tar -czf ${BACKUP_DIR}/html_${TODAY}.tar.gz -C /var/www html find $BACKUP_DIR -name html_*.tar.gz -mtime $KEEP_DAYS -delete echo 备份完成${BACKUP_DIR}/html_${TODAY}.tar.gz这段脚本里有几个值得说的点。-C /var/www html的意思是先切到/var/www目录再打包html文件夹。这样压缩包里的路径是相对路径解压出来不会把文件散到奇怪的目录里恢复时也更容易控制。find ... -mtime 7 -delete是“保留最近 7 天”最常见的实现方式。-mtime 7匹配修改时间超过 7 天的文件-delete直接删除。实际使用前建议先不加-delete跑一次看看匹配到的文件对不对确认无误再加删除。最后是定时执行。用crontab -e编辑定时任务每天凌晨 2 点执行0 2 * * * /bin/bash /opt/scripts/backup_html.sh /var/log/backup.log 21这里一定要解释一下 /var/log/backup.log 21脚本运行时的正常输出和错误输出都会被追加到日志文件里方便事后排查。如果漏掉这一步脚本出错时你根本不知道发生了什么。2.2 场景二日志巡检从海量日志里筛出异常日志排查是最常见的运维工作但很多人停在“手动 grep”。做得稍微好一点是写一个脚本把异常数量统计出来生成一份可读的巡检报告。假设应用日志在/var/log/myapp/app.log每一行是标准日志格式。你想统计最近 1 小时内出现了多少条 ERROR 和 Exception并按小时汇总#!/usr/bin/env bash LOG_FILE/var/log/myapp/app.log REPORT/tmp/app_report_$(date %F).txt echo 应用日志巡检报告 - $(date %F %T) $REPORT echo $REPORT grep $(date -d 1 hour ago %F %T) $LOG_FILE | grep -c ERROR $REPORT echo -------- ERROR 关键字明细 -------- $REPORT grep ERROR $LOG_FILE | tail -20 $REPORT echo -------- Exception 出现次数 -------- $REPORT grep -c Exception $LOG_FILE $REPORTgrep -c统计匹配行数tail -20只取最后 20 条避免报告文件太大。实际生产里日志的格式千变万化你可能需要调整关键字、时间窗口和汇总方式但思路是一样的把排查动作固化成一个脚本每次直接看报告而不是临时想命令。这里要提醒一个常见误区不是所有 ERROR 都值得告警也不是所有 Exception 都需要立即处理。脚本只负责“把可疑内容摘出来”判断要不要处理仍然需要人来看。1.3 场景三批量环境检查为部署前加一道保险部署前检查磁盘、内存、端口、服务状态通常被叫作“环境巡检”。手工做一遍费时费力而且容易漏项。写成脚本的好处是每次部署前都跑一遍检查项不会因为人疲劳而减少。#!/usr/bin/env bash echo 磁盘使用情况 df -h | grep -E Filesystem|/$ echo 内存使用情况 free -m echo 关键端口监听 ss -tlnp | grep -E :80|:443|:3306 echo Nginx 进程状态 if pgrep -x nginx /dev/null; then echo Nginx 运行中 else echo Nginx 未运行需要检查 fi这种脚本不复杂但价值很高。它把“人凭经验检查”变成了“脚本按清单检查”。清单可以不断积累今天发现磁盘空间不够明天就可以把df -h加进去今天发现端口被占用就可以加端口检查项。脚本会随着你们的运维经验一起成长。下表是常见检查项的一个示例你可以根据自己的环境扩展检查维度常用命令判断标准磁盘空间df -h根分区使用率建议低于 80%内存free -m观察 available 是否充足端口监听ss -tlnp确认业务端口已监听关键进程pgrep -f 进程名进程存在服务状态systemctl status 服务名active (running)4. 脚本能跑不代表能稳定长期跑稳定性与排查才是分水岭写一个能跑一次的脚本很简单难的是让它每天都能稳定跑出错时还能知道错在哪。从“能跑”到“稳定跑”中间隔着一堆看起来不起眼但实际决定成败的细节。4.1 新手最容易忽略的 5 类坑**第一类坑路径假设。**脚本里写死相对路径或者假设执行时一定在某个目录下。比如你在项目目录里写了./backup/但 crontab 执行时的工作目录可能完全不一样。解决办法是脚本开头先cd到固定目录或者所有路径都用绝对路径。**第二类坑输入数据带空格。**文件名、目录名、参数值只要含空格没加引号的变量就会被拆分。规律很简单凡是引用变量默认都加双引号。第三类坑set -e的误用。set -e表示脚本中任何一条命令返回非零值时立即退出。听起来很安全但实际很容易误伤。比如grep没匹配到内容会返回非零脚本可能直接退出后面的处理逻辑全都不执行。我自己的习惯是不全局开启set -e而是在关键步骤后面显式检查返回值或者在明确允许失败的命令后面加|| true。# 允许 grep 没有匹配到内容时继续执行 grep 关键字 $LOG_FILE || true**第四类坑环境变量不继承。**crontab 执行时的 PATH 环境变量和手写终端往往不一样。有时脚本里用了一个命令手敲没问题定时任务却报“command not found”就是 PATH 不完整。解决方法是脚本开头显式设置 PATH或者直接用命令的绝对路径。**第五类坑并发和幂等。**如果脚本被重复执行会发生什么备份脚本重复执行会生成同名文件吗日志分析脚本重复执行会对结果有影响吗这些都要考虑。脚本要尽量做到即使重复执行也不会产生脏数据或重复副作用。4.2 脚本出错不要停正确处理错误接口化思考很多时候一个运维脚本要处理多个子任务。如果第一个任务出错就完全停止后面的任务也跟着瘫痪。更合理的思路是区分“必须成功才能继续”和“失败了也可以跳过”两类步骤。“必须成功”的步骤比如备份文件时源目录不存在这时候应该停止并报错if [ ! -d $SOURCE_DIR ]; then echo 源目录不存在停止执行$SOURCE_DIR 2 exit 1 fi“失败了也可以跳过”的步骤比如清理过期备份时find报了一点小错不至于影响备份本身。这时可以用|| true忽略或者把失败信息写进日志但不中断流程。另一个容易忽略的是临时文件清理。脚本执行到一半被中断临时文件可能残留下次执行就可能踩坑。可以用trap在脚本退出时自动清理TMP_FILE/tmp/myapp_scan_$$.txt trap rm -f $TMP_FILE EXIT$$是当前进程的 PID用它拼临时文件名可以避免多个实例同时运行时互相覆盖。4.3 排查链路从现象分层定位脚本出问题不要一上来就怀疑工具本身更不要盲目改参数。我建议按照下面的顺序排查先看现象是报错、卡住、无输出还是输出结果不对现象决定后续的方向。再看输入脚本里的文件路径、参数、变量值是否真的符合预期可以先在脚本开头加echo打印关键变量确认运行时拿到的是什么。再看语法和逻辑执行bash -x script.shShell 会把每一步执行的命令打印出来这是最常用的调试方式。再看环境PATH、当前目录、权限、依赖命令是否存在是 crontab 场景里最容易出问题的地方。再看参数和边界set -e是不是误杀了流程文件名里的空格是不是把参数拆了find的-mtime 7边界算得对不对最后看工具边界这个命令本身在这个环境中是否有版本差异比如同一个tar参数在不同发行版上表现不完全一样。这个链路看起来像常识但很多问题就是因为在第二步没排查清楚直接跳到第五步改参数最后绕了一圈又回来。从“会写命令”到“会沉淀流程”回到开头那个场景。我后来把登录服务器、看日志、备份、清理临时文件这些事全部写成了脚本每天早上只需要跑一条总控命令然后花几分钟看输出报告。省下来的时间没有让我变懒反而让我有余力去做更有价值的事优化备份策略、梳理日志告警、规范化部署流程。学 shell 编程真正给我带来的不是会敲更多命令而是多了一种思维方式**任何重复劳动都可以先尝试把它变成可复用流程。**哪怕第一次写出的脚本很粗糙哪怕一开始只是把三条命令按顺序写进文件它也比“每天手工敲一遍”更接近自动化。如果你现在刚起步最该做的事不是去看语法大全而是找出你工作中最烦的那件重复事把它写成第一个脚本。三行也行五页也行。先跑通再优化最后让它稳定。坚持几次之后你自然会理解 shell 编程里那些参数、管道和工具链设计的真正价值。