
从事Linux运维和开发这些年shell脚本是我每天都会碰的东西。做日志切割、批量改文件、写定时任务、数据同步、环境初始化一件件看起来鸡毛蒜皮的小事最后都是靠脚本把人力省下来的。这是我自己笔记的第十六篇前面写过系统启动、权限、磁盘、网络这些基础内容这篇专门聊聊shell脚本把我这些年踩过的坑和觉得有价值的写法一次性梳理出来希望能帮正在学脚本的读者少走几步弯路。这篇内容不打算从什么是变量这种教科书概念开始而是直接以实战视角切入什么时候该写脚本、脚本骨架怎么搭、核心语法怎么用才不出错、完整案例怎么落地、出了问题怎么排查。适合已经会敲基本命令、想把零散命令变成可复用工具的读者如果你是完全零基础可以先拿着基本命令练几遍再来看这篇效果会更好。1. 什么时候真正需要动手写脚本1.1 脚本最擅长解决的几类问题先说实话不是所有事情都值得写成脚本。我自己判断的标准很简单这件事是不是要重复做两次以上是不是需要多个命令配合才能完成如果答案都是肯定的那基本就该写了。最常见的场景我列一下批量操作类几十台机器上收集信息、几百个文件统一改名、批量压缩解压。手工敲一条条命令又累又容易漏脚本跑一遍还能留下输出日志。定时任务类日志清理、数据备份、临时文件归档、健康检查告警。cron加脚本是绝配脚本负责逻辑cron负责定时触发。部署与初始化类新机器装环境、初始化配置、重启服务。这类操作步骤多且有固定顺序写成脚本后可以反复执行也方便在出问题时快速重来。格式转换与数据处理把CSV转成另一种格式、从日志里面提取关键字段、批量替换文件内容。配合awk、sed、grep这些工具一个瘦脚本就能解决很多所谓大数据需求的土办法方案。我遇到最多的是日志相关。比如业务系统每天产生的日志文件按照日期命名超过30天的需要删除磁盘空间到80%需要告警。这种需求用手工做根本不现实定时跑一个脚本才是正解。后面我会把这个完整脚本展开讲。即使是临时性任务只要我开始觉得这串命令要重复手敲两遍也会顺手写成脚本。写的时候可能只花三分钟但后面再遇到类似需求把脚本翻出来改两行就能用。脚本库就是这么一点一点攒出来的。1.2 别把脚本当万能钥匙shell脚本也不是没有短板。遇到下面这几类情况我建议换个工具而不是硬撑。一是性能敏感的大数据处理。比如几GB的日志要算统计指标shell里一层层循环会慢到让人怀疑人生这时候用Python、Go甚至直接上SQL都更合适。shell适合做胶水层面的组织和协调不适合做重计算。二是逻辑特别复杂的流程。判断分支多了、状态多了、需要复杂数据结构的场景shell写起来会很痛苦调试也费劲。脚本里出现几十行嵌套if基本就是该重构或者换更合适语言重写的信号。三是跨平台要求高的场景。虽然bash在各种Linux发行版上都通用但有些命令在不同发行版上行为略有差别macOS自带的是旧版bashWindows上更是另一套。如果脚本要在完全不同的环境里跑要么用Python这类跨平台解释器要么在脚本里做足兼容判断。我的经验是能用标准命令组合解决的就用脚本感觉脚本写起来已经很别扭了果断换语言。工具选对了后面维护的成本才会低。1.3 动手前先想清楚三个问题写一个脚本之前先花两分钟想三件事输入是什么、输出是什么、中间有哪些约束。输入可能是配置文件、命令行参数或环境变量输出可能是处理后的文件、日志记录或一个退出码约束包括运行权限、运行时间窗口、磁盘空间上限等。想清楚这三件事脚本的结构基本就出来了。然后遵循一个原则一个脚本只干一件事。把功能拆成函数每个函数只做它负责的那部分。这样做的好处是定位问题快、复用方便也方便以后把脚本拆成更细的模块。我早期写脚本总是一个脚本解决所有问题结果脚本越来越长改一个逻辑就动到全局线上出了故障排查得像考古。吃过亏以后才养成先拆功能、再动手写的习惯。2. 脚本骨架搭建与变量引用的关键细节2.1 shebang、执行权限和首行配置一个普通文本文件要让系统当成脚本执行需要两个条件第一行以#!开头指定解释器并且文件要有执行权限。第一行写#!/bin/bash还是#!/usr/bin/env bash两者都能用但意义不同。直接写#!/bin/bash是明确指定解释器路径性能稍好兼容性也更容易预料。写#!/usr/bin/env bash是先找到bash所在路径再执行在bash不在标准位置的系统上更灵活但env本身如果找不到也会带来问题。我自己在服务器上习惯直接写#!/bin/bash因为标准发行版上bash基本都在这个位置少一层解析也更稳。执行权限用chmod x script.sh加上。这一步很多人忽略导致明明脚本没问题却提示Permission denied。另外如果在当前目录运行脚本要写成./script.sh而不是script.sh因为当前目录通常不在PATH环境变量里。这个踩坑率极高尤其是刚从Windows转到Linux环境的读者。首行配置我还建议加两样东西一是文件头的注释写明脚本用途、创建时间和维护人二是set选项。虽然set选项后面详细讲但我现在写任何脚本开头几乎固定是#!/bin/bash # 功能xxxx # 创建2024-xx-xx set -euo pipefailset -euo pipefail这一行带来的变化我后面详细说先记住它能让脚本在出错时立刻暴露问题而不是带病往下跑。2.2 变量取值与引号使用的门道变量赋值在shell里看似简单坑全藏在细节中。赋值语句等号两边不能有空格写成name 张三会变成执行命令name直接报command not found。这个错误我见新人踩过无数次。取变量值用$name但更严谨的写法是${name}。两者差别在变量名后面紧跟其他字符时体现出来echo $name_file会把name_file当成一个变量名而echo ${name}_file才是取name变量的值再加下划线和file。凡是变量名边界不清晰的场景一律用${}包裹这是一个让脚本更清晰、更安全的好习惯。引号的使用是shell脚本最容易出问题的点。什么时候用双引号什么时候不用规则其实很明确如果变量的值可能包含空格、通配符或其他特殊字符并且你希望它作为一个整体被处理就必须加双引号。比如file_path/data/logs/app 2024.log if [ -f $file_path ]; then echo 文件存在 fi如果这里不加双引号[ -f $file_path ]会被展开成[ -f /data/logs/app 2024.log ]test命令会因为参数个数不对直接报语法错误。双引号的作用就是给整个值包一层不让shell把里面的空格当成分隔符。还有一种情况是变量的值来源不可控比如读取外部文件内容、命令输出的结果这时候双引号尤其重要。不加双引号的话文件里如果有分号、感叹号、星号之类的内容shell会做各种展开轻则结果不对重则执行了本不该执行的命令。我在这上面栽过跟头一个读取文件内容后再拼成命令发送的脚本文件里恰巧有分号结果一行原本正常的命令被拆成了两条。从那以后凡是非我手写的字符串默认一律加双引号处理。命令替换是另一个常用特性$(command)会执行命令并把输出作为值。比如拿今天日期today$(date %F)。这里我建议优先用$()而不是反引号因为反引号在处理嵌套时容易出错$()可以嵌套可读性也更好。注意命令替换的结果如果有换行符赋值给变量后会保留echo出来可能看到多行这在处理命令输出时要心里有数。2.3 位置参数与入参检查脚本接收外部输入最常见的是位置参数$1、$2这种。比如./backup.sh /data/mysql /backup脚本里$1就是/data/mysql$2就是/backup。$#表示参数个数$表示所有参数并且每个参数独立$也表示所有参数但视为一个整体。遍历参数时应该用for arg in $而不是$这样才能正确处理带空格的参数值。脚本里的关键参数我习惯加一道检查避免用户忘了传参导致脚本在后面某个莫名其妙的位置报错。典型写法if [ $# -lt 2 ]; then echo 用法: $0 源目录 备份目录 exit 1 fi这样做的好处是错误信息明确而不是脚本跑了一大半才因为找不到文件而失败。exit 1返回非零退出码调用方可以据此判断脚本执行失败。有时候参数是可选的需要默认值。shell的参数展开语法能直接处理${var:-默认值}表示var没设置或为空时使用默认值。比如日志保留天数支持通过参数指定不指定就默认30天keep_days${1:-30}。这样脚本既灵活又稳妥。类似地${var:默认值}不但取默认值还给变量赋值${var:?提示信息}在变量为空时直接退出并输出提示适合强制要求参数的场景。3. 条件判断与循环的实战细节3.1 条件测试写法单中括号和双中括号怎么选shell里做判断本质上是运行一个test命令并根据它的退出码决定分支。if [ ... ]中的方括号其实是test命令的另一种写法所以方括号两边必须有空格[ $a $b ]写成[ $a$b ]就会出错因为shell把整个字符串当成了一个参数。现代bash里更推荐用双中括号[[ ... ]]。它是bash内置的关键词支持更丰富的表达式比如和都可以用支持正则匹配也支持和||这种逻辑运算符。最关键的是在[[ ]]内部变量即使不加双引号也不会被错误分词对新手友好得多。我的建议是既然用的是bash直接统一用[[ ]]不要再拿单中括号去踩那些边边角角的坑。文件判断操作符要记牢-f判断是不是普通文件-d判断是不是目录-e判断路径是否存在-r、-w、-x判断读、写、执行权限-z和-n判断字符串为空或非空。-nt和-ot可以比较文件的修改时间新旧在增量备份场景里很好用。数值比较用-eq、-ne、-gt、-lt而不是和因为在shell里是重定向符号写错会引发文件重定向非常隐蔽。逻辑判断的写法在[[ ]]里直接用和||if [[ -f $file -r $file ]]; then。在单中括号里要用-a和-o再加上各种转义可读性和安全性都差。所以坚持用[[ ]]能让代码干净很多。3.2 for、while、until在什么场景下各司其职for循环适合遍历已知列表遍历一组文件、一组主机名、一组参数。最典型的是遍历当前目录下的所有txt文件for f in *.txt; do echo 处理文件: $f done注意*.txt在这里会先被shell展开成文件列表如果目录里没有匹配的文件展开结果会是字面的*.txt循环还是会执行一次所以最好先判断文件是否存在再进循环。遍历时如果文件名有空格for f in *.txt这种写法会出问题因为空格会被当成列表分隔。处理方法是用find配合while readfind . -name *.txt -print0 | while IFS read -r -d f; do echo 处理文件: $f done-print0把文件名以null分隔-d 让read也按null分隔读取。这个写法看着繁琐但它在处理带空格、带换行的文件名时百分之百可靠是处理任意文件名最稳妥的方式。while循环适合条件驱动的场景读文件直到文件结束、等待某个条件成立、消费某个队列。最常见的while读文件写法while IFS read -r line; do echo $line done input.txtIFS防止行首行尾的空白被trim掉-r防止反斜杠被解释 input.txt把文件作为循环的输入。这种写法比for i in $(cat input.txt)安全得多因为for配合$(cat)会把文件按空格、按换行全部分裂一遍文件内容稍微有点特殊字符就乱套了。until循环和while结构一样只是条件为假时执行。实际用得少但偶尔用来等待某个服务就绪还挺顺手until nc -z 127.0.0.1 3306; do sleep 1; done一直等到数据库端口能连通。循环里break可以提前跳出continue进入下一轮是两个常用的控制指令不要忘记。3.3 管道、子shell与循环变量丢失问题这个问题我必须单独提因为它坑过的绝对不止我一个人在管道后面接while循环循环内对变量的修改在循环结束后会丢失。看这个经典例子count0 echo -e a\nb\nc | while read line; do count$((count 1)) done echo $count输出是0而不是3。原因在于管道会让while循环跑在一个子shell进程里子shell里的变量修改不会传回父shell。我第一次遇到时百思不得其解查来查去才发现是shell的设计使然。解决思路有三种。第一种把输入重定向而不是用管道while read line; do count$((count 1)) done (echo -e a\nb\nc)进程替换(command)产生的输入通过重定向进入循环循环本身还在当前shell里运行变量修改就能保留。第二种是把结果写到文件再读出来。第三种是把处理逻辑封装成函数后用输出捕获结果但本质上还是在子shell里靠的是函数输出而非变量传递。这类子shell边界问题在管道、find -exec、括号子shell里都会出现。遇到变量值诡异消失的现象第一反应就是检查是不是跨越了子shell边界。排查方法也很简单在关键位置加echo输出中间值看看循环内和循环外的值是否一致不一致基本就是边界问题。4. 函数设计、退出码与脚本健壮性4.1 用函数拆骨架local变量防污染脚本一长组织方式就成了大问题。我现在的习惯是脚本主体只保留流程调度真正的逻辑全部放进函数。函数定义用function name() {}或者直接name() {}都可以我倾向不带function关键字和POSIX兼容性更好。函数里要特别注意变量的作用域。默认情况下函数里赋值的变量是全局的会污染脚本其他部分。所以凡是函数内部的临时变量一律用local声明local tmp_file/tmp/xxx。这能避免函数间因为同一个变量名互相影响排查问题时也清楚每个变量的归属。这个习惯和写代码时不让函数偷偷改全局状态是同一个道理。函数内用return返回一个整数状态码0表示成功非0表示失败。return和exit容易混return只是退出当前函数脚本还能继续exit是直接终止整个脚本。在某个函数里判断要不要让整个脚本停下来就要分清楚用return还是exit。我一般是函数内部出错且后续步骤依赖该函数的结果就在函数里exit 1让脚本立刻失败只是单一环节出错、不影响整体就return非零值让调用方自行处理。函数设计上还有一个经验一个函数只做一件事函数名诚实描述行为。read_config作为一个函数就只负责读取配置并校验不把别的逻辑塞进来。这样脚本读起来像是一篇有目录的文章而不是一团纠缠的线。4.2 退出码与set -euo pipefail的核心策略每个命令执行完都会留下一个退出码0是成功1到255是各种失败原因。在条件判断和循环里shell就是靠退出码来决定走向。$?保存着上一个命令的退出码但它有个致命特点任何命令执行都会覆盖$?。你想检查某个命令的结果必须立刻取值中间不能穿插任何其他命令。下面这个写法就是典型的错grep error app.log echo grep退出码记录到变量: $? if [ $? -eq 0 ]; then第一次$?还是grep的退出码第二次$?已经是echo命令的了想判断grep结果时早就不对了。正确做法是第一时间把退出码存下来grep error app.log ret$? if [ $ret -eq 0 ]; thenset选项能大幅改变脚本的容错行为。set -e表示任何一条命令返回非零就立刻退出脚本避免错误被掩盖、脚本带着错误状态继续跑出更多问题。初学者经常觉得set -e太敏感但正是这种敏感让问题尽早暴露。set -u表示使用未定义变量时直接报错退出能顺手抓出很多变量名拼写错误。set -o pipefail让管道中任何一个环节失败都被识别为管道失败否则bash只关心管道最后一个命令的状态前面环节悄悄挂了你都不知道。三个选项拼在一起set -euo pipefail就是我现在所有脚本开头的标配。它们改变了脚本遇到错误时的默认反应从默默继续变成大声失败。这个转变对线上任务的意义非常大我自己是踩过脚本其实早就出错但还在跑的亏之后才真正体会到这行配置的价值。也要说明set -e在某些场景下需要临时关闭。比如你明确知道某条命令可能失败且失败是可以接受的可以用if cmd; then的写法把它放在条件里这样set -e不会干涉。或者用cmd || true主动吞掉失败状态。但|| true相当于告诉读者这行失败无所谓所以要谨慎使用别让该报的错都被蒙混过关。4.3 trap清理与临时文件安全管理脚本跑着跑着被中断了留下的临时文件、锁文件怎么办trap命令就是为这种场景准备的。trap code EXIT可以让脚本在任何方式退出时都会执行那段清理代码无论是正常结束、被中断还是报错退出。我写脚本时凡是创建了临时目录必配一句tmp_dir$(mktemp -d) trap rm -rf $tmp_dir EXITmktemp -d会在/tmp下生成一个随机命名的目录避免用固定路径导致多个实例互相覆盖也防止了路径被其他用户提前猜到带来隐患。trap清理保证脚本就算中途挂了垃圾也能被带走。单独看rm -rf $tmp_dir这个命令还有一个细节变量要用双引号包住否则目录名里有空格时会拆成多个参数甚至可能误删不该删的东西。关于rm -rf的风险后面安全部分还会展开。trap还可以监听信号trap echo 收到中断信号开始收尾; exit 1 INT TERM让脚本在被CtrlC时先做清理再退出。这种无论如何退出都先清理的思维是脚本健壮性的一个核心习惯建议把它内置到自己的脚本模板里。5. 实战拆解一个日志自动清理脚本5.1 需求描述与功能拆分前面讲了这么多理论现在用一个我真实场景里非常常见的例子把整个流程串起来。假设有一个业务日志目录/data/app/logs里面每天生成一个以日期命名的.log文件还可能有子目录。需求有三点删除30天前的日志文件释放磁盘空间删除前记录一份清理清单方便以后查账如果目录所在磁盘使用率超过80%输出告警信息并留下一条醒目的记录。这种需求如果直接写三坨命令也能完成大部分功能但可读性、安全性都差。按前面说的思路我先拆功能校验目录、执行清理、记录日志、检查磁盘水位。每个功能一个函数主流程简单清晰。5.2 完整脚本逐段解析先看完整代码再逐段解释关键处。这个脚本偏向通用教学你可以直接拿来改成本地路径使用。#!/bin/bash # 功能日志目录过期清理与磁盘水位检查 # 用法./clean_logs.sh [保留天数] [目标目录] set -euo pipefail LOG_DIR/data/app/logs KEEP_DAYS${1:-30} LIMIT80 CLEAN_LOG/var/log/clean_logs_history.log log_msg() { echo $(date %F %T) $* $CLEAN_LOG } check_dir() { if [ ! -d $LOG_DIR ]; then log_msg 目录不存在: $LOG_DIR exit 1 fi } disk_usage() { local used used$(df $LOG_DIR | awk NR2 {print $5} | tr -d %) if [ $used -ge $LIMIT ]; then log_msg 磁盘使用率超限: ${used}% ${LIMIT}% echo 警告: 磁盘使用率已达 ${used}% fi } do_clean() { local file while IFS read -r file; do [ -f $file ] || continue log_msg 删除过期日志: $file rm -f $file done (find $LOG_DIR -type f -name *.log -mtime $KEEP_DAYS -print0) } main() { check_dir do_clean disk_usage log_msg 清理任务完成保留天数${KEEP_DAYS} } main逐段来说。set -euo pipefail就是前面说的标配。其中-u在这里尤其重要因为脚本引用了KEEP_DAYS、LOG_DIR这类变量如果变量名写错或者赋值没成功-u会立刻让脚本报错而不是用空值一路带偏后面的逻辑。LOG_DIR和KEEP_DAYS是配置区。我把可变的配置集中在脚本顶部以后要改路径或天数只动这几行就行。KEEP_DAYS${1:-30}表示支持命令行参数指定天数不指定就默认30天。这种配置集中加参数覆盖的模式很实用。log_msg函数把所有运行痕迹追加到CLEAN_LOG。日志统一记录是运维脚本的底线出事后能回溯也是留痕思维的体现。函数里的$*会把所有参数合并成一行配合前面的日期时间戳记录的信息就很完整了。check_dir先验证目录存在。在脚本一开头就校验前置条件是避免任务做到一半才发现环境不满足的经典做法。disk_usage用df拿使用率再用awk取第二行的百分比字段用tr去掉百分号最后和LIMIT比较。这里一个细节是df的输出第一行是标题NR2才是目标文件系统的数据行。do_clean是整个脚本的核心。find $LOG_DIR -type f -name *.log -mtime $KEEP_DAYS查找指定目录下距离修改时间超过保留天数的.log文件。-mtime 30的意思是文件修改时间在30天之前。选项-print0让find以null字符分隔输出的文件名这是为了应对文件名里有空格或换行的极端情况。然后通过进程替换(...)把find的结果作为while循环的输入这样while循环跑在当前shell里不会遇到前面说的子shell变量丢失问题。循环体里先判断[ -f $file ]再删除主要是防止find结果因路径变化已经失效避免把不存在路径当普通参数处理。main函数把整个流程串起来脚本真正执行的地方就一行main。这样设计的好处是脚本的逻辑一目了然想单独调试某个环节可以临时注释掉main里的某一项或者直接拿出某个函数单独测试。5.3 接入crontab与运行验证脚本写好后用crontab -e编辑定时任务添加一行30 2 * * * /usr/local/bin/clean_logs.sh 30 /data/app/logs /dev/null 21这表示每天凌晨2点半执行一次第一个30是保留天数。定时任务里有一个经典坑cron执行环境非常精简PATH可能不包含脚本依赖的命令路径所以脚本里的命令最好写全路径或者调用时显式带上环境。我一般习惯在脚本里固定export PATH/usr/local/bin:/usr/bin:/bin避免cron环境下找不到命令。先手动执行一遍确认退出码为0再检查CLEAN_LOG里有没有正常记录。然后用find手动验证一下过期文件确实被清理了。最后再挂cron不要写完就直接挂上万一有bug定时任务会在凌晨悄悄帮你放大问题。6. 调试技巧与高频避坑速查6.1 调试三板斧bash -x、set -x、关键节点输出shell脚本出了问题怎么排查我用得最多的方法是bash -x script.sh直接以调试模式运行。这个模式下每条命令执行前都会把展开后的实际命令打印到标准错误前面带一个加号。看到加号后面的实际命令就能立刻发现变量展开是不是符合预期、某条命令是不是被拆成了两条。比如变量没加引号导致参数分裂在-x模式下会非常直观地看到命令参数被拆开。脚本内部也可以用set -x开、set x关把调试范围限制在某个区间echo 开始调试 set -x rm -f $file set x echo 调试结束这样做可以避免整个脚本的输出被调试信息淹没。还有一种低技术含量但极有效的方法在关键节点加echo输出。变量赋值后、分支进入前、循环每次迭代时各打一行带标记的输出比如echo DEBUG: file$file, count$count。配合bash -x基本能定位九成的逻辑问题。再有就是trap echo line:$LINENO, ret$? ERR每行出错时自动打印行号和退出码。这个对定位到底哪条命令挂了特别有帮助。需要留意ERR触发和set -e、命令返回非零等条件有依赖关系实际使用中配合bash -x效果更好。6.2 高频错误对照速查我整理了平时答疑时遇到最多的问题做成一个速查表能省不少排查时间。现象常见原因解决办法执行脚本提示Permission denied没有执行权限chmod x script.sh能执行但报command not found文件名含特殊字符或CRLF换行用file查看格式dos2unix转换变量值不对多出奇怪内容未加双引号被shell分词或扩展变量引用一律加双引号if判断总是走false方括号左右缺空格[ $a $b ]每个地方留空格循环后变量值没变子shell边界问题改用重定向或进程替换整数比较报语法错误用了 而不是-gt -lt数值比较用-eq -ne -gt -ltrm在某些文件上报参数过多find结果过多导致参数上限超限改用find -delete或分批处理脚本某个环节静默失败没开启set -e和pipefail脚本开头加set -euo pipefail这里面CRLF换行的问题值得单独说一句。在Windows上编辑过的脚本上传到Linux文件每行末尾会带着\r第一行shebang会变成#!/bin/bash\r系统会报bad interpreter错误。用file script.sh看一下显示with CRLF line terminators就知道是这个原因。解决用sed -i s/\r$// script.sh或者dos2unix命令。养成在Linux环境里编辑脚本、或者给编辑器设置好换行符的习惯能从根上避开这个问题。6.3 隐蔽风险rm -rf、通配符展开、eval与临时文件shell脚本因为可以直接执行任意系统命令安全上的坑比其他语言更需要警惕。首当其冲的是rm -rf这类危险命令。如果变量没设置好rm -rf $dir/里的dir是空字符串就会变成rm -rf /后果不堪设想。我的习惯是凡是涉及rm -rf先用[ -n $dir ]校验变量非空再用set -u兜底并且在执行前echo出完整命令供人工检查。虽然这次写的清理脚本用的是rm -f不带-r但任何删除操作都值得同样的谨慎。另一个隐蔽风险是变量里的通配符展开。如果文件名里带有*号不加引号的使用会把星号按通配符展开可能展开成一堆文件路径。这个问题在读取外部数据来源时尤其值得注意外部数据不可信你的引号就是防线。凡是对变量内容不确定默认加双引号这一条能挡住一大片问题。eval命令要尽量少用。eval会先把字符串拼接成命令再执行等于允许外部输入直接变成命令注入风险极高。在我处理的场景中绝大多数eval需求都可以通过关联数组、case分支或函数映射解决没有必要用eval。如果你确实绕不开至少确保eval的参数完全由脚本内部受控内容构成绝不让外部输入直接进eval。临时文件的安全也要注意。脚本里别用固定的/tmp/my_temp_file这种路径多个实例同时跑会互相覆盖别人也可能猜到路径做手脚。统一用mktemp或mktemp -d生成随机路径用完配合trap清理。关于脚本安全我的总原则是输入严格校验、输出路径明确、权限最小化、危险操作前置检查。这些习惯不复杂但能在关键时候把损失控制住。7. 脚本长期维护的几点习惯7.1 命名、注释与版本管理脚本命名要一看就知道用途clean_logs.sh、backup_mysql.sh、check_disk.sh都比test1.sh、new2.sh强一百倍。命名里建议包含动词表达做什么而不是是什么。脚本存放到统一目录比如~/bin或/usr/local/bin按功能分目录更清晰比如scripts/clean、scripts/backup、scripts/monitor。注释不需要泛滥但文件头的用途说明、每个函数的输入输出说明、以及关键代码行中为什么不这样写的说明值得认真写。我最重视的是为什么的注释一行代码解释不了为什么需要这段逻辑而这里不直接用rm是因为...这种注释能帮半年后的自己快速回忆当初的决策。版本管理方面哪怕只是个人脚本也建议纳入git。脚本会不断演进能回溯每一次改动、看到什么时候改了什么排查线上问题时价值巨大。如果嫌每次提交麻烦可以只在脚本改动达到一定规模时提交但至少要保留一份修订记录比如在文件头加一行修改历史2024-01-01 增加磁盘水位检查。我在自己的环境里是喜欢都用git管理的脚本库一个仓库分支按环境区分发布用标签。长期下来脚本库从一坨快看不懂的文件变成了有历史有说明的工具集维护成本明显下降。7.2 幂等设计与锁文件好的脚本应该可以重复运行而不产生坏结果这叫做幂等性。举例说初始化目录的脚本mkdir -p /data/app/logs无论执行多少次都不报错但如果是清空目录的脚本不加判断跑两遍可能就删掉了不该删的东西。设计脚本时问自己一个问题同一个脚本跑两遍、跑十遍结果应该一样吗如果答案应该是一样那就要确保关键步骤是幂等的。常见做法包括创建目录用mkdir -p而不是先判断再加mkdir写文件前先检查是否已存在或先备份删除操作前先确认目标存在且符合预期涉及状态变更的步骤用锁文件避免重复执行。锁文件可以简单用flock实现exec 9${script_dir}/.lock flock -n 9 || { echo 脚本已在运行; exit 1; }这样能防止同一个脚本并发执行互相干扰。定时任务和手动任务指向同一个脚本时这种做法尤其重要。7.3 写给下一个自己最后聊点个人的体会。脚本这行功夫不是把语法背熟就完了真正值钱的是踩坑之后的判断力知道什么场景该写脚本、什么场景不该上知道哪几行代码在运行时最容易出问题知道出问题后第一步应该看哪里。这些能力没有捷径就是多写多踩多总结。我现在的习惯是每次踩到一个不常见的坑就在自己的笔记里记一笔包括现象、原因、解决过程。久而久之这本笔记就变成了自己的排障字典。这也正是我写这第十六篇笔记的初衷很多经验不记下来三个月后我可能还是得靠重新踩坑才能想起来。如果你刚开始学shell脚本我的建议很朴素从改造自己昨天手工敲过的命令开始今天把它写成脚本明天给脚本加参数和错误处理后天再试着用函数重构一遍。不用特意去找复杂案例你日常的重复劳动就是最好的练习题。一次把一个小的需求做扎实脚本能力就是这么积累起来的。