news 2026/4/3 6:55:06

Bash脚本实战:从重复劳动中解放出来

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Bash脚本实战:从重复劳动中解放出来

写了十年运维脚本,最深的体会是:Bash不难,难的是写出不坑人的脚本

见过太多"能跑"但一改就崩的脚本,也踩过不少自己挖的坑。这篇把我积累的经验整理出来,都是血泪教训。


为什么还要学Bash

有人说现在都用Python了,Bash还有必要学吗?

我的看法是:轻量任务用Bash,复杂逻辑用Python

部署脚本、日志清理、批量操作这些,几十行Bash搞定的事,没必要起个Python环境。而且很多时候服务器上就只有Bash,你不得不用。


脚本开头:别省这几行

#!/bin/bashset-euo pipefail# 脚本说明# 作者:xxx# 日期:2024-12-29# 用途:xxx

set -euo pipefail这行很重要:

  • -e:命令失败立即退出,不会继续执行后面的
  • -u:使用未定义变量报错,避免typo导致的问题
  • -o pipefail:管道中任一命令失败,整个管道返回失败

没加这个的脚本,经常是前面出错了,后面还在跑,最后一看结果全乱了。

# 反面例子:没有 set -ecd/data/backup# 这个目录不存在rm-rf *# 灾难发生...

变量:引号是个大坑

永远用双引号包裹变量,这是血泪教训。

# 错误写法file_path=/data/my file.txtrm$file_path# 实际执行: rm /data/my file.txt (删了两个文件!)# 正确写法file_path="/data/my file.txt"rm"$file_path"

还有一个常见问题:

# 变量为空时的坑if[$name="admin"];then# name为空时语法错误echo"hi admin"fi# 正确写法if["$name"="admin"];thenecho"hi admin"fi# 更推荐用双括号if[["$name"=="admin"]];thenecho"hi admin"fi

字符串操作:不用awk也能干

# 获取文件名path="/data/logs/app.log"filename="${path##*/}"# app.logdirname="${path%/*}"# /data/logs# 字符串替换str="hello world world"echo"${str/world/bash}"# hello bash world (只替换第一个)echo"${str//world/bash}"# hello bash bash (替换所有)# 提取子串str="hello world"echo"${str:0:5}"# hello (从0开始取5个)echo"${str:6}"# world (从6开始到结尾)# 字符串长度echo"${#str}"# 11# 默认值echo"${name:-default}"# name为空用defaultecho"${name:=default}"# name为空用default,并赋值给name

这些操作比调用外部命令快很多,处理大量数据时差距明显。


数组:批量操作的基础

# 定义数组servers=("192.168.1.1""192.168.1.2""192.168.1.3")# 遍历forserverin"${servers[@]}";doecho"检查$server"ping-c1"$server"&>/dev/null&&echo"OK"||echo"FAIL"done# 数组长度echo"共${#servers[@]}台服务器"# 添加元素servers+=("192.168.1.4")# 取特定元素echo"第一台:${servers[0]}"# 取所有索引foriin"${!servers[@]}";doecho"索引$i:${servers[$i]}"done

实际应用:批量部署

#!/bin/bashset-euo pipefailservers=("web1""web2""web3")package="app-v2.0.tar.gz"forserverin"${servers[@]}";doecho"=== 部署到$server==="scp"$package""$server:/tmp/"ssh"$server""cd /tmp && tar xzf$package&& ./install.sh"echo"===$server完成 ==="done

条件判断:方括号的玄学

Bash的条件判断语法挺乱的,我整理个对照表:

# 字符串比较[["$a"=="$b"]]# 相等[["$a"!="$b"]]# 不等[[-z"$a"]]# 为空[[-n"$a"]]# 不为空# 数值比较[["$a"-eq"$b"]]# 等于[["$a"-ne"$b"]]# 不等于[["$a"-gt"$b"]]# 大于[["$a"-lt"$b"]]# 小于[["$a"-ge"$b"]]# 大于等于[["$a"-le"$b"]]# 小于等于# 或者用双括号做算术比较((a>b))((a==b))# 文件判断[[-f"$file"]]# 是普通文件[[-d"$dir"]]# 是目录[[-e"$path"]]# 存在[[-r"$file"]]# 可读[[-w"$file"]]# 可写[[-x"$file"]]# 可执行[[-s"$file"]]# 文件大小>0# 逻辑运算[[$a&&$b]]# 与[[$a||$b]]# 或[[!$a]]# 非

为什么推荐双括号[[]]而不是单括号[]

