“Tool文件夹”这四个字,我盯着它看了很久。这可能是开发者、运维、甚至普通办公党电脑里最不起眼,却最能体现“生产力”的一个目录。我自己的Tool文件夹从最早只有两三个散落的.sh脚本,发展到今天成为一个覆盖备份、巡检、日志清理、快速诊断的“瑞士军刀库”,中间踩过的坑、趟过的雷,比很多正式项目的上线经历都精彩。这篇文章就是把这几年的积累做一个完整的复盘,从目录怎么分,到脚本怎么写,再到那些文档里绝对不会告诉你的报错排查,一次性讲透。无论你的Tool文件夹目前是一堆杂乱无章的下载文件,还是已经有了雏形但不成体系,这篇文章都值得认真看一遍。
1. 目录结构设计:别让瑞士军刀变成一堆废铁
很多人拿到一把瑞士军刀,第一反应是“哇,好多工具”,然后放在抽屉里吃灰。Tool文件夹也一样,如果你只是把脚本、工具、配置一股脑丢进去,那它很快就变成一个谁都看不懂、谁都不想碰的垃圾场。我最早就是这么干的,直到有一次急需一个半年前写过的临时脚本,我翻了二十分钟没找到,最后发现它藏在某个以“新建文件夹(3)”结尾的目录里,那一刻我决定重构整个结构。
1.1 我踩过坑之后定下的最终结构
以一个典型的个人工作台为例,我的Tool文件夹长这样:
~/tool/ ├── bin/ # 可执行入口,放软链或封装命令 │ └── tl # 统一命令入口,指向 scripts/ 下的脚本 ├── scripts/ # 核心脚本区,所有功能代码都在这里 │ ├── backup_config.sh │ ├── cleanup_logs.sh │ ├── sysinfo.sh │ └── netcheck.sh ├── docs/ │ ├── README.md │ └── tool_manual.md └── logs/ # 工具运行日志,按日期归档这个结构看起来简单,但每个目录的存在都有它的理由。
scripts/是整个工具库的心脏,我要求里面每个脚本只干一件事,并且干到极致。比如backup_config.sh只负责备份环境配置,cleanup_logs.sh只负责清理过期日志,绝不在一个脚本里同时做两件事。为什么这么做?因为单一职责的脚本太好维护了,出问题的时候只需要看那几十行代码,不需要在一千多行的大杂烩里找bug。
bin/目录是给“用户”用的,这个用户可能是未来的你。我不会直接让你去bash scripts/backup_config.sh,而是做一个叫tl的统一入口。打个比方,scripts是工具箱里的各个工具,bin就是那个让你一眼能看到所有工具的挂板。
docs/是很多技术人员最容易忽略的部分。我见过太多人写完脚本就扔在那里,三个月后连自己都看不懂当初为什么要加那个参数。这个目录就放一份简明手册,每个脚本的用途、参数、输出格式,不超过三行说明。
logs/则是运行痕迹,不管是脚本成功的输出,还是失败的报错,都会追加到这个目录下。排查问题的时候,没有日志等于没有证据。
1.2 目录设计三个原则
在确定这个结构之前,我试过按语言分(bash/、python/、go/),也试过按项目分,最后发现都不好使。按语言分的问题在于,一个脚本可能用shell启动、用Python做数据处理、再用awk过滤结果,你该放哪个目录?按项目分的问题在于,工具和项目一耦合,换一个项目就用不上了。
经历过这些摸索之后,我总结出三个原则:
第一,按“动作”分类,不按“对象”分类。备份是一个动作,清理是一个动作,获取系统信息是一个动作。它取代的是“我要处理nginx配置”、“我要处理数据库日志”这种按对象划分的思路。动作是通用的,对象是易变的,通用性才是一个工具库的生命力。
第二,按“场景”命名,不按“实现方式”命名。sysinfo.sh就比get_cpu_mem_disk.sh好,前者是一个稳定场景,后者是三个易变要素的拼接。今天你要加一个磁盘IO监控,改的是sysinfo.sh内部,外部调用完全不受影响。
第三,预留一个“垃圾场”。我自己维护一个不放进版本管理的scratch/目录,专门放那些“先跑通再说”的实验脚本。等实验稳定了,再把它标准化并迁移到核心目录。这既保持了主目录的整洁,又不会扼杀灵感的火花。
2. 核心脚本实战拆解:每把刀都需要打磨
目录结构是骨架,脚本就是血肉。这一节我会拿出我工具库里使用频率最高的四个脚本,每一行代码都解释清楚为什么这么写。这些脚本都有一个共同特点:简洁、健壮、可复用,复制到任何一台同构环境的机器上都能直接跑。
2.1 配置备份脚本:最不起眼但救命次数最多
这个脚本是我重装系统、更换电脑时最大的救星。它做的事情非常简单:把散落在各个位置的配置文件打包压缩,按日期归档,并保留最近30天的备份。
#!/usr/bin/env bash set -euo pipefail BACKUP_ROOT="$HOME/backups" TIMESTAMP=$(date +"%Y%m%d_%H%M%S") BACKUP_DIR="$BACKUP_ROOT/config_$TIMESTAMP" mkdir -p "$BACKUP_DIR" # 备份常用的配置文件 cp -r "$HOME/.config/nvim" "$BACKUP_DIR/nvim" cp -r "$HOME/.config/kitty" "$BACKUP_DIR/kitty" cp "$HOME/.zshrc" "$BACKUP_DIR/zshrc" cp "$HOME/.gitconfig" "$BACKUP_DIR/gitconfig" # 打包压缩并删除原始目录 tar -czf "$BACKUP_DIR.tar.gz" -C "$BACKUP_ROOT" "config_$TIMESTAMP" rm -rf "$BACKUP_DIR" # 清理30天前的过期备份 find "$BACKUP_ROOT" -name "config_*.tar.gz" -mtime +30 -delete echo "backup done: $BACKUP_DIR.tar.gz"第一行#!/usr/bin/env bash和直接写#!/bin/bash的区别在于,env会去PATH里找bash,兼容性更好。第二行set -euo pipefail很多人一开始看不懂,它其实是四件事的组合:-e表示任何一条命令出错就退出,-u表示使用未定义变量时报错,-o pipefail表示管道中只要有一个命令失败整个管道就算失败。这三者加起来,就是“宁可错杀一千,不能放过一个错误”。
时间戳格式%Y%m%d_%H%M%S我特意设计成无特殊字符的纯数字加下划线。为什么不用2025-06-15 12:30:00这种格式?因为冒号和空格在Linux文件名里都需要转义,纯数字格式可以直接用于排序和比较。
压缩后rm -rf "$BACKUP_DIR"会不会不安全?会,但前提是变量值没被正确设置。这里依赖的是-u参数,如果$BACKUP_DIR未定义,脚本会直接退出而不会执行删除命令。另外,find的-mtime +30代表修改时间超过30天的文件,注意这个值可以根据实际场景调整,我自己的习惯是保留30天,因为有些配置的旧版本可能在两周后才暴露出问题,这时候还有机会回退。
2.2 日志清理脚本:一个高效的自动化流程
服务器和本地应用的日志增长速度远超想象,如果不加清理,一个不写日志滚动策略的应用能在半年内塞满整个磁盘。这个脚本的核心逻辑极简,但工程上所有该考虑的细节都有。
#!/usr/bin/env bash set -euo pipefail LOG_DIR="${1:-/var/log/myapp}" DAYS="${2:-7}" if [ ! -d "$LOG_DIR" ];then echo "error: $LOG_DIR does not exist" >&2 exit 1 fi find "$LOG_DIR" -type f -name "*.log" -mtime +"$DAYS" -exec rm -v {} \; echo "cleanup done. removed log files older than $DAYS days in $LOG_DIR"注意第一处关键设计:${1:-/var/log/myapp}允许从命令行传入第一个参数,如果没传就使用默认值。这意味着这个脚本可以复用给任何日志目录。参数默认值这块,我吃过不小的亏。早期版本里没有设置默认值,调用的时候忘了传参,脚本就把当前目录当成了日志目录去清理,差点把源码删了。更安全的做法其实是参数校验,我在4.1节还会细说。
第二个关键点是find的参数顺序。-name "*.log"意味着只清理log后缀的文件,不碰其它类型,-mtime +7表示只看7天前的文件,-exec rm -v {} \;逐条处理找到的文件。这里如果用-delete会更简洁,但-exec rm -v的优势是能打印出每个被删除的文件名。对于可能影响数据的操作,留痕永远比整洁重要。
rm -v的-v参数很多人会忽略,我强烈建议保留。生产环境上清理日志时,你应该明确知道删了谁、在哪里删的。如果担心误删,可以先去掉-exec,让find只打印匹配的文件列表,人工过目一遍再执行。
2.3 系统信息快照:你可以随时回溯
不在现场却要知道一台机器的过去几小时发生了什么,这是运维里很常见的诉求。sysinfo.sh做的就是这样一个系统快照——把当前的时间、负载、内存、磁盘、关键进程和网络连接一次性抓到日志目录,供事后分析。
#!/usr/bin/env bash { echo "==== $(date) ====" echo "hostname: $(hostname)" echo "kernel: $(uname -r)" echo "cpu load: $(uptime)" echo "memory:" free -h echo "disk:" df -h echo "listening ports:" ss -tlnp 2>/dev/null || netstat -tlnp } | tee -a "$HOME/tool/logs/sysinfo_$(date +%Y%m%d).log"这个脚本的一个亮点是用大括号把多个命令的输出聚合在一起,再统一通过管道交给tee -a。-a参数表示追加,不覆盖历史记录。也就是说,你连续跑这个脚本多次,日志文件里会有多个时间点的快照,方便对比。
为什么要获取这些信息?因为它们可以回答绝大多数基础故障场景:是CPU负载高还是内存不足?是磁盘满了还是端口被占?这些都是先于业务层排查的第一步。ss -tlnp列出当前监听的TCP端口,2>/dev/null是为了在没有权限查看进程名时不输出大量报错,|| netstat -tlnp是降级方案,老一点的系统可能没有ss命令,这时候自动切到netstat。
这个脚本我喜欢配上cron使用,比如每30分钟自动跑一次,生成一个当日快照文件。如果某天凌晨3点服务异常,翻出那个时间点的快照,内存、磁盘、网络一目了然,不用靠猜。
2.4 网络连通性检查:先保底再定位
ping不通和网站打不开是完全不同的两件事,前者是网络层问题,后者可能是应用层问题。这个脚本用最直接的方式帮你做第一层区分。
#!/usr/bin/env bash set -uo pipefail HOSTS=("8.8.8.8" "1.1.1.1" "example.com" "github.com") for host in "${HOSTS[@]}"; do if ping -c 1 -W 2 "$host" >/dev/null 2>&1;then echo "OK $host" else echo "FAIL $host" fi done # 检查 HTTP 服务是否响应 curl -s -o /dev/null -w "http_code: %{http_code}\n" --max-time 5 https://www.example.com这里特别注意,我没有加-e参数,因为网络检查的特性就是“允许失败”,失败也需要记录并继续检查下一个。这里对set -euo pipefail做了调整,只保留-uo pipefail,这个细节说明工具链必须理解标志位的含义,按需选用,而不是机械套用。
为什么选择这四个检查目标?8.8.8.8和1.1.1.1是公共DNS,用来确认基础外网连通性;example.com是跨境站点连通性;github.com则更偏向开发者场景。你可以根据自己的工作性质替换成内网网关、云厂商的DNS、公司门户站点等。-W 2表示超时时间为2秒,不用默认的10秒,避免脚本卡顿太久。curl的--max-time 5同理,超时即放弃。
3. 让工具库“活”起来:从脚本集合到统一工具箱
有了干活的脚本,接下来要解决的是“怎么顺手地调用它们”。直接bash /home/yourname/tool/scripts/backup_config.sh这种长命令,用一两次就有挫败感。真正好用的工具库,应该在几秒钟内完成调用,最好还有一个让人看一眼就懂的命令。
3.1 统一命令入口:用Makefile或软链接打造“一键”体验
我选择了最简单直接的方式:在~/tool/下放一个Makefile,把日常操作变成make backup、make clean、make sysinfo这种天然自带语义的命令。
.PHONY: backup clean sysinfo netcheck backup: bash scripts/backup_config.sh clean: bash scripts/cleanup_logs.sh sysinfo: bash scripts/sysinfo.sh netcheck: bash scripts/netcheck.sh all: @echo "Usage: make [backup|clean|sysinfo|netcheck]"为什么是Makefile不是别的?因为它零依赖,任何Linux发行版和macOS都自带make。Python的invoke、Node的npm scripts也很好,但要额外装运行时,对小工具库来说太笨重了。
除了Makefile,我还把这几个脚本软链到~/bin/下:
ln -s ~/tool/scripts/backup_config.sh ~/bin/tl-backup ln -s ~/tool/scripts/cleanup_logs.sh ~/bin/tl-clean ln -s ~/tool/scripts/sysinfo.sh ~/bin/tl-sysinfo ln -s ~/tool/scripts/netcheck.sh ~/bin/tl-netcheck在~/.bashrc或~/.zshrc里确保export PATH="$HOME/bin:$PATH"。这样随便在哪个终端窗口,敲tl-sysinfo就能看到系统快照,比make sysinfo更快。
用统一前缀tl-在这里起了大作用。敲命令的时候,输入tl-再按Tab,所有工具一次列出来,不用记具体脚本名。
3.2 每把刀配一本说明书:文档和代码同等重要
代码维护者是未来的自己,而不是别人,所以写文档是给自己的投资。我的docs/tool_manual.md格式相当固定,每个脚本用一张小表描述清楚:
# backup_config.sh 用途:备份常用配置文件 用法:bash scripts/backup_config.sh 参数:无 输出:$HOME/backups/config_YYYYmmdd_HHMMSS.tar.gz 特别说明:保留最近30天,删除前请确认备份包可正常解压有朋友会问:“脚本里都写了注释,为什么还要单独一份文档?”因为注释只能说明“这行代码做了什么”,说明不了“为什么要做”。比如备份脚本里为什么要排除某个目录,只有需求文档和变更记录能讲清楚。
文档还有一个重要用途:当你三个星期没碰这些脚本再回来时,它让你用一分钟恢复上下文。为了充分利用这个价值,我会在每次修改脚本后同步更新对应文档,哪怕只是加了一个参数说明。这个习惯让我从“重新读代码才能改”变成“看文档就能知道差异在哪”。
3.3 预留扩展位:变成小型平台而不是死目录
工具箱最大的风险是有一天“装不下新工具了”——这里的装不下不是空间问题,而是结构不支持。如果每次新增脚本都要修改Makefile、修改文档、调整目录结构,那说明结构设计有问题。
我的扩展策略很简单:任意新脚本先放到scripts/,在Makefile里加一行,在docs/里加一段。整个过程不超过两分钟。另外,我特别强调“按场景拆分”的原则,永远不要让单个脚本膨胀成几百行的巨无霸。如果一个脚本开始超过200行,我就会审视:是不是该拆了?拆出来的公共函数是不是该放到lib/目录?
一个很实用的扩展实践是给工具库写一个帮助命令:
#!/usr/bin/env bash echo "Toolbox 常用命令:" echo " tl-backup 备份配置文件" echo " tl-clean 清理过期日志,参数: <日志目录> [保留天数]" echo " tl-sysinfo 系统信息快照" echo " tl-netcheck 网络连通性检查"这个help脚本不需要任何逻辑,只负责把每条命令的用途用一句话说明白。它就是整个瑞士军刀的“开箱示意图”。
4. 踩坑记录与排查技巧实录
任何工具库,用的时间长了,一定会遇到各种诡异的问题。这一节我不会讲“完美写法”,而是把我踩过的坑原原本本列出来。这些问题里,有一些是新手才会遇到的,有一些是写了几百个脚本的老手也会疏忽的。
4.1 最常见的五个坑
第一个坑:路径硬编码。早期脚本里到处是/home/username/config这种绝对路径,换一台机器、换一个用户,全部都要改一遍。正确做法是用$HOME、$(dirname "$0")来推导出脚本所在的目录,再基于这个目录构建其他路径。$(dirname "$0")的意思是获取脚本自身所在目录,在跨目录调用时无比重要。
第二个坑:没有给脚本加执行权限。很多脚本能跑,是因为你用了bash script.sh,一旦你直接./script.sh,就会得到Permission denied。这个问题我自己遇到过太多次了。解决方案是养成本身设置可执行权限的习惯:chmod +x script.sh。
第三个坑:在Windows上编辑过脚本,导致换行符是CRLF,拿到Linux上跑直接报/usr/bin/env bash: bad interpreter。这不是什么高深的问题,但特别隐蔽。我以前为这个问题折腾了一个多小时,最后发现是换行符在作怪。一行命令就能解决:
sed -i 's/\r$//' script.sh第四个坑:文件名带空格。rm $file和rm "$file"在文件路径没有空格时看起来一样,一旦路径变成my config file,前者会把一个文件路径拆成三个参数,结果就是找不到文件,甚至在极端情况下删错文件。我的规矩是:所有变量在命令行展开时都必须加双引号。
第五个坑:脚本遇到错误继续跑。我见过程序执行到一半,因为某个临时文件没生成,后续逻辑全用错误数据继续跑,最后产出一个看似正常其实满是问题的结果。set -e会强制脚本在遇到错误时退出,配合trap可以打印出错的行号:
#!/usr/bin/env bash set -euo pipefail trap 'echo "error at line $LINENO"' ERR4.2 问题速查与排查技巧:一张表和一串命令
我把上面遇到的问题整理成一张速查表,放在docs/目录下。遇到异常时先查表,效率比看完整日志高得多。
| 现象 | 原因 | 解决方案 |
|---|---|---|
| bad interpreter | 换行符是CRLF | sed -i 's/\r$//' script.sh |
| Permission denied | 脚本没有执行权限 | chmod +x script.sh |
| 换一个目录就跑不了 | 脚本内部用相对路径 | 脚本开头cd "$(dirname "$0")" |
| 文件名带空格被拆成多个 | 变量未加双引号 | 所有变量加"$VAR" |
| 脚本中途出错但看不到原因 | 缺少-x或日志 | bash -x script.sh或增加trap |
| 变量未定义导致危险操作 | 缺少-u保护 | 加set -u |
排查脚本还有一个实战技巧:先做语法检查再运行。bash -n script.sh只会检查语法,不执行任何命令,能让最基础的错误在动手之前暴露。逻辑层面,我经常用bash -x script.sh把每一步展开成一行打印出来,配合echo临时打点,几轮下来就能定位问题。
在给脚本加日志的时候,我有几句话想直接跟各位分享:日志不是越多越好,而是“关键步骤有迹可循,出错现场有据可查”。备份成功打印一行,备份失败打印错误原因,正常情况下别刷屏。日志要带上时间戳、脚本名、错误码。
4.3 让脚本天然带“防护栏”
如果说排查是事后补救,那么脚本设计时加一些防护栏就是事前预防。我强烈建议给所有可能误操作的脚本加“危险操作确认”。比如清理脚本,在执行删除前打印将要删除的文件列表,并要求输入yes确认:
echo "The following log files will be deleted:" find "$LOG_DIR" -type f -name "*.log" -mtime +"$DAYS" read -p "Type yes to continue: " CONFIRM if [ "$CONFIRM" != "yes" ];then echo "aborted." exit 1 fi多花这三行,就能少做很多次“删错文件”的噩梦。另外,备份脚本在压缩之后,我会顺手做一次校验:
tar -tzf "$BACKUP_DIR.tar.gz" >/dev/null echo "backup archive integrity check passed"tar -tzf只列内容不解压,执行成功说明压缩包没损坏,再执行删除操作也不迟。
5. 让Tool文件夹真正成为你的瑞士军刀
工具箱终究是个人的东西,每个人手里那把瑞士军刀,组合方式和使用习惯都不一样。但是有一些经验是通用的,比如:怎么保持工具库不腐化、怎么跟日常工作结合起来、怎么让它越来越顺手。
5.1 我的工具库使用习惯演化
最早我的工具库一个月才用几次,基本上只有“啊,又要登录服务器了”“啊,又需要跑个命令了”才会想起来。后来随着我不断往里加工具,使用频率越来越高,现在几乎每天都离不开它。
一个重要的转折点是给工具库加了“每日快照”的能力。我写了一个daily_report.sh,会自动跑系统信息、磁盘占用、关键服务状态检查,然后生成一份当日报告放到logs/下。就像给服务器和电脑做了一次体检。每天都有一份报告,倒逼我每天都打开一次工具库。当使用频率上去了,自然就能发现哪些功能还能优化、哪些脚本还有bug。
另一个人人都能复制的操作是给工具库加“索引”。我会在docs/README.md里维护一份清单,列出每个工具的名字、一句用途、最近一次修改日期。这个文件我建议每月抽十分钟更新一次,它能让你清醒地认识到:哪些工具是常用的,哪些其实根本一次都没用过。那些长期没用的,可以标记为废弃,下个月再确认一次就可以删了。
5.2 如何避免工具库慢慢变成垃圾堆
任何一个Directory,如果不加维护,最终归宿一定是垃圾堆。工具库最容易踩的坑是“什么工具都想往里塞”。今天看到一个大佬的awk一行流,收藏进Tool文件夹;明天自己写了个一键部署脚本,也往里丢。时间一长,Tool文件夹又成了一个“收藏夹”。
我的维护原则:一个脚本要进入Tool文件夹,必须满足“至少用过两次”这个条件。第一次用可能是灵光一现,第二次说明确实有通用价值。只跑过一次的脚本,放在scratch/或者干脆不放进来。这个原则听起来有点苛刻,但它保证了Tool文件夹里每一件工具都是值得信任的。
另外,一定要给工具库做版本管理。把整个~/tool/目录初始化成一个Git仓库,每次修改脚本、调整文档都提交一次。当然,仓库会包含配置备份或隐私信息,所以我把backups/、logs/都放进了.gitignore,只管理代码和文档本身。这样意味着什么呢?意味着当我把工具库改出一个新的bug,可以随时git diff看改了什么,git revert回滚之前的状态。
5.3 从个人工具到团队协作的思路升级
个人工具库用得顺手之后,很自然会想把它推广给团队用。这时候要做几件额外的功夫。
最重要的一件事是“环境变量参数化”。我的个人脚本里混杂了不少我自己的配置路径,如果直接丢给团队,一个人改一个路径,维护成本会非常高。团队版会给每个脚本都增加一个config.env,统一管理所有路径和参数,脚本本身不再出现任何硬编码信息。
第二件事是“输出格式规范化”。个人脚本可能打印“备份完成!”就完了,团队版需要约定统一的日志格式:时间戳、级别、模块、消息。这些输出可以被统一的日志采集体系吸收,便于集中查看和告警。让团队接受一个新工具最难的不是工具本身的学习成本,而是跟现有体系的融合成本。输出格式规范是最优先要解决的融合问题。
第三件事是“文档共建”。个人工具库的文档自己写自己看,团队工具库需要每个人都能持续维护。我会把文档和代码放在同一个仓库里,让新增、修改、文档更新在同一个Pull Request中完成。评审者看到代码改动的同时,一定会问:“文档更新了吗?”这个约束能保证工具库的健康状态。
小结
我从一个只有两个脚本的目录起步,用了几年时间,把Tool文件夹打磨成了真正意义上的“瑞士军刀库”。它帮我省下的时间,累计起来大概是一整个月的带薪假期。更重要的是,这个不断迭代的过程让我养成了几个终身受用的习惯:所有脚本默认加上set -euo pipefail,所有删除操作前必须有确认,所有工具必须有文档。我不建议你照抄我的任何脚本,但我真诚地建议你从今天开始,认真整顿一下自己的Tool文件夹。先不急着写新脚本,先把已有文件按“动作”重新摆好,写一份两三页的手册,然后在一个月内坚持使用它。一个月后回头看,你大概率会发现,这把瑞士军刀越来越趁手了。