ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Linux .sh 脚本实战:从运维夜班到自动化清理与健康检查

Linux .sh 脚本实战:从运维夜班到自动化清理与健康检查 1. 从一个运维夜班说起为什么.sh文件值得每个Linux使用者吃透凌晨两点告警群里弹出一条消息某台业务服务器的磁盘使用率飙到 92%。我登录上去df -h一看日志目录里堆了几十个 G 的历史文件。手动删几十个目录一个个rm敲到天亮。当时我写了一个不到十行的.sh文件遍历目录、按修改时间过滤、批量清理、输出清理报告跑完只用了十几秒。那一刻我真切体会到在 Linux 世界里.sh 文件不是“可选项”而是把重复劳动压缩成一次点击的核心工具。这篇内容我想聊的就是.sh文件在 Linux 系统中的实战应用与技巧。它是什么简单说.sh是 Shell 脚本文件最常见的后缀里面写的是一串可以被bash、sh、zsh等 Shell 解释器逐行执行的命令。它能做什么把日常重复的命令组合、流程编排、定时任务、批量处理、环境初始化全部自动化。适合谁看刚接触 Linux 的新手、需要维护服务器的运维、做数据处理的开发甚至只是想在个人电脑上少敲几行命令的普通用户。很多人对.sh有误解觉得它是“高级运维才玩的东西”。其实你只要会在终端里敲ls、cd、cp就已经具备写脚本的基础了。脚本无非是把你手敲的命令按顺序写进文件再加上变量、判断、循环让它能应对不同情况。真正拉开差距的不是语法背得多熟而是知不知道在什么场景下该用哪种写法、踩过哪些坑、怎么让脚本跑得稳。下面我按自己的实战经验从设计思路到落地细节一层层拆开讲。2. 脚本整体设计与思路拆解先想清楚“谁来跑、跑在哪、跑完留下什么”2.1 为什么很多人写的脚本第一次能跑第二次就翻车我见过太多这样的脚本在作者自己的机器上跑得好好的换一台服务器就报错。原因往往不是语法问题而是设计阶段没想清楚三件事——执行环境、输入来源、输出结果。执行环境决定了你用哪个解释器。文件头那行#!/bin/bash叫 shebang它告诉系统用哪个程序来执行这个脚本。如果你写#!/bin/sh但在脚本里用了[[ ]]这种 bash 特有的语法在某些把sh指向dash的系统上就会直接报错。我的习惯是只要用到数组、[[ ]]、${var//}这类 bash 扩展就老老实实写#!/bin/bash如果追求最大兼容性就只用 POSIX 标准语法shebang 写#!/bin/sh。输入来源决定了脚本的健壮性。硬编码路径的脚本最脆弱今天目录叫/data/log明天改成/data/logs就废了。更好的做法是把可变部分抽成变量或者通过参数传入。比如清理日志的脚本我会把目标目录、保留天数、文件匹配模式都做成变量放在开头改的时候只动一处。输出结果决定了脚本能不能被“信任”。一个合格的脚本跑完应该告诉你处理了多少文件、释放了多少空间、有没有失败项。如果它默默跑完什么都不说出了问题你根本不知道从哪查。所以我在脚本里几乎都会加日志输出重要的操作还会写进日志文件方便回溯。2.2 解释器选型bash、sh、zsh 到底怎么选热词里经常出现bash、zsh、fish这些词它们统称为 Shell是用户和 Linux 内核之间的“翻译官”。你敲的命令由 Shell 翻译成系统能理解的操作。sh是最古老的 POSIX 标准 Shell几乎每个 Linux 系统都有兼容性最好但功能最少。bash是大多数 Linux 发行版的默认 Shell功能丰富支持数组、字符串操作、进程替换等是写脚本的首选。zsh交互体验好配置花哨但写脚本时语法和 bash 有细微差别不太适合作为脚本解释器。fish语法和主流 Shell 差异较大基本不用于写.sh脚本。我的建议很直接写脚本就用 bash。除非你的脚本要在极其精简的嵌入式环境里跑否则没必要为了兼容性牺牲开发效率。shebang 写#!/usr/bin/env bash比写#!/bin/bash更灵活因为env会在 PATH 里找 bash适配不同系统上 bash 的安装位置。2.3 脚本的“骨架”应该长什么样一个结构清晰的脚本我通常按这个顺序组织shebang 和脚本说明注释严格模式设置set -euo pipefail变量定义区函数定义区主逻辑退出码处理set -euo pipefail这行值得单独说。-e让脚本遇到错误命令立即退出避免错误累积-u让使用未定义变量时报错防止变量名拼错导致意外-o pipefail让管道中任何一个环节失败都算失败。这三个选项加上去脚本的“抗造”程度会明显提升。我早期写脚本不爱加这些结果有一次变量名拼错脚本把空值当成路径差点删错目录从那以后这行成了我的标配。3. 核心细节解析与实操要点从 chmod 到变量每个环节都有讲究3.1 chmod让脚本“能跑”的第一步写完.sh文件直接./script.sh执行大概率会看到Permission denied。这是因为新建的文件默认没有执行权限。这时候就要用到chmod。chmod是 change mode 的缩写用来修改文件权限。Linux 权限分三组文件所有者、所属组、其他用户每组有读r4、写w2、执行x1三种权限。chmod x script.sh是最常用的写法给文件加上执行权限。等价于chmod 755 script.sh即所有者可读写执行7421组和其他用户可读可执行541。热词里频繁出现chmod 777我得提醒一句777 意味着所有用户都能读写执行这是安全隐患。生产环境里给脚本 755 就够了除非有特殊需求否则不要用 777。我见过有人为了图省事把整个 web 目录设成 777结果被上传了恶意脚本教训很深刻。还有一种情况脚本在 Windows 上编辑过传到 Linux 后执行报bad interpreter: No such file or directory。这通常是换行符问题Windows 用\r\nLinux 用\nshebang 行末尾多了个\r系统就找不到解释器了。解决办法是用dos2unix script.sh转换或者用sed -i s/\r$// script.sh去掉回车符。3.2 变量与输入脚本灵活性的来源变量是脚本的“记忆”。定义变量很简单LOG_DIR/var/log/app注意等号两边不能有空格这是新手最容易犯的错。使用变量用$LOG_DIR或${LOG_DIR}后者在变量名后面紧跟其他字符时更清晰比如${LOG_DIR}_backup。变量分局部和全局。函数里用local声明的变量只在函数内有效不加local默认是全局的。我踩过的坑是在函数里用了和外部同名的变量没加local结果把外部的值覆盖了排查了半天才发现。所以函数内的变量能加local就加。脚本接收外部输入有三种常见方式。第一种是位置参数$1、$2分别代表第一个、第二个参数$#是参数个数$是所有参数。第二种是read命令交互式读取适合需要用户确认的场景。第三种是环境变量适合配置类信息。我写脚本时习惯在开头检查参数个数比如if [ $# -lt 2 ]; then echo 用法: $0 源目录 目标目录; exit 1; fi这样用户用错了能立刻得到提示而不是跑到一半才报错。3.3 条件判断与循环脚本的“大脑”和“肌肉”条件判断用if循环用for和while。这两个是脚本自动化的核心。if的写法有个细节[ ]和[[ ]]的区别。[ ]是 POSIX 标准[[ ]]是 bash 扩展。[[ ]]支持正则匹配~、逻辑运算符||而且不用对变量加引号也不会因为空值报错。所以用 bash 写脚本时我优先用[[ ]]。判断文件是否存在用-f判断目录用-d判断变量是否为空用-z这些是高频操作。for循环遍历列表很直观for file in *.log; do ...; done。但要注意如果目录里没有匹配的文件*.log会原样作为字符串传入导致循环体执行一次却处理了不存在的文件。解决办法是加shopt -s nullglob让没有匹配时展开为空。这个坑我在批量处理文件时踩过脚本报了一堆“文件不存在”查了半天才发现是这个原因。while循环常配合read逐行读取文件while IFS read -r line; do ...; done file.txt。IFS防止行首行尾空格被吃掉-r防止反斜杠被转义。这两个参数不加处理配置文件时经常出问题。3.4 函数与模块化让脚本从“能用”到“好维护”脚本超过一百行就该考虑用函数拆分。函数定义有两种写法function name { ... }和name() { ... }后者兼容性更好我一般用后者。函数的价值在于复用和隔离。比如日志输出我定义一个log()函数统一加上时间戳和级别所有地方调用它格式一致改起来也只改一处。再比如错误处理定义一个die()函数打印错误信息后退出比到处写echo ...; exit 1清爽得多。函数返回值要注意bash 函数只能返回 0-255 的整数作为退出码想返回字符串得用echo输出调用方用$(function_name)捕获。这个设计初看别扭习惯了就好。我通常用退出码表示成功失败用标准输出传递数据。4. 实操过程与核心环节实现三个可直接抄作业的脚本4.1 日志清理脚本从需求到落地需求很明确清理/var/log/app下超过 7 天的.log文件保留最近 7 天输出清理报告。先想清楚几个点。怎么判断“超过 7 天”用find的-mtime 7表示修改时间在 7 天以前。怎么保证安全先echo出要删的文件确认无误再真正删。怎么输出报告统计删除数量和释放空间。脚本骨架如下#!/usr/bin/env bash set -euo pipefail LOG_DIR/var/log/app KEEP_DAYS7 PATTERN*.log log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* } if [[ ! -d $LOG_DIR ]]; then log 错误目录 $LOG_DIR 不存在 exit 1 fi log 开始清理 $LOG_DIR 下超过 $KEEP_DAYS 天的 $PATTERN 文件 count$(find $LOG_DIR -name $PATTERN -mtime $KEEP_DAYS | wc -l) size$(find $LOG_DIR -name $PATTERN -mtime $KEEP_DAYS -exec du -ch {} 2/dev/null | tail -1 | cut -f1) if [[ $count -eq 0 ]]; then log 没有需要清理的文件 exit 0 fi log 找到 $count 个文件合计 $size开始删除 find $LOG_DIR -name $PATTERN -mtime $KEEP_DAYS -delete log 清理完成这里有几个细节值得说。du -ch的-c是总计tail -1取最后一行总计行cut -f1取第一列大小。2/dev/null是屏蔽权限不足等错误信息避免干扰输出。-delete比-exec rm {} \;效率高因为它不需要为每个文件启动一个rm进程。注意-delete会直接删除没有确认环节。如果你不放心可以先跑一遍不带-delete的版本看看输出列表对不对确认后再加-delete。4.2 批量重命名脚本处理文件名里的空格和特殊字符另一个高频场景是批量重命名。比如把目录下所有.jpeg改成.jpg或者给文件名加统一前缀。朴素写法是for f in *.jpeg; do mv $f ${f%.jpeg}.jpg; done。${f%.jpeg}是字符串操作去掉末尾的.jpeg。但这里有个隐患如果文件名里有空格for f in *.jpeg会把带空格的文件名拆成多个词。解决办法是用find配合-print0和while read -d #!/usr/bin/env bash set -euo pipefail TARGET_DIR${1:-.} OLD_EXTjpeg NEW_EXTjpg find $TARGET_DIR -maxdepth 1 -name *.${OLD_EXT} -print0 | while IFS read -r -d file; do newname${file%.${OLD_EXT}}.${NEW_EXT} if [[ -e $newname ]]; then echo 跳过$newname 已存在 continue fi mv -- $file $newname echo 重命名$file - $newname done-print0用空字符分隔文件名read -d 以空字符为分隔符读取这样带空格、换行符的文件名都能正确处理。mv --里的--是告诉mv后面没有更多选项了防止文件名以-开头被当成参数。这些细节平时不起眼但处理用户上传的文件时能避免很多诡异问题。4.3 服务健康检查脚本配合定时任务做监控这个脚本的用途是检查某个服务是否在运行不在就尝试重启并记录日志。适合放在crontab里每分钟跑一次。#!/usr/bin/env bash set -euo pipefail SERVICE_NAMEmyapp CHECK_URLhttp://127.0.0.1:8080/health LOG_FILE/var/log/health_check.log MAX_RETRY3 log() { echo [$(date %Y-%m-%d %H:%M:%S)] $* $LOG_FILE } check_service() { curl -sf -o /dev/null --max-time 5 $CHECK_URL } if check_service; then exit 0 fi log 服务 $SERVICE_NAME 健康检查失败尝试重启 for i in $(seq 1 $MAX_RETRY); do systemctl restart $SERVICE_NAME 2/dev/null || true sleep 5 if check_service; then log 第 $i 次重启成功 exit 0 fi log 第 $i 次重启后仍未恢复 done log 服务 $SERVICE_NAME 重启 $MAX_RETRY 次后仍失败请人工介入 exit 1curl -sf的-s静默模式-f让 HTTP 错误码返回非零退出码。--max-time 5设置超时防止脚本卡死。systemctl restart ... || true是防止重启命令本身失败导致set -e让脚本退出我们要的是继续重试而不是直接挂掉。提示这个脚本依赖systemctl在容器环境里可能不可用。容器里通常用supervisor或直接检查进程。写脚本前先确认目标环境的服务管理方式别照搬。5. 常见问题与排查技巧实录那些文档里不会写的坑5.1 脚本报错速查表报错信息常见原因解决办法Permission denied没有执行权限chmod x script.shbad interpreter: No such file or directory换行符是\r\n或 shebang 路径错误dos2unix转换或检查 shebangcommand not found命令不在 PATH 里或拼写错误用绝对路径或which确认命令位置unbound variable使用了未定义变量且开了set -u给变量设默认值${var:-default}syntax error near unexpected token括号、引号不匹配或用了不支持的语法用bash -n script.sh检查语法Too many arguments[ ]里变量没加引号且为空改用[[ ]]或给变量加引号No such file or directory路径不存在或变量为空导致路径错误echo出路径确认加存在性判断5.2 调试脚本的三个实用手段第一个是bash -x script.sh它会打印每条执行的命令和变量展开后的值能快速定位到哪一行出了问题。输出太多的话可以配合set -x和set x只对关键段落开启追踪。第二个是在脚本里加echo调试。虽然土但有效。我习惯在关键分支加echo DEBUG: 进入清理分支count$count跑一遍看输出对不对确认后删掉。第三个是用shellcheck。这是一个静态检查工具能发现很多潜在问题比如未加引号的变量、不可达代码、语法陷阱。装好之后shellcheck script.sh它会给出详细的警告和修改建议。我现在的习惯是脚本写完先过一遍 shellcheck能省下大量调试时间。5.3 几个我踩过的真实坑坑一cd失败后继续执行。脚本里写cd /some/dir然后rm -rf *如果cd失败rm就在当前目录执行了后果不堪设想。解决办法是cd /some/dir || exit 1或者用set -e让脚本在cd失败时退出。坑二管道中的错误被忽略。cat file | grep pattern如果file不存在cat报错但grep正常退出整个管道退出码是 0脚本以为成功了。加set -o pipefail能解决这个问题。坑三rm -rf $DIR/中变量为空。如果DIR没定义或为空命令变成rm -rf /灾难性后果。防御写法是rm -rf ${DIR:?DIR is not set}/${DIR:?}在变量为空时直接报错退出。坑四定时任务里环境变量缺失。手动跑正常的脚本放进crontab就报command not found。因为cron的 PATH 和登录 shell 不一样。解决办法是在脚本开头显式设置 PATH或者所有命令用绝对路径。坑五脚本在 Windows 编辑后传到 Linux。除了换行符问题还可能遇到编码问题。Windows 默认 GBKLinux 用 UTF-8中文注释可能乱码。统一用 UTF-8 保存编辑器里设置好。5.4 关于“张豪全防格机sh文件”这类热词的说明热词里出现了一些特定名称的.sh文件比如“张豪全防格机sh文件”。这类词往往指向某些特定场景下的脚本但具体内容我不了解也不做展开。我想说的是拿到任何来源不明的.sh文件执行前一定要先看内容。用cat或less打开重点看有没有rm -rf、dd、mkfs、 /dev/sda这类危险操作。不认识的脚本先在虚拟机或测试环境里跑确认安全再上生产。这个习惯能帮你避开绝大多数“脚本事故”。6. 从入门到进阶脚本能力提升的路径建议6.1 新手阶段先把手敲的命令变成脚本如果你刚接触 Linux别急着学高级语法。先做一件事把你每天重复敲的命令序列记下来写成一个.sh文件。比如每天要cd到项目目录、git pull、重启服务、看日志这四步就可以写成一个脚本。写的过程你会自然遇到权限问题、路径问题、变量问题解决这些问题的过程就是最好的学习。这个阶段的目标是“能跑”。不用追求优雅不用纠结用[ ]还是[[ ]]先让脚本完成工作。跑通了你就有成就感就有动力继续深入。6.2 进阶阶段关注健壮性和可维护性当你能写出跑得通的脚本后下一步是让它“跑得稳”。加上set -euo pipefail加上参数检查加上日志输出加上错误处理。把重复逻辑抽成函数把可变配置抽成变量。这个阶段你会开始理解为什么有些脚本“看起来差不多但就是更靠谱”。同时开始用shellcheck检查自己的脚本看它给出的每一条建议理解背后的原因。比如它提示“变量加引号”你去查为什么就会学到单词拆分和 glob 展开的知识。这种“先实践后理论”的路径比先啃语法书效率高得多。6.3 高阶阶段脚本编排与工程化再往上走就是多个脚本的协作和工程化。比如用Makefile或justfile管理常用脚本用crontab或systemd timer做定时调度用ansible做批量部署。脚本本身也会变得更复杂需要处理并发、锁、信号、超时等。这个阶段的核心能力是“设计”。知道什么该用脚本做什么该用专门工具做知道脚本的边界在哪什么时候该换成 Python 或 Go。脚本不是万能的它的优势是轻量、直接、无需编译适合胶水逻辑和系统管理。把脚本用在合适的地方比把脚本写得多么复杂更重要。6.4 几个值得长期坚持的习惯第一脚本开头写注释说明用途、作者、修改记录。三个月后你回来看会感谢当时的自己。第二危险操作加确认。删除、覆盖、重启这类操作要么加read -p确认要么先 dry-run 输出一遍。第三脚本纳入版本管理。用 git 管理你的脚本目录每次修改都有记录改坏了能回滚。第四定期回顾和重构。用得多了会发现有些写法可以优化有些函数可以合并保持脚本的整洁。我个人在实际操作中的体会是脚本能力的提升不靠背语法靠解决真实问题。每遇到一个重复劳动就想“这个能不能写成脚本”每写一个脚本就想“下次怎么写得更好”。一年下来你会发现自己处理问题的速度和底气完全不一样了。最后再分享一个小技巧把你最常用的脚本放在~/bin目录下把这个目录加到 PATH 里以后在任何地方都能直接敲脚本名执行省去./和路径的麻烦。
返回列表