# 单括号的坑name=""[$name="admin"]# 语法错误:[ = "admin" ]# 双括号没问题[[$name=="admin"]]# 正常工作# 单括号要转义["$a"\>"$b"]# 字符串比较大于# 双括号不用[["$a">"$b"]]

函数:写可复用的代码

# 基本写法log(){locallevel="$1"localmessage="$2"echo"[$(date'+%Y-%m-%d %H:%M:%S')] [$level]$message"}log"INFO""脚本启动"log"ERROR""出错了"# 返回值check_service(){localservice="$1"systemctl is-active"$service"&>/dev/nullreturn$?# 返回上个命令的退出码}ifcheck_service nginx;thenecho"nginx 正在运行"elseecho"nginx 未运行"fi# 返回字符串(通过echo)get_ip(){hostname-I|awk'{print $1}'}my_ip=$(get_ip)echo"本机IP:$my_ip"

注意local关键字,函数内的变量如果不加local,会变成全局变量,很容易出问题:

# 坑count=10add_count(){count=20# 修改了全局变量!}add_countecho$count# 20,不是10# 正确做法add_count(){localcount=20# 局部变量}

参数处理:让脚本更专业

简单参数用位置变量:

#!/bin/bash# usage: ./deploy.sh <env> <version>env="${1:-prod}"# 第一个参数,默认prodversion="${2:-latest}"# 第二个参数,默认latestecho"部署$version$env环境"

复杂参数用getopts:

#!/bin/bashset-euo pipefailusage(){cat<<EOF 用法:$0[选项] 选项: -e, --env ENV 环境 (prod/test) -v, --version VER 版本号 -f, --force 强制执行 -h, --help 帮助 EOFexit1}# 默认值env="prod"version="latest"force=false# 解析参数while[[$#-gt0]];docase"$1"in-e|--env)env="$2"shift2;;-v|--version)version="$2"shift2;;-f|--force)force=trueshift;;-h|--help)usage;;*)echo"未知参数:$1"usage;;esacdoneecho"环境:$env"echo"版本:$version"echo"强制:$force"

错误处理:优雅地失败

#!/bin/bashset-euo pipefail# 清理函数cleanup(){localexit_code=$?echo"清理临时文件..."rm-rf"$tmp_dir"2>/dev/null||trueexit$exit_code}# 注册退出时执行trapcleanup EXIT# 错误处理error_handler(){echo"错误发生在第$1行"exit1}trap'error_handler $LINENO'ERR# 临时目录tmp_dir=$(mktemp -d)echo"临时目录:$tmp_dir"# 你的逻辑...

trap是个好东西,常用信号:

  • EXIT:脚本退出时
  • ERR:命令出错时
  • INT:Ctrl+C时
  • TERM:kill时

实战:日志清理脚本

#!/bin/bashset-euo pipefail# 配置LOG_DIR="/var/log/app"KEEP_DAYS=7MAX_SIZE_MB=100log(){echo"[$(date'+%Y-%m-%d %H:%M:%S')]$1"}# 删除N天前的日志clean_old_logs(){localcountcount=$(find"$LOG_DIR"-name"*.log"-mtime"+$KEEP_DAYS"|wc-l)if[[$count-gt0]];thenlog"删除$count${KEEP_DAYS}天前的日志"find"$LOG_DIR"-name"*.log"-mtime"+$KEEP_DAYS"-deleteelselog"没有需要删除的旧日志"fi}# 压缩大日志compress_large_logs(){localmax_size=$((MAX_SIZE_MB*1024*1024))whileIFS=read-r -d''file;dolocalsizesize=$(stat-f%z"$file"2>/dev/null||stat-c%s"$file")if[[$size-gt$max_size]];thenlog"压缩大文件:$file($((size/1024/1024))MB)"gzip"$file"fidone<<(find"$LOG_DIR"-name"*.log"-print0)}# 主逻辑main(){log"=== 日志清理开始 ==="if[[!-d"$LOG_DIR"]];thenlog"目录不存在:$LOG_DIR"exit1ficlean_old_logs compress_large_logs log"=== 日志清理完成 ==="}main"$@"

实战:服务健康检查

