ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Shell脚本编程实战:从核心语法到日志清理与自动化运维

Shell脚本编程实战:从核心语法到日志清理与自动化运维 Shell脚本编程这个概念很多刚入行的朋友一听就觉得“老”“现在不都Python了吗还有必要学Shell”但你在真实服务器上跑一次任务就会发现凡是跟Linux/Unix打交道的地方Shell就是绕不过去的那一层。我见过有人用Python写几百行实现批量文件处理最后被同事一个十几行的Shell脚本轻松搞定效率还更高。这篇文章我不整那些虚的直接用做实操的口吻把Shell脚本编程从底层概念、核心语法、真实案例到排查技巧整个过一遍。前端、后端、运维、测试哪怕你只是偶尔碰一下服务器看完都能立刻上手写自己的第一个脚本。1. 先搞清楚Shell脚本到底是“谁”它为什么无处不在1.1 从“跟操作系统说话”聊起Shell的本质是命令解释器很多人对Shell有两个极端误解一种觉得它特别神秘是某种底层语言另一种觉得它就是“命令行里敲命令”根本谈不上编程。其实Shell就是一个命令解释器它是用户和操作系统内核之间的那层“翻译官”。你在终端里敲的每条命令都是先交给Shell由它解析、展开、调用系统接口再把结果显示给你。Linux系统里常见的Shell有bash、sh、zsh、fish等。其中bashBourne Again Shell是绝大多数发行版默认的Shell也是脚本里最通用的解释器。所谓“Shell脚本编程”本质上就是把一串本来要手动敲的命令提前写进一个文本文件里让Shell按顺序、按条件去执行。它不像C语言或Python那样需要编译器也不需要装一大堆依赖只要你有一台Linux系统天然就能跑这正是它“无处不在”的根本原因。1.2 脚本和“在终端敲命令”的区别以及它能帮你解决的三类问题在终端一行一行敲命令适合临时操作但一旦遇到下面这几类情况就必须上脚本重复性批量任务比如每天要清理日志、每周要备份数据库、每月要汇总报表。手工敲不但效率低还容易漏步骤、敲错参数。带条件和循环的复杂逻辑比如“磁盘使用率超过80%就发告警邮件”或“循环处理目录下所有文件”。这类带判断、循环、分支的事情手敲根本忙不过来。需要定时或无人值守的任务配合cron定时任务脚本能在凌晨两点自动执行第二天早上你看到结果就行。所以Shell脚本解决的核心问题就是把“手动、零散、易错”的命令操作变成“自动化、可复用、可排错”的流程。它适合两类人一类是系统管理员、运维工程师日常运维自动化离不开它另一类是开发、测试、数据分析师需要批量处理文件、打包发布、跑数据处理流程。就算你主要写Java或Go部署、构建、日志分析这些环节也大概率要用到Shell。2. 动手写脚本前这几个底层概念必须吃透2.1 一切皆文件与命令查找路径PATH为什么“命令找不到”最常发生Unix/Linux的设计哲学里有句经典名言一切皆文件。目录是文件、磁盘设备是文件、甚至进程间的通信管道也是文件。基于这点Shell脚本里的大部分操作最终都能落到“对文件的读写、创建、删除、查找”上。你理解了这一点再看脚本逻辑就会特别清晰无非是先找到文件再处理内容最后输出结果。那“命令”本身又是什么其实也是一些可执行文件它们被放在特定目录下比如/bin、/usr/bin、/usr/local/bin。当你敲一个命令时Shell会按照一个叫PATH的环境变量里记录的路径顺序逐个目录去查找有没有这个名字的可执行文件。如果没找到就报“command not found”。提示很多新手写好脚本一执行就提示“xxx: command not found”不是命令真的不存在而是脚本执行时的PATH环境不完整。比如用cron调度脚本时默认PATH往往只有/bin和/usr/bin你平时能用的/usr/local/bin下的命令就找不到了。解决办法是在脚本开头显式声明PATH或者直接写命令的完整路径。2.2 环境变量、退出码与管道脚本的“输入输出神经”脚本不是孤立运行的它需要读取系统信息、告诉上层它是成功还是失败还要能把多个命令串起来。这三件事分别对应三个核心机制环境变量脚本运行环境里的“全局参数”。常见的如HOME当前用户家目录、USER当前用户名、SHELL默认Shell路径。在脚本里用$变量名引用比如echo $HOME。退出码Exit Code每个命令执行完都会返回一个数字0代表成功非0代表失败不同数字常代表不同错误类型。脚本里用$?可以拿到上一条命令的退出码。这很关键因为自动化任务必须靠它判断“这步到底成没成功”。管道|把前一个命令的标准输出作为后一个命令的标准输入。比如cat access.log | grep ERROR就是把日志内容“喂”给grep去筛选。这三者加在一起脚本就有了最基本的“感知能力”能读环境、能表状态、能串联命令。后面写判断逻辑时if后面的条件本质上就是在检查命令的退出码。2.3 shebang与执行权限让脚本“能跑”的第一步经常看到Shell脚本的第一行是#!/bin/bash或#!/bin/sh这叫shebang井号感叹号。它的作用是指定“用哪个解释器来执行这个脚本”。当你直接执行脚本文件时系统会读这一行然后把脚本文件当作参数传给对应的解释器。要让脚本可以执行还需要有执行权限。最简单的方式是chmod x script.sh。很多新手在这儿卡住脚本明明写对了执行却提示“Permission denied”就是因为没有执行权限。执行Shell脚本有几种常见方式区别要清楚bash script.sh显式用bash解释执行不需要执行权限当前Shell的自定义函数和变量不会带入子Shell。./script.sh直接执行需要执行权限并且脚本第一行shebang决定解释器。source script.sh或. script.sh在当前Shell里执行不新起子Shell。常用于加载配置、让变量影响当前环境。注意我用bash script.sh来调试时有个习惯先看脚本里是否用了相对路径。因为脚本的工作目录取决于“你在哪里执行它”而不是“脚本文件放在哪里”。这是个高频坑我后面在案例里还会再提。3. 核心语法拆解变量、判断、循环、文本处理一次讲透3.1 变量与参数传递等号两边不能有空格引号有大讲究Shell变量的定义非常宽容但有两个细节几乎所有人都会踩一次细节一等号两边不能有空格。写name 张三会报错因为Shell会把name当命令、把当参数。正确写法是name张三。细节二引用变量时建议养成加双引号的习惯。比如echo $name。如果变量里含空格不加引号会被拆成多个参数。比如filemy report.txt然后cat $file会变成cat my report.txt相当于打开两个文件肯定报错。但写成cat $file就安全。脚本接收外部参数也常用到一组内置变量$0脚本本身的名字$1、$2第一个、第二个位置参数$#参数个数$或$*所有参数字符串处理是脚本里最高频的操作。这里分享几个我常用的写法strhello.world.txt echo ${str%.txt} # 去掉后缀输出 hello.world echo ${str#*.} # 去掉左边直到第一个点输出 world.txt echo ${str//./_} # 把所有点替换成下划线输出 hello_world_txt这种${变量#模式}、${变量%模式}的写法比起sed处理变量内容效率更高是纯Shell内建能力不用额外起进程。语法稍微有点绕但用熟了之后处理文件名特别顺手。3.2 条件判断、循环与函数封装写法细节决定成败条件判断if的语法是if [ -f /etc/hosts ]; then echo file exists elif [ -d /tmp ]; then echo dir exists else echo not found fi这里要注意两个坑第一[后面、]前面必须有空格[ -f /etc/hosts ]如果写成[-f /etc/hosts]会直接语法报错。第二then前面要用分号或换行分隔。更现代一点的写法是用[[ ]]它在bash里更安全支持正则匹配和更复杂的逻辑组合我推荐直接用if [[ $name a* ]]; then echo 以a开头 fi常用文件判断参数-f是否存在且为文件、-d是否目录、-x是否可执行、-s文件是否非空。数值比较用-eq、-ne、-gt、-lt字符串比较用或。循环主要有for和while# for 遍历列表 for ip in 192.168.1.1 192.168.1.2; do ping -c 1 $ip /dev/null echo $ip is up done # while 逐行读取文件 while read -r line; do echo 处理: $line done ./data.txtwhile read这个写法是处理文件按行遍历的标准姿势。-r参数可以防止反斜杠被转义解释处理路径类内容时务必加上。函数封装则能把重复代码收拢log_info() { echo [INFO] $(date %Y-%m-%d %H:%M:%S) $1 } log_info 脚本开始执行Shell函数的两个特点要牢记一是函数里的变量默认是全局的跟脚本共用如果你想要局部变量得在定义时用local关键字。二是函数要“先定义后调用”不像其他语言有提升机制所以函数定义一般放脚本靠前。3.3 文本处理三剑客grep、sed、awk在脚本里的正确用法Shell脚本里最值钱的能力不是语法花哨而是能用一行命令把“日志文件、配置文件”里的信息抽出来。我平时用得最多的三个工具grep过滤行。# 查日志里的ERROR并带行号 grep -n ERROR app.log # 忽略大小写、显示前后两行上下文 grep -i -n -C 2 error app.log # 只输出匹配部分而不是整行 grep -o [0-9]\\.[0-9]\\.[0-9]\\.[0-9]\ app.logsed流编辑替换和删除。# 把旧的IP替换成新的注意macOS和Linux的 -i 参数写法不同 sed -i s/192.168.1.1/192.168.2.1/g config.properties # 只删除第3到第5行 sed -i 3,5d data.txtawk按列抽取和统计。# 打印第1列和第3列冒号分隔 awk -F: {print $1, $3} /etc/passwd # 按条件统计日志里状态码为500的次数 awk $9 500 {count} END {print count} access.log我的经验是能把一个文本处理需求拆成“grep先圈定范围、sed做修改、awk做统计”这个流程脚本就已经能解决80%的数据处理问题。后面案例里我会组合起来用。4. 实操案例从零手写一个日志清理与备份脚本4.1 需求分析与方案设计先想清楚再动手直接讲语法容易让人原地转圈我拿一个真实项目里的需求来串一遍。某台业务服务器上有一个应用会持续写日志日志文件按天滚动存在/data/app/logs/下形如app-2024-01-15.log。现在的痛点是日志越积越多磁盘空间告警频繁。需要做两件事找出15天前、7天前的日志分别做压缩和删除。保留最近7天的日志不处理7到30天之间的日志压缩归档超过30天的日志直接删除。方案里用到两个核心命令find按时间、名字、类型查找文件。tar或gzip压缩归档。4.2 编写脚本一步一步把逻辑落成代码我先写一个基础版本逻辑是定义日志根目录、保留天数、告警阈值。用find找需要处理的老文件。超过30天的直接删除超过7天的先压缩。每次操作写一条带时间戳的日志记录方便事后追溯。脚本内容如下#!/bin/bash # 配置区 LOG_DIR/data/app/logs ARCHIVE_DAYS7 DELETE_DAYS30 SCRIPT_LOG/var/log/cleanup_script.log # 前置检查 if [ ! -d $LOG_DIR ]; then echo $(date %Y-%m-%d %H:%M:%S) [ERROR] 日志目录不存在: $LOG_DIR $SCRIPT_LOG exit 1 fi echo $(date %Y-%m-%d %H:%M:%S) [INFO] 开始清理日志目录 $LOG_DIR $SCRIPT_LOG # 压缩 7天前的日志 find $LOG_DIR -name *.log -type f -mtime $ARCHIVE_DAYS -mtime -$DELETE_DAYS | while read -r file; do if gzip -9 $file; then echo $(date %Y-%m-%d %H:%M:%S) [INFO] 压缩成功: $file.gz $SCRIPT_LOG else echo $(date %Y-%m-%d %H:%M:%S) [ERROR] 压缩失败: $file $SCRIPT_LOG fi done # 删除 30天前的日志 find $LOG_DIR -name *.log* -type f -mtime $DELETE_DAYS -delete echo $(date %Y-%m-%d %H:%M:%S) [INFO] 日志清理完成 $SCRIPT_LOG这段代码里有几个细节值得解释-mtime 7表示“7天以前修改过的文件”-mtime -30表示“30天之内修改过的文件”。两条组合正好圈出7到30天之间这个区间。压缩用gzip -9表示最高压缩比日志文件文本重复度高压缩效果明显。压缩成功后再删除原文件避免压缩一半出问题导致数据丢失。-delete是find直接删除比-exec rm {} \;更安全高效注意它必须放在find命令的最后。4.3 部署调度用cron让脚本“自己跑起来”脚本写好后先手动执行一遍验证效果。确认没问题后再配置cron定时任务# 每天凌晨2点30分执行 30 2 * * * /usr/local/bin/cleanup_log.sh /dev/null 21这里有个非常关键的细节cron环境下PATH环境变量极简脚本里如果用到/usr/local/bin下的命令必须写绝对路径或者在脚本开头显式设置PATH。我一般在脚本配置区加一行export PATH/usr/local/bin:/usr/bin:/bin不要小看这一行。很多人的脚本手动执行正常但一进cron就“行为异常”十有八九就是PATH的问题。另一个部署心得是上线前先在脚本里打开调试模式跑一次看输出确认了再关掉。后面第五部分我会细说调试方法。4.4 这个案例还能怎么扩展真实环境里清理任务往往没这么简单我习惯给它再加几个能力磁盘空间告警脚本里加一个判断用df -h取当前使用率超过阈值就调用一个告警函数比如写钉钉/企业微信机器人接口。异常退出码检查每次操作后检查$?一旦失败就发错误日志而不是悄然继续。互斥锁防止cron调度重叠执行。实现方法是在脚本开头创建一个锁文件比如/var/run/cleanup_log.lock用mkdir的原子性来确保只有一个实例在跑。互斥锁加锁的标准写法是这样LOCK_DIR/var/run/cleanup_log.lock if ! mkdir $LOCK_DIR 2/dev/null; then echo $(date %Y-%m-%d %H:%M:%S) [WARN] 已有清理任务在执行退出 $SCRIPT_LOG exit 0 fi trap rmdir $LOCK_DIR EXITtrap很关键它保证脚本不管是正常结束还是中途出错都会去删掉锁目录避免死锁。5. 调试技巧与常见问题排查实录5.1 最实用的调试手段bash -x、-n、set -euxo pipefail写Shell脚本最痛苦的是“报错看不懂、不报错结果也不对”。我调试时基本靠三板斧第一板斧语法检查不需要执行。bash -n script.sh它只检查语法错误不执行脚本。有语法问题会立刻提示“line xx: syntax error”改完再跑。第二板斧跟踪执行过程。bash -x script.sh它会打印每一行命令展开后的实际内容在每行前面用号标出来。这对排查“变量为什么是空的、命令参数有没有传对”特别有效。第三板斧脚本内嵌严格模式。我写稍微复杂一点的脚本时开头一般是这样的#!/bin/bash set -euo pipefail这一行的四个参数分别解决不同问题-e任何一条命令退出码非0就立刻退出。避免“最后一行成功前面其实已经失败”的假象。-u使用未定义的变量直接报错而不是当成空字符串。能抓出一堆拼写错误。-o pipefail管道中只要有任何一个命令失败整条管道就算失败。否则cmd1 | cmd2的退出码只取决于cmd2cmd1挂了你也发现不了。-x把trace开关放开方便看执行流程。这个可以单独在调试时开。但这类严格模式也不是无脑加。比如你的脚本里本来就希望某条命令失败后继续执行就需要在命令末尾加|| true或者临时关闭set -e再用set e恢复。我自己的习惯是清洗数据类脚本、有明确重试逻辑的脚本慎用全局-e日常任务脚本该加就加。5.2 线上常碰到的报错与处理思路把实际运维里反复出现的几个问题整理成速查表方便对症下药报错现象可能原因解决办法command not foundPATH不完整或命令没安装写全路径或脚本开头export PATHPermission denied脚本没有执行权限chmod x script.sh$\r: command not found脚本在Windows下编辑带CRLF换行sed -i s/\r$// script.sh或改用LF换行[: missing ]if判断方括号里空格没写对检查[后、]前是否都有空格变量值为空变量名拼错或等号两边有空格set -u开启未定义变量检查用bash -x查看展开结果本地正常cron里不正常PATH不同、环境变量缺失、执行用户不同cron里显式用绝对路径脚本内固定PATH日志时间不对或乱码未设置时区或LANG环境变量脚本开头export TZAsia/Shanghai、export LANGC.UTF-8排查时我通常遵循一个顺序先看退出码再用bash -x看执行轨迹接着检查变量值最后看文件权限和路径。80%的问题都能在这几步里定位。6. 脚本风格、可维护性与我个人踩过的那些坑6.1 写脚本也要讲“工程化”命名、注释、模块化很多人觉得Shell脚本是“临时东西”不必讲究规范。但真实情况是脚本进了cron以后可能要在生产环境跑两三年。你三个月后再回头看自己写的脚本如果没有清晰注释真的会忘了当初为什么这么写。我个人的脚本规范是这几条脚本头部写明用途、作者、创建时间、最后修改时间、用法示例。配置项全部集中放在脚本顶部用大写变量名比如LOG_DIR。函数按功能拆分函数名统一用动词名词如backup_db()、send_alert()。关键命令执行前echo一句提示信息关键步骤后用退出码判断并记录日志。全文统一用bash语法脚本中尽量使用bash而不是sh的兼容子集。6.2 我实际使用中印象最深的几个教训写Shell这么多年来有几个教训是我每次分享都要念叨一遍的第一个不要把“等价命令”擅自替换。我早期写脚本时为了图省事把find删除改成了rm -rf $(find ...)。结果某次路径变量为空命令最终变成了rm -rf /的前半段幸好及时发现。后来我定了死规矩凡是删除操作一律用find -delete并且在脚本里对目录变量做非空校验。这一条值得所有人刻进脑子里。第二个处理文件名时永远记得加引号。有些文件名会带空格甚至带换行符。不加引号时循环遍历会把一个名字拆成多个。用while read -r file; do配合$file是处理“文件名带特殊字符”的标准解法。第三个别用sleep硬扛等待任务。最早我写一个等待后端启动的脚本用的是sleep 30然后检查进程。后来同事提醒我应该用循环探测端口while ! nc -z 127.0.0.1 8080; do sleep 2; done。这样容器启动快时不用白等启动慢时也不用担心时间不够。这种“探测式等待”比死等稳健得多。第四个脚本里要处理“半途失败”的善后。比如压缩日志时压缩到一半磁盘满了原文件还完好但.gz文件是残的。这时如果脚本继续执行删除会发现gzip返回非0直接用连接就不会执行删除。我习惯是if gzip $file; then rm $file; fi确保压缩成功才删原文件。6.3 后续可以继续扩展的几个方向Shell脚本学到这儿基本日常自动化已经够用了。再往上走有几个方向你可以根据自己的场景继续深入和监控系统结合让脚本输出标准格式的指标数据接入已有的监控告警平台。用Shell做CI/CD流水线脚本很多发布流程里的构建、测试、部署步骤底层都是Shell脚本在驱动。学习配套工具链比如jq处理JSON、yq处理YAML、parallel做并行处理。这些工具配合Shell能覆盖更复杂的数据加工场景。理解其他脚本语言Shell适合“胶水层”和任务编排但复杂的数据结构、算法逻辑还是交给Python、Go这些更合适。真实项目里的最佳实践往往是“ShellPython”混合Shell负责调度、调用、处理文件Python负责重逻辑。我自己现在写自动化任务时思路已经固定了能一条awk解决的绝不上循环能用find解决的绝不手写遍历能用函数封装的绝不复制粘贴。脚本写得越短越清晰出问题的概率就越低。这种“偷懒”其实是最认真的工程态度——把时间省下来去处理真正需要人处理的事情。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表