
讲一个让我抓狂的下午。我在维护着一个已有三年时间的公司部署脚本, 该脚本是用Bash进行编写的, 其长度大概为200行。某次需要给这个脚本添加一个小功能, 即「在失败的时候回滚到上一个版本」, 我当时认为半个小时就能够将其搞定。结果我改了三个小时。并非是逻辑具备复杂性, 而是Bash将我从事的每一步操作都狠狠地按在地面之上摩擦。存在变幻莫测的变量类型陷阱, 有着对空格极为敏感的状况, 存在那些条件判断时所遵循的诡异规则, 有着错误处理方面呈现出的反人类设计, 还有字符串进行拼接时遭遇的引号仿若噩梦的境况……在修改完毕之后, 我望着这多达200行的代码, 做出了一项决定: 全部予以重写。过后的一天, 我耗费了 4 个小时完成了对它的重新编写, 新产生的版本脚本有 280 行, 相较于 Bash 要长出 40% , 然而其可读性提高了 5 倍, 新增的功能从本来的几小时缩减到了几分钟。今儿个说说, 我基于何种缘由要脱离 Bash , 以及 Bash 在哪些情形之下仍会被保留着。Bash 把我逼疯的几个时刻不是Bash不行, 它于50行以内的脚本里头堪称无敌, 问题在于大部分真实场景之中, 脚本会逐渐变长, 一旦超过100行, Bash的每一个“特性”均化成阻碍。时刻 1变量没有类型全是字符串COUNT10COUNT$((COUNT 1)) # 11COUNThello # 现在 COUNT 是字符串将其改写为, 等于, 左括号, 计数, 加, 一, 右括号, 井号, 报错冒号, 你好冒号, 找不到, 命令句号。# 程序崩了你看着报错很懵Bash之中所有的变量皆是字符串, 那被你视作数字的事物, 有可能陡然间转变为字符串, 于编译期以及运行期均不会对你作出提醒, 起码要运行至那一行的时候才会有所报错, 然而Bash会令你对自己产生怀疑, 仿佛身处迷雾困局之中。时刻 2空格敏感到要命# 这是赋值NAME张三# 这是命令调用(因为等号两边有空格)NAME 张三# → 报错: 找不到 NAME 命令# if 语句里:if$NAME 张三# 中括号两边要空格if$NAME 张三# 报错if$NAME张三# 也错(逻辑变了)# 数组取值echo ${arr} #echo ${arr } # 错echo $arr # 取 $arr 然后输出字符串写Bash之际, 你百分之三十的时间用于处理空格调试, 代码看似无误, 运行却出问题, 需反复核查每个空格, 标点符号。时刻 3错误处理是地狱# 默认行为:命令失败也继续往下跑mkdir /tmp/wat先将 /tmp/wat/ 进行复制操作, # 上一步存在着有可能失败的情况, 而这一步存在着有可能成功的情况, 同时也存在着有可能失败的情况。echo OK # 永远会打这一句,不管中间有没有错# 标准做法:开 modeset -euo# -e 任一命令失败就退出# -u 用未定义变量报错# -o 管道任一环节失败就算失败# 但 set -e 有大坑:有些场景它不生效check {在名为 log.txt 的文件里, 使用 grep 程序去查找包含ERROR 的内容, 若查找不到也算作(把 grep 退出码 1 当作)是失败 的情况, 这种失败 情况会触发 set -e 执行退出的操作。check # 你可能并不想因为没找到 ERROR 就退出bash的错误处理并非是关于写不写错误处理的问题, 而是即便写了也难以做到万无一失, 除使用set -e之外还需要补充的设置有trap, 还需要补充的内容是|| true, 还需要补充各种各样的边界判断。时刻 4数据结构匮乏Bash 4 有 array但用起来非常诡异-A useruser张三user30# 取值echo ${user}# 遍历(看清这个语法)for key in ${!user}; doecho $key ${user}done是什么意思呢? 那个“${!user}”, 它所代表的含义是, 去获取名为“user”的这个具有关联性的数组当中的所有的键。# 第一次写这种代码要查 5 次文档想有嵌套结构map of map、list of map吗? 直接把Bash抛一边去, 压根表达不了, 这可是它的硬伤啊。时刻 5字符串处理鬼画符# 截取字符串NAME打出, 把${NAME%_*}的内容显示出来, # hello (在去掉最后一个 _ 之后)。echo ${NAME#*_} # worldecho ${NAME//l/L} #echo ${NAME:6:5} # world# 这种语法写一遍能记住的我服你同样的事name 将name进行通过下划线来划分为多段的操作, 然后输出划分后的结果打印出来, # hello。打印, 名字, 通过下划线进行分割, # 世界。print(name.(l, L)) #print(name) # world哪个易读哪个难读不用我多说。那个 200 行 Bash 改成 之后来对比一下我那段代码的核心部分。#!/bin/bash# Bash 版:部署 失败回滚set -euomyapp/opt/${}$1# 备份当前版本${}除以2, 将结果赋值, 若出现错误信息重定向到/dev/null不可见文件, 否则输出空字符串。if-z $; thenecho 首次部署fi# 拉新版本mkdir -p ${}//${}wget -q ${}/${}.tar.gz \-O /tmp/${}.tar.gz || {echo 下载失败; exit 1tar, 将 /tmp/ 下以 ${} 命名的.tar.gz 文件解压, 解压到 ${} 下的 ${}的目录中。# 切换软链接ln -sfn ${}//${} ${}/.newmv -T ${}/.new ${}/# 重启服务${} || {echo 重启失败,回滚...if-n $; thenln -sfn $ ${}/${}fiexit 1# 健康检查sleep 5for i in {1..5}; do如果, 使用curl工具, 以静默模式, 访问端口8080的路径, 其输出结果重定向到/dev/null设备中, 那么。echo 部署成功exit 0fisleep 2doneecho 健康检查失败,回滚# ... 重复的回滚代码exit 1版#!/usr/bin/env部署脚本,失败自动回滚time.from Pathsys myapp Path(f/opt/{})定义函数运行命令项并需核对是否正常 - 运行由列表形式呈现的命令, 同时设置核对为真, 该过程返回某种特定结果。运行命令, 进行检查时参数设为检查, 等于真值, 文本值为真值。def () - str | None:link / 若存在链接的某种操作结果, 那就取该链接的某种操作后的名称, 若不存在链接的某种操作结果, 那就是无。def (: str): / /.mkdir(True, True)print(f下载 {}...)tar Path(f/tmp/{}.tar.gz)..(f{}/{}.tar.gz,tar,run(将“tar”, 采用“xzf”方式, 对“str(tar)”进行操作, “-C”后接“str()”。print(切换软链接...)link / / .newif .():.().().(link)def ():run(, ,def (5, 2) - bool:time.sleep(3)for i in range():try:with ..(:8080/, 2) as r:if r. 200:Trueas e:“空格健康检查第, 我加一的结果次失败冒号太空格, e”。time.sleep()Falsedef (prev: str):print(f回滚到 {prev}...)link / if link.():link.()link.( / / prev)()def main():if len(sys.argv) 2:print(用法: .py )sys.exit(1) sys.argvprev ()try:()()if not ():raise (健康检查失败)print(f部署成功: {})as e:print(f部署失败: {e})if prev and prev ! :(prev)sys.exit(1)if :main()版的好处一目了然啥时候 Bash 还是首选不是我讲的要全面去禁用Bash, 在下面这些场景当中, Bash仍然属于最佳可选的选择范畴:1. 具有较少行数的, 可以直接在Bash中运用的, 简洁直白的命令组合限定在10行以内。# 这种不要写ps -a -- -q | xargs rm2. 环境较简单之地域, 容器入口还有所涉及的系统启动脚本, 仅 Bash 层面便可满足相关所需啦3. 一次性的运维操作临时跑一下就丢没必要写 。4. 位于CI/CD步骤当中的那个被称作「胶水」的东西, 它存在于CI里多个步骤的组合之内, 把Bash直接书写在yaml里面是最为便利的。判断的标准是, 这段脚本在以后会不会遭受反复修改, 会不会需要展开测试, 会不会有别人接手去维护呢? 倘若这三者之中有一个答案为“要”, 那就予以考虑。要是这三者的答案均为“不要”, 那么Bash是完全没问题的。一些迁移技巧Bash 转 的过程中有几个等价替换很常用# Bash: ls *.txtglobfiles glob.glob(*.txt)# 或更现代from Path# Bash: rm -rf /tmp/foo.(/tmp/foo, True)对打开文件“file”中的每一行, 若该行中存在“ERROR”, 则对其进行1的累加, 最后求总和。使用Bash, 查找。匹配名为“*.log”的文件, 文件修改时间大于7天。from Pathtimenow time.time()当下的时间减去, 进程状态统计信息如果大于, 七乘以八万六千四百秒这样的情况。用.open(out.tar.gzw:gz)这种方式, 以t作为操作对象。t.add(dir/)# Bash: curl., json对..()进行读取操作, 将读取结果通过json.loads函数处理后赋值给data。对所有shell操作而言, 标准库基本都做了覆盖了。刚开始写的时候要多写一点, 等习惯之后就没问题。收尾那次, 完成了从 Bash 的更改之后, 我们组, 其他几个 Bash 脚本, 也就是数据库备份脚本、日志归档脚本、监控采集脚本, 也都相继迁移过来了。虽然迁移成本并非处于较低水平, 平均每个脚本需要耗费两至三个小时, 然而之后维护成本会呈断崖式下降, 菜鸟也能够上手, 尽管没人乐意去学习Bash那些怪异扭曲的语法, 不过一周时间便能够写得像模像样。最后引用一句我所看到的话语: 「Bash的语法乃是1970年代的设计, 而另个语法是1990年代的设计。倘若你在2025年依旧运用1970年代的事物去书写复杂逻辑, 那么你或许在浪费自身的时间。」。改为 100 行的 Bash 代码, 进行修改, 或许这就是你为自己未来三年减轻负担的, 最具性价比的一项投资?