#!/bin/bashset-euo pipefail# 配置SERVICES=("nginx""mysql""redis")WEBHOOK_URL="https://your-webhook-url"CHECK_INTERVAL=60send_alert(){localmessage="$1"# 发送告警,根据实际情况对接企业微信/钉钉/飞书curl-s -X POST"$WEBHOOK_URL"\-H"Content-Type: application/json"\-d"{\"text\":\"$message\"}"&>/dev/null||true}check_service(){localservice="$1"ifsystemctl is-active"$service"&>/dev/null;thenreturn0elsereturn1fi}check_port(){localhost="$1"localport="$2"ifnc-z -w3"$host""$port"&>/dev/null;thenreturn0elsereturn1fi}main(){localfailed_services=()forservicein"${SERVICES[@]}";doif!check_service"$service";thenfailed_services+=("$service")fidoneif[[${#failed_services[@]}-gt0]];thenlocalmessage="[告警]$(hostname)服务异常:${failed_services[*]}"echo"$message"send_alert"$message"elseecho"所有服务正常"fi}# 单次检查或持续监控if[["${1:-}"=="--daemon"]];thenwhiletrue;domainsleep"$CHECK_INTERVAL"doneelsemainfi

调试技巧

# 方法1:打印执行的每条命令bash-x script.sh# 方法2:在脚本里开启set-x# 开启调试# ... 你的代码 ...set+x# 关闭调试# 方法3:只调试一部分#!/bin/bashecho"正常输出"set-x problematic_functionset+xecho"继续正常"# 方法4:打印变量echo"DEBUG: var=$var">&2

常见坑汇总

1. 空格问题

# 错误var="value"# 赋值等号两边不能有空格if[$a=$b]# 判断等号两边必须有空格# 正确var="value"if["$a"="$b"]

2. 路径中的特殊字符

# 永远用引号包路径forfilein"$dir"/*;doprocess"$file"done

3. 管道中的变量

# 坑:管道在子shell执行,变量改不了count=0catfile.txt|whilereadline;do((count++))doneecho$count# 还是0!# 正确:用进程替换count=0whilereadline;do((count++))done<file.txtecho$count# 正确的值

4. 命令替换中的换行

# 换行会变成空格files=$(ls)echo"$files"# 保留换行echo$files# 变成一行

总结

写Bash脚本的几个原则:

  1. 开头加set -euo pipefail,早发现问题
  2. 变量一律用引号包,避免空格和空值问题
  3. 用双括号[[]]做条件判断
  4. 函数内变量用local
  5. 写注释,一个月后你自己都看不懂
  6. 加日志,出问题好排查

Bash不是银弹,超过200行就该考虑Python了。但对于日常运维的小工具,Bash足够好用。


有问题评论区聊,我尽量回。

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

Java计算机毕设之基于springboot学院党建工作管理系统设计与实现springBoot的高校大学生党建系统设计与实现(完整前后端代码+说明文档+LW,调试定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/3/28 11:13:54

货分三次到、钱分四次收,进销存和财务的账为什么永远对不齐?

说到企业账对不齐&#xff0c;我见得太多了。销售那边在进销存系统里看到客户的钱已经收了两次&#xff0c;仓库那边看到货只到了一半&#xff0c;财务看报表直接炸毛。老板坐不住了&#xff1a;“我问你们&#xff0c;这单到底谁的钱和货没对上&#xff1f;”其实&#xff0c;…

作者头像 李华
网站建设 2026/4/3 3:08:46

无需手动配置!PyTorch-CUDA-v2.7开箱即用镜像详解

无需手动配置&#xff01;PyTorch-CUDA-v2.7开箱即用镜像详解 在深度学习项目开发中&#xff0c;最让人头疼的往往不是模型结构设计或训练调参&#xff0c;而是环境搭建——尤其是当你的同事跑得飞快的代码&#xff0c;在你机器上却报出 CUDA error: no kernel image is availa…

作者头像 李华
网站建设 2026/3/31 5:28:41

我发现RAG生成虚拟病例,乡村医生误诊率直降40%

&#x1f4dd; 博客主页&#xff1a;Jax的CSDN主页 当AI开始“翻译”中医&#xff1a;一场关于模糊语言的医疗革命目录当AI开始“翻译”中医&#xff1a;一场关于模糊语言的医疗革命 一、为什么中医需要AI&#xff1f;痛点太扎心 痛点1&#xff1a;病历“鸡同鸭讲” 痛点2&…

作者头像 李华
网站建设 2026/4/1 18:52:07

2小时,我实现了销售线索的自动分配与提醒!

“小王&#xff0c;上周分配给你的20条线索&#xff0c;跟进了几条&#xff1f;”“五六条吧&#xff0c;最近实在太忙了……”剩下的十几条线索呢&#xff1f;沉睡在系统里、无人问津。很多企业的销售线索都是这样&#xff1a;数量很多&#xff0c;但流转效率低&#xff0c;响…

作者头像 李华