news 2026/9/19 14:31:23

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux .sh 脚本实战:从运维夜班到自动化清理与健康检查

1. 从一个运维夜班说起:为什么.sh文件值得每个Linux使用者吃透

凌晨两点,告警群里弹出一条消息:某台业务服务器的磁盘使用率飙到 92%。我登录上去,df -h一看,日志目录里堆了几十个 G 的历史文件。手动删?几十个目录,一个个rm敲到天亮。当时我写了一个不到十行的.sh文件,遍历目录、按修改时间过滤、批量清理、输出清理报告,跑完只用了十几秒。那一刻我真切体会到:在 Linux 世界里,.sh 文件不是“可选项”,而是把重复劳动压缩成一次点击的核心工具

这篇内容我想聊的就是.sh文件在 Linux 系统中的实战应用与技巧。它是什么?简单说,.sh是 Shell 脚本文件最常见的后缀,里面写的是一串可以被bashshzsh等 Shell 解释器逐行执行的命令。它能做什么?把日常重复的命令组合、流程编排、定时任务、批量处理、环境初始化全部自动化。适合谁看?刚接触 Linux 的新手、需要维护服务器的运维、做数据处理的开发,甚至只是想在个人电脑上少敲几行命令的普通用户。

很多人对.sh有误解,觉得它是“高级运维才玩的东西”。其实你只要会在终端里敲lscdcp,就已经具备写脚本的基础了。脚本无非是把你手敲的命令按顺序写进文件,再加上变量、判断、循环,让它能应对不同情况。真正拉开差距的,不是语法背得多熟,而是知不知道在什么场景下该用哪种写法、踩过哪些坑、怎么让脚本跑得稳。下面我按自己的实战经验,从设计思路到落地细节,一层层拆开讲。

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 到底怎么选

热词里经常出现bashzshfish这些词,它们统称为 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 脚本的“骨架”应该长什么样

