news 2026/4/15 23:20:12

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/4/16 11:07:03

PyTorch-CUDA-v2.7镜像是否可用于语音识别系统

PyTorch-CUDA-v2.7镜像是否可用于语音识别系统 在当今智能语音技术飞速发展的背景下&#xff0c;构建高效、稳定的语音识别系统已成为AI工程实践中的核心任务之一。无论是智能助手、会议转录&#xff0c;还是实时字幕生成&#xff0c;背后都依赖于深度学习模型对音频信号的精准…

作者头像 李华
网站建设 2026/4/16 4:00:19

PyTorch-CUDA-v2.7镜像中导出实验报告用于团队协作

PyTorch-CUDA-v2.7镜像中导出实验报告用于团队协作 在AI研发团队日常工作中&#xff0c;一个常见的场景是&#xff1a;某位成员在一个“完美运行”的本地环境中完成模型训练&#xff0c;信心满满地将代码推送到仓库&#xff0c;结果其他同事拉下来一跑&#xff0c;却报出各种Im…

作者头像 李华
网站建设 2026/4/16 4:27:06

4 个近期 yyds 的 AI 开源项目,绝了。

01 谷歌开源 AI Agent 大杀器 谷歌刚刚开源了一个 AI Agent 神器&#xff1a;Gemini CLI&#xff0c;直接把自家最强的 Gemini AI 模型搬到了你的命令行里。 24 小时就斩获了 2W 多颗星星&#xff0c;相当火爆呀。 支持 Google 搜索联网、多模态内容生成、内置 MCP 支持、自…

作者头像 李华
网站建设 2026/4/16 4:10:16

计算机Java毕设实战-基于springboot+vue个性化电影推荐系统的设计与实现影视推荐系统的设计与实现【完整源码+LW+部署说明+演示视频,全bao一条龙等】

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

作者头像 李华
网站建设 2026/4/16 2:01:01

PyTorch-CUDA-v2.7镜像训练BERT模型实测性能对比

PyTorch-CUDA-v2.7镜像训练BERT模型实测性能对比 在当前大模型训练日益普及的背景下&#xff0c;如何快速构建一个稳定、高效且可复现的深度学习环境&#xff0c;已成为AI工程师和研究人员面临的核心挑战之一。尤其是在使用如BERT这类参数量巨大、计算密集的Transformer模型时&…

作者头像 李华
网站建设 2026/4/16 4:29:03

PyTorch-CUDA-v2.7镜像助力大模型Token生成效率翻倍

PyTorch-CUDA-v2.7镜像助力大模型Token生成效率翻倍 在大模型推理场景中&#xff0c;一个常见的尴尬局面是&#xff1a;硬件投入不菲&#xff0c;显卡动辄数万元&#xff0c;但实际跑起 Llama 或 Qwen 这类主流模型时&#xff0c;GPU 利用率却常常徘徊在 30% 以下。更令人头疼的…

作者头像 李华