多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Linux Shell脚本编程实战:从自动化基础到批量运维技巧

Linux Shell脚本编程实战:从自动化基础到批量运维技巧 不少刚接触Linux的朋友都有一种感觉命令背了一堆遇到重复性工作还是只能一条一条手工敲。真正让Linux运维效率起飞的关键恰恰是Shell脚本编程这门基本功。简单说Shell脚本就是把一串命令写进文件里让它批量、自动、可复用地去执行这也是“脚本自动化基础”最核心的价值所在。这篇文章我会从脚本的底层逻辑讲起再到变量、判断、循环、位置参数这些必备要素然后结合真实场景把高频命令的坑和组合用法拆开揉碎最后给出一套可以直接抄作业的自动化案例。无论你是刚入门的新手还是写过几个脚本但总被各种报错卡住的进阶用户这篇都能给你一些用得上的东西。1. 脚本到底是个什么东西——先建立正确的自动化思维1.1 别把脚本当“高级命令”它是一套执行逻辑我见过太多人把Shell脚本当成“把命令罗列在一起”的文件这种理解虽然不算错但对写脚本这件事帮助不大。Shell脚本的本质是让Shell解释器按你设定的顺序、条件和循环去执行一系列命令并且在这个过程中处理变量、接收参数、判断结果。用人话说你在终端里敲命令就像手动炒菜每一步都得自己看着来放油、下菜、加盐、翻炒。而脚本相当于你把整个菜谱写下来交给一个厨师去照做他还会自己检查“盐放够了没有”“菜熟没熟”。这个“检查”就是脚本里的判断逻辑“反复翻炒”就是循环“这一勺盐是多少”就是变量和参数。理解了这一层你就明白为什么脚本自动化基础这么重要了。比如你要在20台服务器上批量改一个配置文件的权限、要把一天的日志按小时归档、要定时检测某个进程是否挂了然后自动拉起这些事靠手工敲命令会让人崩溃但用脚本写下来就是几秒钟的事。1.2 自动化思维的三个层次替代、复用、编排我在带新人的时候经常跟他们讲一句话写脚本不是为了显得自己很厉害而是为了把时间省下来去干更重要的事。自动化思维是可以分层次的第一层是替代把一条长命令、一串重复命令写进脚本减少击键量。这个层次门槛最低但它能让你尝到甜头。第二层是复用写一个脚本通过传递不同的参数来适应不同的场景。比如一个日志清理脚本传路径和保留天数就能处理任意目录不用每次改代码。第三层是编排把多个脚本、多个命令组合成一个完整流程加入判断、循环、异常处理。比如“先备份、再更新、然后重启服务、最后检查健康状态”这就是一个标准的部署自动化脚本的雏形。绝大多数人卡在第二层到第三层之间。原因不是不懂语法而是缺少“用程序逻辑去思考运维任务”的思维习惯。举个例子你接到一个任务把某个目录下的所有.log文件重命名加上日期后缀。第一反应可能是mv a.log a_20250101.log这样手动敲但你要是用循环加变量去写一条for就能处理所有文件。2. 脚本自动化的核心元素——变量、作用域与流程控制2.1 变量脚本的“记忆单元”以及被误解的export变量是脚本里最基础也最重要的概念它相当于给数据贴了个标签之后你可以到处引用。Shell变量的语法很直白namevalue注意等号两边绝对不能有空格这是新手最容易踩的坑。我见过有人写name value然后报command not found一脸懵。光会定义变量还不够你得理解变量的作用域。默认情况下你在脚本里定义的变量是“局部”的——只对当前Shell进程有效不会传递给子进程。这里就要说到export了。很多教程会告诉你“export是导出环境变量”但没告诉你到底导出了什么。实际上export做的事是把变量标记为“可传递给子进程”。打个比方普通变量是你写在便签上的信息只给你自己看export之后的变量相当于你把便签贴在了工牌上你的下属子进程一看工牌就知道这些信息。看一个我会在实际教学中反复用的例子#!/bin/bash MY_NAMElinux echo 当前进程: $MY_NAME # 启动一个子shell试试 bash -c echo 子进程直接读取: $MY_NAME export MY_NAME bash -c echo 子进程export后读取: $MY_NAME执行结果非常直观第一次子进程读不到export之后就能读到了。很多人写脚本时发现调用的其他脚本怎么都获取不到自己定义的变量问题就出在这里——忘了export。2.2 条件判断if、test与那个让人迷惑的-n自动化的核心能力之一就是“会看情况办事”这个在Shell里靠if判断实现。最基础的写法if [ 条件 ]; then 命令 else 命令 fi注意那个[ ]它实际上是一个叫test的命令所以方括号两边必须有空格。写if [$name ]会直接报错。条件判断里字符串判断是高频操作尤其是-n。-n的意思是“字符串长度不为0”也就是“变量非空”。常见的组合是if [ -n $1 ]; then echo 收到了第一个参数: $1 fi这种写法的典型应用场景是脚本必须在有参数时才能继续执行。如果用户忘了传参就提示用法并退出。实际操作中我强烈建议变量加双引号因为变量为空时[ -n $1 ]会被解释成[ -n ]而-n本身是个非空字符串判断结果恒为真这就会产生逻辑错误。这类问题排查起来非常隐蔽我后面会专门讲。关于判断条件我还想提醒几个容易混淆的-z表示“字符串为空”和-n正好相反-f判断文件是否存在且是普通文件-d判断是否为目录-eq、-ne、-gt、-lt是整数比较不要和字符串比较混用2.3 循环for循环批量处理的威力说for循环是脚本自动化第一功臣一点也不夸张因为日常运维里到处都是“批量”需求。最常用的两种写法# 写法一遍历一段数字 for i in {1..10}; do echo 第 ${i} 次 done # 写法二遍历一个列表 for file in /var/log/*.log; do echo 处理文件: ${file} done # 写法三C风格 for ((i1; i10; i)); do echo $i done我在批量处理文件时最常用的是第二种写法。比如压缩多天的日志直接一条for f in /data/logs/access_2025*.log; do gzip $f done这里必须提醒for file in /var/log/*.log在没有任何匹配文件时file会被赋成字面量/var/log/*.log也就是通配符本身。这种野路子的结果就是后面命令拿一个不存在的文件去操作报错还不容易发现。预防办法是先判断文件是否存在或者用nullglob之类的选项。2.4 位置参数与shift让脚本“接得住话”写一个能接收参数的脚本是脚本复用性的关键一步。Shell里$1、$2就是第1个、第2个参数$#是参数总数$是所有参数。但很多人不知道shift的妙用。shift的作用是把所有位置参数向左移动一位原来的$2变成$1原来的$1就被丢掉了。这有什么用最经典的应用是处理带选项的命令行。比如你写一个脚本需要支持-f 文件名 -d 天数这种带值参数#!/bin/bash while [ $# -gt 0 ]; do case $1 in -f) FILE$2 shift 2 ;; -d) DAYS$2 shift 2 ;; *) echo 未知参数: $1 exit 1 ;; esac done echo 文件: $FILE, 天数: $DAYS这种“循环shiftcase”的组合是很多高级脚本里处理参数的通用框架。理解了shift你就能看懂别人脚本里那些“莫名其妙”的shift 2是在干什么——跳过选项名和它的值处理下一个参数。3. 高频命令的深度用法与组合实战——cd、cat、重命名一次讲透3.1 cd和cat你以为你会的可能真的没弄明白cd和cat算是Linux里最常用的两个命令了但恰恰是基础命令很多人用的时候有认知偏差。先看cd。它不只是“切换目录”这么简单它直接影响的是当前Shell的工作路径而这个路径决定了你脚本里所有相对路径的解析起点。写脚本时我基本不用cd改用绝对路径或者变量拼接就是因为cd会让脚本的路径体系变得脆弱。你在脚本里cd /data后执行rm somefile如果/data不存在脚本不会停而是继续执行rm此时它删的是当前目录下的somefile这是非常危险的事。实际开发中我推荐的做法是BASE_DIR/data/app LOG_DIR${BASE_DIR}/logs if [ -d $LOG_DIR ]; then cd $LOG_DIR # 在确认目录存在后才cd else echo 目录不存在: $LOG_DIR exit 1 fi再看cat。很多人只用它来查看文件却忽略了它在脚本里有两个非常高频的用法一是读取文件内容赋值给变量二是配合EOF生成多行文本。# 读取文件到变量 content$(cat /etc/hostname) echo 主机名是: $content # 用cat生成多行配置文件 cat /tmp/nginx.conf EOF server { listen 80; server_name example.com; } EOF第二种写法在自动化部署场景里几乎是标配。你不用再手写文件再上传脚本自己就能生成想要的配置文件配合变量替换一套模板适配多套环境。3.2 重命名的正确姿势从mv到批量处理的进化和重命名相关的热词里很多人搜“linux用shell重命名文件”。单文件重命名很简单mv oldname newname但批量重命名就有讲究了。给大家分享一个我常用的批量加日期后缀的脚本#!/bin/bash # 批量给指定目录下的.log文件加上日期后缀 TARGET_DIR$1 DATE_SUFFIX$(date %Y%m%d) if [ -z $TARGET_DIR ]; then echo 用法: $0 目标目录 exit 1 fi for file in ${TARGET_DIR}/*.log; do if [ -f $file ]; then base${file%.log} new_name${base}_${DATE_SUFFIX}.log mv $file $new_name echo 已重命名: $file - $new_name fi done这里有两个小技巧值得展开。${file%.log}是Shell的变量模式匹配%表示从尾部删除最短匹配也就是把.log后缀摘掉然后拼接新的后缀。另一个技巧是if [ -f $file ]用来防止通配符没有匹配到任何文件时的误操作。批量重命名场景里还会有更复杂的需求比如“去掉文件名中的特定字符串”“改成统一前缀”思路都是先提取、再拼接、最后mv。核心不是mv这个命令本身而是你怎样用变量操作和循环把命名规则表达出来。4. Shell常见坑与排查技巧实录——这些年我踩过的雷4.1 变量加不加引号结果天差地别在我遇到的所有Shell问题里因为引号引发的问题占比最高。前面提到过[ -n $1 ]的陷阱这属于变量没加引号导致语义变化的典型。再举一个更隐蔽的例子name if [ -n $name ]; then echo 非空 else echo 为空 fi你以为会输出“为空”实际上输出的是“非空”。因为[ -n $name ]在name为空时被解释成[ -n ]而-n作为字符串长度为2不为空所以判断为真。这个Bug非常经典几乎每个写过Shell的人都踩过。解决方式就是任何变量都养成加双引号的习惯[ -n $name ]。同样的道理适用于变量展开。echo $file和echo $file表面上差不多但文件名里有空格时不加引号就会被拆成多个词后面命令就懵了。写脚本的黄金法则变量出现的地方默认加双引号除非你有明确理由不加。4.2 语法细节空格、换行和隐藏字符Shell对空格和换行的敏感程度远超其他语言这是新手最大的拦路虎。ab是赋值a b是执行一个叫a的命令并传两个参数。[条件]和[ 条件 ]也不一样后者才是正确写法因为[是一个命令命令和参数之间必须有空格。还有一个陷阱是Windows下编辑脚本导致的问题。在Windows里写脚本保存后传到Linux执行经常报$\r: command not found。这是因为Windows的换行是\r\nLinux只认\n多出来的\r被当成命令的一部分。解决办法是转换格式sed -i s/\r$// script.sh # 或者用 dos2unix script.sh4.3 crontab环境变量与脚本执行环境的差异很多人写好的脚本手动运行时一切正常一放到crontab里就出幺蛾子。最典型的场景是脚本里用了某个命令手动执行能找到定时执行就报command not found。原因在于手动执行脚本时你的Shell已经加载了/etc/profile和~/.bashrcPATH环境变量里包含了各种可执行文件路径。而crontab执行脚本时用的是非交互Shell加载的环境变量少得可怜PATH可能只剩/usr/bin:/bin。解法也简单脚本开头显式声明环境#!/bin/bash source /etc/profile export PATH/usr/local/bin:/usr/bin:/bin:$PATH还有一种更隐蔽的情况脚本在终端里执行正常但通过sh script.sh执行就报错。这通常是因为脚本里有[[或${var//pat/rep}等bash专用语法而你用sh调用时实际解释器可能是dash它不认这些高级语法。规范做法是脚本开头写#!/bin/bash执行时用bash script.sh或直接./script.sh不要用sh script.sh去跑bash脚本。4.4 调试三板斧bash -x、set -x和回显脚本出问题不可怕可怕的是你不会排查。我最常用的方法是bash -x script.sh它能逐行显示脚本执行的每一步展开变量后的实际命令都会打印出来前缀表示执行的命令。看输出你就能定位到到底是哪一步出了问题、变量值是什么。如果不想整段跑可以在脚本内部加set -x从该位置开始打印调试信息配合set x关闭#!/bin/bash set -x echo 调试这一段 set x echo 这一段不调试还有一个容易被忽略的排查工具是echo。在关键步骤前后加输出把变量值打出来虽然原始但异常好用。尤其是判断分支比较多的时候在if的每个分支里加个提示你立刻知道脚本走了哪条路。建议所有脚本开头加set -u它能让脚本在遇到未定义变量时报错退出而不是静默地用空值继续跑。很多诡异问题就是变量的名字拼错了Shell却把空的未定义变量当作合法值用导致结果莫名其妙。再加一个set -e遇到任何命令失败就退出避免错误被掩盖、脚本带着错误状态一路执行下去。不过set -e有些时候会误伤比如grep没匹配到内容返回非零、你的脚本逻辑里这不算错误所以要根据场景选择。另外排查问题时别忽略语法检查。bash -n script.sh只做语法检查不执行能在运行前发现拼写、引号配对这类低级问题。5. 脚本自动化的落地案例——三个可以直接抄的实战脚本5.1 场景一日志文件按日期归档并清理过期日志日志处理是运维自动化里最先接触也是最典型的需求。这个脚本要做的事把昨日日志重命名加上日期后缀然后删除超过30天的旧日志。#!/bin/bash # 日志归档与清理脚本 # 用法: ./log_archive.sh /var/log/myapp 30 LOG_DIR$1 KEEP_DAYS${2:-30} YESTERDAY$(date -d yesterday %Y%m%d) if [ -z $LOG_DIR ] || [ ! -d $LOG_DIR ]; then echo 错误: 日志目录不存在或未提供 exit 1 fi # 归档昨日日志 for file in ${LOG_DIR}/*.log; do if [ -f $file ]; then base${file%.log} new_name${base}_${YESTERDAY}.log mv $file $new_name echo 归档: $(basename $file) - $(basename $new_name) fi done # 清理过期日志 find $LOG_DIR -name *.log -mtime $KEEP_DAYS -delete echo 已清理 ${KEEP_DAYS} 天前的日志这里用到了几个值得细说的点。${2:-30}是Shell的参数默认值写法意思是如果第二个参数没传或为空就用30。date -d yesterday能得到昨天的日期。find -mtime 30表示修改时间超过30天的文件-delete直接删除。这三个都是自动化脚本里的高频件。5.2 场景二批量重命名文件并统一格式这个脚本解决的是素材文件批量整理问题。比如你有一堆照片名字乱七八糟想统一改成photo_001.jpg这种格式#!/bin/bash # 批量重命名统一为指定前缀加序号 # 用法: ./batch_rename.sh /path/to/photos photo DIR$1 PREFIX${2:-file} EXT${3:-jpg} COUNT1 if [ ! -d $DIR ]; then echo 错误: 目录不存在 exit 1 fi for file in ${DIR}/*.${EXT}; do if [ -f $file ]; then new_name${PREFIX}_$(printf %03d $COUNT).${EXT} mv $file ${DIR}/${new_name} echo 重命名: $(basename $file) - ${new_name} COUNT$((COUNT 1)) fi doneprintf %03d的作用是把数字格式化为三位不足补零这样排序时文件名不会出现photo_10排在photo_9前面的问题。$((COUNT 1))是Shell的算术运算语法注意变量名前不需要加$符号也能运算。这种脚本写起来不难但能极大提升整理文件的效率。我有一次整理几百张活动照片手动改会疯掉用类似脚本几秒钟全搞定。5.3 场景三系统巡检脚本生成报告巡检脚本是自动化运维的另一个典型场景。它把系统状态搜集下来生成一份可读的报告#!/bin/bash # 系统基础巡检脚本 REPORT/tmp/sys_check_$(date %Y%m%d_%H%M%S).txt { echo 系统巡检报告 echo 时间: $(date %Y-%m-%d %H:%M:%S) echo 主机名: $(hostname) echo echo --- 系统负载 --- uptime echo echo --- 内存使用 --- free -h echo echo --- 磁盘使用 --- df -h echo echo --- CPU占用TOP5 --- ps aux --sort-%cpu | head -6 echo echo --- 当前监听端口 --- ss -tlnp } $REPORT echo 巡检报告已生成: $REPORT这里用的{ ... } file技巧很多人不熟它把一组命令的标准输出整体重定向到同一个文件省得每条命令单独写一次重定向。配合date生成带时间戳的报告文件名每一份报告都不会被覆盖。如果想让脚本更智能一点可以加上条件判断比如磁盘使用率超过80%就额外输出警告。巡检脚本的价值不在于命令有多高级而在于它把散落的检查项组织成了一个标准化的、可重复执行的产品。5.4 如何把这些脚本纳入定时任务脚本写好了最后一步是让它自动化地跑起来。crontab -e打开的是当前用户的定时任务表每一行的格式是“分 时 日 月 周 命令”。举个实际例子# 每天早上3点执行日志归档 0 3 * * * /opt/scripts/log_archive.sh /var/log/myapp 30 # 每周一上午9点执行系统巡检 0 9 * * 1 /opt/scripts/sys_check.sh加上定时任务之前记得先确认脚本有执行权限chmod x script.sh。另外我建议把crontab里的命令用绝对路径写完整包括脚本路径和日志路径这能避免环境不同导致的诡异问题。日志重定向也要养成习惯 /tmp/cron.log 21这样脚本出错时你有迹可查。6. 我的几点体会写Shell脚本这几年我最大的感受是语法真的不难难的是把复杂任务拆解成清晰的执行步骤然后用Shell的逻辑把它表达出来。初学者不妨从最笨的办法入手——先手动执行一遍要自动化的任务把每一步命令记录下来再逐步替换成变量、加上判断和循环脚本自然就成型了。另外不要试图写出一个万能脚本。脚本越通用可能性和分支就越多调试成本也越高。实际运维中我更倾向于把一个大任务拆成几个职责单一的小脚本再用一个主脚本把流程串起来。这样做的好处是单个脚本好理解、好排错出了问题时定位范围小也不会因为一个环节挂了把整条链路拖垮。最后想说Shell脚本是那种投入产出比特别高的技能花两周时间把基础打牢之后几年的运维效率都会因此受益。这篇提到的坑和技巧都是我实际干活时反复用到的希望对你有点帮助。
返回列表