不少人第一次认真学 bash,心里想的是“背几条常用命令就够了吧”。但真到了要写脚本的时候,才发现一头雾水:变量怎么赋值、参数怎么传、判断怎么写、循环怎么跑,每个点都要现查。这个“25 个常见 Linux bash 脚本示例”的项目标题,就是冲着这个痛点来的——不跟你讲枯燥的语法手册,直接给 25 个能落地、能改改就用的脚本场景。这篇博文我打算换个讲法,不把 25 个例子简单地堆成一列,而是拆成“为什么这么设计”“每类示例在解决什么问题”“实际跑起来要注意哪些坑”三个层次,让你是真的学会写脚本,而不只是复制粘贴。
25 个常见 Linux bash 脚本示例,写给刚上手的你
1. 内容整体设计与思路拆解
1.1 为什么“25 个示例”是最合适的入门粒度
我见过很多教程,一上来就扔 200 个代码片段,看两页就放弃了;也见过只讲 5 个例子的,看完还是不会自己写。25 个这个数量,恰好覆盖了 bash 脚本的核心语法骨架:变量、位置参数、用户输入、条件判断、常见循环、文件操作、文本处理、系统监控、以及最后的综合实战。
这个数量不是拍脑袋定的。如果你把日常脚本拆开看,会发现 80% 的操作都在重复使用几件事:拿输入(变量或者参数)、做判断(if/case/while)、处理文件(查找、改名、备份)、提取信息(grep、awk、sort)、执行系统命令。25 个示例正好能把这几大类全部覆盖到,又不会让初学者产生“我永远学不完”的挫败感。
更关键的一点是,这 25 个示例是“递进式”的。前 5 个解决“怎么把脚本跑起来”,中间 10 个解决“怎么处理文件和文本”,后面 10 个进入“系统和日常自动化”。你在阅读时不用一口气吃成胖子,每看一组,动手敲一遍,再往下走,基础会更扎实。我自己带团队时也这样要求新人:看完前 5 个示例,必须独立写出一个带参数的备份脚本;看完后面 10 个,能写一个日志统计小工具。这两关过了,才算入门。
1.2 这 25 个示例覆盖的知识点分布与学习路径
为了方便你对照自查,我先把 25 个示例涉及的知识点整理成一张思维地图:
| 阶段 | 示例编号 | 核心知识点 | 学习目标 |
|---|---|---|---|
| 基础流程控制 | 1-5 | 变量、$USER/$PWD、位置参数、read、if、for | 掌握脚本输入和输出 |
| 文件与目录操作 | 6-10 | 目录判断、tar 备份、find 查找、for 改名、du 统计 | 能搞定日常文件维护 |
| 文本处理与日志分析 | 11-15 | awk、grep -o、字符串替换、while read、sort/uniq | 能从日志中提取关键信息 |
| 系统状态监控 | 16-20 | uptime、free、pgrep、df、ps 排序 | 能监控服务器健康状态 |
| 自动化与综合实战 | 21-25 | 随机密码、进度条、批量 ping、环境自检、一键发布 | 能独立完成小型自动化任务 |
这个分布也反映了我推荐的进阶路线:先解决“怎么让脚本听话”,再解决“怎么让它处理一堆文件”,接着提高“怎么从文本里找金子”,然后才是“怎么盯住系统资源”,最后把它们组合成真正的自动化工具。很多时候你觉得 bash 脚本难,是因为变量和条件判断还没熟,就去啃 sed/awk 的高级写法,自然一碰就碎。
2. 开始写 bash 脚本前,先搞懂这三件事
2.1 脚本的可执行权限与 Shebang
新手经常卡在一个很尴尬的问题上:脚本明明写对了,执行时报permission denied。原因很简单,Linux 下要运行一个以./方式调用的脚本,必须先给脚本加上可执行权限。
chmod +x myscript.sh ./myscript.sh同时再看脚本的第一行,也就是 Shebang:
#!/usr/bin/env bash这行告诉系统用哪个解释器来运行文件。我推荐写成#!/usr/bin/env bash,而不是/bin/bash,因为不同发行版中 bash 的绝对路径不一定都在/bin下,用env去 PATH 里找更稳妥。实际工作中经常有人把 Windows 下的脚本直接传到 Linux 服务器,第一行带着\r,导致系统找不到解释器,报一堆莫名其妙的错,稍后我会在排错篇里细说。
2.2 变量、引号与空格这三个“隐形杀手”
bash 的语法自由度很大,但自由度也意味着容易踩坑。我总结了新手最容易犯的三类问题。
第一个坑:=号两边不能有空格。写 Python 或 JavaScript 习惯了的人,很容易写成:
name = "tom" # 错误!bash 会认为 name 是一个命令 name="tom" # 正确第二个坑:变量取值一定要用双引号包起来。假设有一个文件路径是my documents/backup.tar.gz,如果你写成rm -rf $path,bash 会把空格当分隔符,直接拆成两个参数,轻则误删,重则把目录搞乱。最稳妥的写法是:
path="/home/user/my documents/backup.tar.gz" rm -f "$path"第三个坑:命令替换应该用$( ),不推荐用反引号。反引号在嵌套时极易看错,而$( )的可读性和可嵌套性都好很多。
today=$(date +%Y%m%d) echo "今天是 $today"2.3 调试和防护手段:bash -n 与 set -euo pipefail
写脚本没人保证一次通过,所以调试是你的日常操作。我建议从一开始就养成两个习惯:
第一,每写完一个脚本,先做语法检查:
bash -n myscript.sh这条命令只检查语法,不真正执行。如果输出为空,说明语法没问题。
第二,在脚本头部加上防护选项:
#!/usr/bin/env bash set -euo pipefail我来拆解一下这行的含义:
set -e:遇到第一个错误就退出,避免在错误状态下继续执行造成更大破坏。set -u:使用未定义的变量时报错退出,这能救回无数人的手。set -o pipefail:管道中只要有一个命令失败,整个管道就视为失败。
这三个选项对我而言是“保命符”,尤其是set -u。你可能不理解,直到你某天写了一个rm -rf "$dir",而$dir因为变量拼写错误没被赋值——如果没有set -u,bash 会把它当成空字符串,执行rm -rf /,这是很多运维事故的根源。加了它,脚本会当场报错退出。
3. 25 个经典示例的实操拆解
3.1 第 1-5 个:变量、参数与基础流程控制
这个阶段解决的是“怎么让脚本接收输入、做出反应”。我用 5 个例子带你走一遍 bash 的基本骨架。
示例 1:Hello World 与内置变量
#!/usr/bin/env bash echo "Hello, World!" echo "当前用户: $USER" echo "当前目录: $PWD"这是所有脚本的起点。$USER和$PWD是 bash 内置的环境变量,直接拿来用就行。注意,变量名是用$引用的,但是赋值的时候不需要$。很多新手在这点上绕不清楚,记住一句话:赋值不带$,取值才带$。
示例 2:位置参数与默认值
#!/usr/bin/env bash name="${1:-world}" echo "Hello, $name!"这个脚本运行./greet.sh tom时输出Hello, tom!;不传参数时输出Hello, world!。${1:-world}这个写法是 bash 参数展开的一个经典技巧:如果$1存在就用$1,否则用默认值world。后面你写脚本时,凡是“参数可省”的场景,都能用这个套路。
示例 3:read 读取用户输入
#!/usr/bin/env bash read -p "请输入项目名称: " project mkdir -p "$project" echo "已创建项目目录: $project"read -p会在读取前打印提示文本,然后把你输入的内容存进变量。这个脚本虽然简单,但它体现了交互式脚本的编写思路。注意,mkdir -p后面我给变量加了引号,防止项目名里带空格导致路径被拆开。
示例 4:if/elif/else 判断参数数量
#!/usr/bin/env bash if [ "$#" -eq 0 ]; then echo "没有传任何参数" elif [ "$#" -eq 1 ]; then echo "只传了一个参数: $1" else echo "传了多个参数: $@" fi$#是参数个数,$@是所有参数列表。[ ]是test命令的简写,注意[后面和]前面一定要有空格,写成["$#" -eq 0]会直接报错。这是新手最常犯的语法错误,我几乎每周都会在社区回答里看到一次。
示例 5:for 循环展示文件名
#!/usr/bin/env bash for file in *.txt; do echo "找到文本文件: $file" done如果当前目录下没有.txt文件,*.txt不会被展开成空,而是原样输出字符串*.txt,严格来说会打印一行不存在的文件名。想严谨一点,可以在脚本开头加一句:
shopt -s nullglob这样当没有匹配文件时,循环体就不会执行了。这个小细节是很多人在批量处理文件时踩过的坑。
3.2 第 6-10 个:文件与目录日常操作
处理文件和目录是运维场景里出现频率最高的事情。这 5 个示例能解决大部分“备份、归档、清理、改名”的需求。
示例 6:判断目录是否存在,不存在则创建
#!/usr/bin/env bash if [ ! -d "/backup/logs" ]; then mkdir -p "/backup/logs" echo "目录 /backup/logs 已创建" fi-d是判断目录是否存在的条件表达式,!表示取反。这个模式非常常用,几乎每个需要写日志的服务脚本都会用到。你可以把它当成一个固定模板,哪里需要保证目录存在,就往哪里贴。
示例 7:按日期打 tar 包备份
#!/usr/bin/env bash today=$(date +%Y%m%d) tar -czf "backup_${today}.tar.gz" /home/user/project echo "备份完成: backup_${today}.tar.gz"这个脚本用$(date +%Y%m%d)拿到当天日期,拼到文件名里。好处是每天执行都会生成一个新归档,不会覆盖前一天的备份。backup_${today}这个写法要注意,如果写成backup_$today.tar.gz,bash 会把变量名解析成$today.tar.gz,因为today.tar.gz是一个合法变量名的一部分,这就是为什么要用花括号把变量名明确围起来。
示例 8:find 查找并删除 30 天前的日志
#!/usr/bin/env bash find /var/log/myapp -name "*.log" -mtime +30 -delete生产环境里日志轮转是必须做的事,不然磁盘迟早被撑爆。这里的-mtime +30意思是修改时间在 30 天前,-delete直接删除。但我强烈建议,第一次执行时先不要加-delete,而是用-print或-ls把匹配到的文件列出来看看,确认无误后再删。
find /var/log/myapp -name "*.log" -mtime +30 -print示例 9:批量把 .txt 重命名为 .md
#!/usr/bin/env bash for file in *.txt; do mv "$file" "${file%.txt}.md" done${file%.txt}是“从右边开始匹配并删除.txt后缀”的变量展开语法。这个操作批量改名时非常实用。同样的思路可以扩展到改前缀、加时间戳等场景。再次提醒,如果你开了nullglob会更安全,同时mv的两个参数都要加引号,防止文件名带空格时报错。
示例 10:统计当前目录下各子目录大小
#!/usr/bin/env bash du -sh /var/www/* 2>/dev/null | sort -rh | head -20du -sh输出每个目标的总大小,sort -rh按人类可读数字从大到小排序,head只取前 20 行。2>/dev/null的意思是吞掉错误输出,比如某些目录没有权限访问时不刷屏报错。这个用法在排查“哪个目录把磁盘吃满了”时属于必杀技。
3.3 第 11-15 个:文本处理与日志分析
日志和文本处理是 bash 脚本最显价值的地方。前面还只算体力活,这 5 个示例开始让脚本变得“聪明”。
示例 11:统计访问日志中排名前 10 的 IP
#!/usr/bin/env bash awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -10这个管道命令非常经典:awk 取第一列,也就是访问日志里的客户端 IP;sort 排序后让相同 IP 连续排列;uniq -c 统计重复次数;sort -rn 按次数倒序;head 取前十名。如果你第一次看到这个命令组合感到眼花,建议拆开一段一段加管道,一步步看中间结果,很快就能理解。
示例 12:从文本中提取所有 IP 地址
#!/usr/bin/env bash grep -oE '([0-9]{1,3}\.){3}[0-9]{1,3}' access.log | sort -ugrep -o只输出匹配的部分,-E启用扩展正则。([0-9]{1,3}\.){3}[0-9]{1,3}是一个粗略的 IPv4 匹配规则。sort -u去重,得到不重复的 IP 清单。这个脚本可以用于审计日志中的外部访问来源,也可以配合其他工具做进一步分析。
示例 13:字符串替换与截取
#!/usr/bin/env bash url="https://example.com/api/v1/users" domain="${url#https://}" echo "去掉协议后: $domain" version="${domain##*/}" echo "最后一段路径: $version"#用于从左边删除匹配的前缀,##是从左边删除最长匹配;%从右边删除匹配的后缀,%%从右边删除最长匹配。如果你经常处理 URL 或文件路径,这四个符号必须掌握。这就像 Python 里strip、split的轻量替代方案,在脚本里能省掉不少外部命令调用。
示例 14:while read 逐行处理文件
#!/usr/bin/env bash while IFS= read -r line; do echo "处理: $line" done < userlist.txtIFS=表示不重置字段分隔符,read -r防止反斜杠被转义。这两个细节保证每一行被“原样”读取,不会把行首空格吞掉,也不会把\n之类的转义变样。循环结束后用< userlist.txt把文件内容作为标准输入喂给循环。这是批量处理用户名单、服务器列表的标准写法。
示例 15:找出当前目录下最大的 10 个子目录
#!/usr/bin/env bash du -h --max-depth=1 . 2>/dev/null | sort -rh | head -10--max-depth=1的意思是不递归到更深的层级,只看当前目录下的一级子目录。如果你管理一个数据盘,想知道哪个项目占了最多空间,这个脚本两秒钟就能给你答案。配合计划任务,还能在磁盘快满时自动发提醒。
3.4 第 16-20 个:系统状态与资源监控
这 5 个示例直接与服务器健康状态挂钩。监控不一定非要上 Prometheus 这类重型工具,很多场景下,一个 bash 脚本加 cron 就足够用了。
示例 16:输出 CPU 平均负载
#!/usr/bin/env bash load=$(uptime | awk -F'load average:' '{print $2}') echo "当前系统负载: $load"uptime会输出系统运行时间和平均负载,awk -F'load average:'用字符串作为分隔符把后半段取出来。负载值一般有三个数字,分别对应 1 分钟、5 分钟、15 分钟。判断是否过载,不能只看 1 分钟值,要结合核数和 15 分钟趋势。
示例 17:内存使用率与告警
#!/usr/bin/env bash mem_total=$(free -m | awk 'NR==2{print $2}') mem_used=$(free -m | awk 'NR==2{print $3}') mem_percent=$((mem_used * 100 / mem_total)) echo "内存使用率: ${mem_percent}%" if [ "$mem_percent" -gt 90 ]; then echo "内存告警!请及时检查进程。" fi这里有一点必须说明:free输出里第二行的used列其实已经减掉了 buffer/cache 的一部分,如果你追求更准确的“真实占用率”,可能要按available来计算。但对于一个入门告警脚本,这个写法足够用。等你有需要时再去研究 free 命令的详细输出逻辑。
示例 18:检查 nginx 进程是否存活
#!/usr/bin/env bash if pgrep -x "nginx" >/dev/null; then echo "nginx 运行中" else echo "nginx 未运行,尝试启动..." systemctl start nginx fipgrep -x是精确匹配进程名,>/dev/null表示不把这个命令的输出显示到终端,只关心它的退出状态码。如果状态码是 0,说明进程存在。这个脚本可以挂在 cron 里每 5 分钟执行一次,做最基础的服务守护。
示例 19:磁盘分区使用率检查
#!/usr/bin/env bash used=$(df -h / | awk 'NR==2{print $5}' | tr -d '%') if [ "$used" -gt 80 ]; then echo "警告:根分区已使用 ${used}%" else echo "磁盘状态正常: ${used}%" fidf -h /拿到根分区信息,NR==2取第二行,第 5 列是已用百分比,结尾带着%符号,所以要用tr -d '%'把它删掉,这样后续才能做数字比较。-gt是大于的意思。这个模式是磁盘监控的基础模板,你可以照着扩展出内存、inode 等多项检查。
示例 20:列出占用内存最高的前 5 个进程
#!/usr/bin/env bash ps aux --sort=-%mem | head -6ps aux列出所有进程的详细状态,--sort=-%mem按内存占用从大到小排序。head -6是因为第一行是表头,真正取到的前 5 条才是数据。排查内存泄漏时,我会先跑这个脚本,再对比不同时间点的进程内存变化,判断哪个进程在持续增长。
3.5 第 21-25 个:自动化运维与综合实战
前面 20 个示例都是“零件”,最后 5 个开始“组装”。这也是你从“会写命令”走向“会写工具”的分水岭。
示例 21:生成随机密码
#!/usr/bin/env bash password=$(openssl rand -base64 12 | tr -d '\n') echo "新密码: $password"openssl rand -base64 12生成 12 字节随机数,并做 base64 编码。tr -d '\n'把换行符删掉。如果你不想依赖 openssl,也可以用/dev/urandom:
head -c 16 /dev/urandom | base64 | tr -d '=' | head -c 12两条都可以。如果你需要批量创建系统账户并下发随机密码,把这个脚本放进 for 循环里就能一次性生成几十个。
示例 22:用 bash 写一个简易进度条
#!/usr/bin/env bash for i in {1..10}; do printf "进度: " for j in $(seq 1 "$i"); do printf "#" done echo " $((i * 10))%" sleep 0.2 done这只是一个演示性质的进度条,真正要做命令行进度条有很多成熟工具,但理解这个双层循环结构很重要。外层的{1..10}是 bash 支持的序列展开,内层的seq用来控制#的数量。你以后写“批量处理 N 个文件,每个文件给个进度反馈”的逻辑时,思路就是从这里来的。
示例 23:批量 ping 测试多台主机连通性
#!/usr/bin/env bash hosts=("192.168.1.1" "192.168.1.2" "192.168.1.3") for host in "${hosts[@]}"; do if ping -c 1 -W 1 "$host" >/dev/null 2>&1; then echo "$host 可达" else echo "$host 不可达" fi donehosts=(...)定义数组,"${hosts[@]}"取出所有元素。ping -c 1只发一个包,-W 1等待 1 秒超时。与其手动一台台敲 ping,不如把这个命令挂到 cron 里每天跑一次,早上看一眼结果,哪台机器掉线一目了然。
示例 24:环境自检脚本
#!/usr/bin/env bash tools=(git node python3 gcc docker) for tool in "${tools[@]}"; do if command -v "$tool" >/dev/null 2>&1; then echo "$tool: 已安装 ($(command -v "$tool"))" else echo "$tool: 未安装" fi donecommand -v是查找命令路径的标准方法,比which更通用。这个脚本非常适合放在新服务器的初始化阶段,或者 CI 流程的第一步。它能快速告诉你这台机器缺哪些依赖,省去一个环境问题排查半天的时间。
示例 25:一键发布脚本
#!/usr/bin/env bash set -euo pipefail APP_DIR="/opt/myapp" cd "$APP_DIR" git pull origin main || { echo "拉取代码失败" exit 1 } if command -v make >/dev/null 2>&1; then make build fi systemctl restart myapp echo "发布完成: $(date)"这个综合脚本把很多技巧串到了一起:set -euo pipefail保证出错即停;git pull || { ... exit 1; }是“命令失败时执行分支”的写法;构建步骤用command -v判断有没有 make;最后重启服务并打印时间。写发布脚本最重要的原则是:一旦某个环节失败,绝不能让后面的步骤继续执行,否则会出大问题。我见过太多发布事故,都是因为脚本没加 error handler,代码都没拉下来,后面还硬要执行重启,结果服务直接挂了。
4. 常见问题与排查技巧实录
4.1 脚本“跑不起来”的五个典型原因
我把新手反馈最多的“脚本不工作”案例汇总成了一个速查表,帮你快速定位:
| 报错或现象 | 最可能的原因 | 解决方式 |
|---|---|---|
Permission denied | 脚本没有可执行权限 | chmod +x script.sh |
command not found | 第一行 shebang 有问题,或变量名写错 | 用bash script.sh直接调;检查 shebang |
syntax error near unexpected token | 脚本里有 Windows 换行符(CRLF) | sed -i 's/\r$//' script.sh |
| 变量输出为空 | 变量拼写不一致或赋值语句写错 | 用echo "DEBUG: $var"打印检查 |
| 命令明明存在,但脚本报找不到 | PATH 不包含该命令路径 | 不要假设环境,脚本里用全路径或command -v |
4.2 语法报错、CRLF 换行与运行环境的坑
Windows 上写脚本再传到 Linux 执行,是新手重灾区。Windows 记事本或部分编辑器使用的是\r\n作为换行符,而 Linux 只认\n。这个看不见的\r会让 bash 在执行第一行 shebang 时找到的是bash\r,直接报错,也可能导致脚本每个命令末尾都带着一个回车字符。
遇到这种情况,先用file命令确认:
file myscript.sh如果输出里出现with CRLF line terminators,就用下面的命令清理:
sed -i 's/\r$//' myscript.sh更省事的办法是安装dos2unix:
dos2unix myscript.sh还有一类问题是运行环境差异。例如脚本里#!/usr/bin/env bash,但系统里 bash 可能不在 PATH 中;或者你在一台 CentOS 上写的语法,拿到 Ubuntu 上没问题,但反过来就可能因为 sed/awk 版本不同而报错。我处理这类问题时,会先确认脚本要跑在什么系统上,再决定语法能写到多“花哨”。
4.3 我处理线上脚本问题的三段式排查法
在部署环境里出问题,没有编辑器可以慢慢调试,所以我的排查路径基本固定成三步。
第一步:复现并看完整报错。很多人只看报错的最后一行,比如unexpected end of file,然后就懵了。正确的做法是回到终端最上面,找到脚本实际执行到哪个命令时开始出错。报错信息前面通常会有脚本文件名和行号,先记住它。
第二步:用bash -n做语法校验。如果语法没问题,再执行bash -x script.sh。-x模式会把每个被执行的命令展开并打印出来,你可以清楚看到变量被替换成了什么值。很多时候,“不知道脚本里某个变量到底是什么”才是排查不动的根源,而-x一跑全都暴露了。
第三步:给关键节点插入调试输出。我一般会在可疑变量后面加一行:
echo "DEBUG: project=$project" >&2>&2表示把调试信息输出到标准错误,这样即使脚本把标准输出重定向到了日志文件,调试信息依然会在终端里显示。完成排查后,把这些 DEBUG 行删掉即可。
除了这三步,还有一个关于排错的独家心得:如果你发现一个脚本在手动执行时正常,放进 crontab 里却不工作,十有八九是环境变量问题。cron 的执行环境只有极简的 PATH,很多脚本里直接写的命令(比如python3)在 cron 下是找不到的。解决办法是在脚本开头显式设置 PATH:
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH"这个坑我踩过不止一次,现在每写一个要交给 cron 执行的脚本,第一件事就是把 PATH 补全。
还有个容易被忽略的问题:在脚本里用rm -rf这类高危命令时,一定要确认变量不为空。例如:
rm -rf "/backup/$project"如果$project因为某种原因为空,实际执行的是rm -rf /backup/,整个备份目录会被删光。这种灾难性误删,完全可以通过提前检查变量来避免:
if [ -z "$project" ]; then echo "错误:project 变量为空,已终止执行" >&2 exit 1 fi最后我再分享一个实用经验:学这 25 个示例最好的方法不是读,而是抄下来改着玩。把示例 7 的备份目录换成你自己的项目目录跑一遍,把示例 11 的日志路径换成你手边任何一个文本文件跑一遍,把示例 25 的去和自己的部署流程比一比。改着改着,你就会发现 bash 脚本不过是一堆命令的组合,而你已经能自己组合出需要的工具了。这 25 个示例练完,建议立刻挑一个自己最烦的重复性任务,给它写一个自动化脚本——那才是你真正入门的时刻。