一个结构清晰的脚本,我通常按这个顺序组织:

  1. shebang 和脚本说明注释
  2. 严格模式设置(set -euo pipefail
  3. 变量定义区
  4. 函数定义区
  5. 主逻辑
  6. 退出码处理

set -euo pipefail这行值得单独说。-e让脚本遇到错误命令立即退出,避免错误累积;-u让使用未定义变量时报错,防止变量名拼错导致意外;-o pipefail让管道中任何一个环节失败都算失败。这三个选项加上去,脚本的“抗造”程度会明显提升。我早期写脚本不爱加这些,结果有一次变量名拼错,脚本把空值当成路径,差点删错目录,从那以后这行成了我的标配。

3. 核心细节解析与实操要点:从 chmod 到变量,每个环节都有讲究

3.1 chmod:让脚本“能跑”的第一步

写完.sh文件,直接./script.sh执行,大概率会看到Permission denied。这是因为新建的文件默认没有执行权限。这时候就要用到chmod

chmod是 change mode 的缩写,用来修改文件权限。Linux 权限分三组:文件所有者、所属组、其他用户,每组有读(r=4)、写(w=2)、执行(x=1)三种权限。chmod +x script.sh是最常用的写法,给文件加上执行权限。等价于chmod 755 script.sh,即所有者可读写执行(7=4+2+1),组和其他用户可读可执行(5=4+1)。

热词里频繁出现chmod 777,我得提醒一句:777 意味着所有用户都能读写执行,这是安全隐患。生产环境里给脚本 755 就够了,除非有特殊需求,否则不要用 777。我见过有人为了图省事,把整个 web 目录设成 777,结果被上传了恶意脚本,教训很深刻。

还有一种情况:脚本在 Windows 上编辑过,传到 Linux 后执行报bad interpreter: No such file or directory。这通常是换行符问题,Windows 用\r\n,Linux 用\n,shebang 行末尾多了个\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,循环用forwhile。这两个是脚本自动化的核心。

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.txtIFS=防止行首行尾空格被吃掉,-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_DAYS=7 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配合-print0while read -d ''

#!/usr/bin/env bash set -euo pipefail TARGET_DIR="${1:-.}" OLD_EXT="jpeg" NEW_EXT="jpg" 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_NAME="myapp" CHECK_URL="http://127.0.0.1:8080/health" LOG_FILE="/var/log/health_check.log" MAX_RETRY=3 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 1

curl -sf-s静默模式,-f让 HTTP 错误码返回非零退出码。--max-time 5设置超时,防止脚本卡死。systemctl restart ... || true是防止重启命令本身失败导致set -e让脚本退出,我们要的是继续重试而不是直接挂掉。

提示:这个脚本依赖systemctl,在容器环境里可能不可用。容器里通常用supervisor或直接检查进程。写脚本前先确认目标环境的服务管理方式,别照搬。

5. 常见问题与排查技巧实录:那些文档里不会写的坑

5.1 脚本报错速查表

报错信息常见原因解决办法
Permission denied没有执行权限chmod +x script.sh
bad interpreter: No such file or directory换行符是\r\n或 shebang 路径错误dos2unix转换,或检查 shebang
command 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 -xset +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 默认 GBK,Linux 用 UTF-8,中文注释可能乱码。统一用 UTF-8 保存,编辑器里设置好。

5.4 关于“张豪全防格机sh文件”这类热词的说明

热词里出现了一些特定名称的.sh文件,比如“张豪全防格机sh文件”。这类词往往指向某些特定场景下的脚本,但具体内容我不了解,也不做展开。我想说的是:拿到任何来源不明的.sh文件,执行前一定要先看内容。用catless打开,重点看有没有rm -rfddmkfs> /dev/sda这类危险操作。不认识的脚本,先在虚拟机或测试环境里跑,确认安全再上生产。这个习惯能帮你避开绝大多数“脚本事故”。

6. 从入门到进阶:脚本能力提升的路径建议

6.1 新手阶段:先把手敲的命令变成脚本

如果你刚接触 Linux,别急着学高级语法。先做一件事:把你每天重复敲的命令序列记下来,写成一个.sh文件。比如每天要cd到项目目录、git pull、重启服务、看日志,这四步就可以写成一个脚本。写的过程你会自然遇到权限问题、路径问题、变量问题,解决这些问题的过程就是最好的学习。

这个阶段的目标是“能跑”。不用追求优雅,不用纠结用[ ]还是[[ ]],先让脚本完成工作。跑通了,你就有成就感,就有动力继续深入。

6.2 进阶阶段:关注健壮性和可维护性

当你能写出跑得通的脚本后,下一步是让它“跑得稳”。加上set -euo pipefail,加上参数检查,加上日志输出,加上错误处理。把重复逻辑抽成函数,把可变配置抽成变量。这个阶段你会开始理解为什么有些脚本“看起来差不多,但就是更靠谱”。

同时开始用shellcheck检查自己的脚本,看它给出的每一条建议,理解背后的原因。比如它提示“变量加引号”,你去查为什么,就会学到单词拆分和 glob 展开的知识。这种“先实践后理论”的路径,比先啃语法书效率高得多。

6.3 高阶阶段:脚本编排与工程化

再往上走,就是多个脚本的协作和工程化。比如用Makefilejustfile管理常用脚本,用crontabsystemd timer做定时调度,用ansible做批量部署。脚本本身也会变得更复杂,需要处理并发、锁、信号、超时等。

这个阶段的核心能力是“设计”。知道什么该用脚本做,什么该用专门工具做;知道脚本的边界在哪,什么时候该换成 Python 或 Go。脚本不是万能的,它的优势是轻量、直接、无需编译,适合胶水逻辑和系统管理。把脚本用在合适的地方,比把脚本写得多么复杂更重要。

6.4 几个值得长期坚持的习惯

第一,脚本开头写注释,说明用途、作者、修改记录。三个月后你回来看,会感谢当时的自己。

第二,危险操作加确认。删除、覆盖、重启这类操作,要么加read -p确认,要么先 dry-run 输出一遍。

第三,脚本纳入版本管理。用 git 管理你的脚本目录,每次修改都有记录,改坏了能回滚。

第四,定期回顾和重构。用得多了会发现有些写法可以优化,有些函数可以合并,保持脚本的整洁。

我个人在实际操作中的体会是:脚本能力的提升,不靠背语法,靠解决真实问题。每遇到一个重复劳动,就想“这个能不能写成脚本”;每写一个脚本,就想“下次怎么写得更好”。一年下来,你会发现自己处理问题的速度和底气完全不一样了。最后再分享一个小技巧:把你最常用的脚本放在~/bin目录下,把这个目录加到 PATH 里,以后在任何地方都能直接敲脚本名执行,省去./和路径的麻烦。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 14:31:00

医学影像重采样:空间坐标系重建与HU值保真

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 14:30:36

Flutter插件鸿蒙化适配:flutter_iot_wifi WiFi配网功能迁移实战

前阵子把一个智能家居 App 的 Flutter 工程往 OpenHarmony 设备上迁移&#xff0c;页面、状态管理、网络层都还算顺利&#xff0c;卡得最久的反而是一个平时没人注意的插件&#xff1a;flutter_iot_wifi。这个插件干的是 IoT 设备 WiFi 配网里最基础的事——扫描附近热点、读取…

作者头像 李华
网站建设 2026/9/19 14:30:10

Exchange Server 2016部署与DAG高可用实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/19 14:30:08

Unity资源管理与代码热更:YooAsset与HybridCLR黄金组合实践

1. 为什么要把它俩“焊”在一起做 Unity 客户端开发做到一定阶段&#xff0c;大家基本都会撞上两堵墙&#xff1a;一是资源包体越堆越大、加载流程越来越乱&#xff1b;二是线上玩法出 Bug 想紧急修&#xff0c;却只能干等审核和整包更新。单看这两个问题&#xff0c;业界都已经…

作者头像 李华
网站建设 2026/9/19 14:29:04

BMS电池管理系统开发指南:硬件架构、SOC/SOP算法与CAN调试

简介&#xff1a;一份关于电动汽车电池管理系统&#xff08;BMS&#xff09;设计的专业参考文献&#xff0c;面向新能源汽车研发工程师、高校师生及技术爱好者&#xff0c;围绕BMS在整车中的核心作用&#xff0c;系统梳理了电池状态监控、健康诊断、充电管理、温度管理、安全保…

作者头像 李华