news 2026/9/4 18:56:30

写了 200 行 Bash 脚本之后,我决定全改成 Python

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
写了 200 行 Bash 脚本之后,我决定全改成 Python

讲一个让我抓狂的下午。

我在维护着一个已有三年时间的公司部署脚本, 该脚本是用Bash进行编写的, 其长度大概为200行。某次需要给这个脚本添加一个小功能, 即「在失败的时候回滚到上一个版本」, 我当时认为半个小时就能够将其搞定。

结果我改了三个小时。

并非是逻辑具备复杂性, 而是Bash将我从事的每一步操作都狠狠地按在地面之上摩擦。存在变幻莫测的变量类型陷阱, 有着对空格极为敏感的状况, 存在那些条件判断时所遵循的诡异规则, 有着错误处理方面呈现出的反人类设计, 还有字符串进行拼接时遭遇的引号仿若噩梦的境况……在修改完毕之后, 我望着这多达200行的代码, 做出了一项决定: 全部予以重写。

过后的一天, 我耗费了 4 个小时完成了对它的重新编写, 新产生的版本脚本有 280 行, 相较于 Bash 要长出 40% , 然而其可读性提高了 5 倍, 新增的功能从本来的几小时缩减到了几分钟。

今儿个说说, 我基于何种缘由要脱离 Bash , 以及 Bash 在哪些情形之下仍会被保留着。

Bash 把我逼疯的几个时刻

不是Bash不行, 它于50行以内的脚本里头堪称无敌, 问题在于大部分真实场景之中, 脚本会逐渐变长, 一旦超过100行, Bash的每一个“特性”均化成阻碍。

时刻 1:变量没有类型,全是字符串

COUNT=10

COUNT=$((COUNT + 1)) # 11

COUNT="hello" # 现在 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" # 永远会打这一句,不管中间有没有错

# 标准做法:开 mode

set -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 user

user="张三"

user=30

# 取值

echo ${user}

# 遍历(看清这个语法)

for key in "${!user}"; do

echo "$key = ${user}"

done

是什么意思呢? 那个“${!user}”, 它所代表的含义是, 去获取名为“user”的这个具有关联性的数组当中的所有的键。

# 第一次写这种代码要查 5 次文档

想有嵌套结构(map of map、list of map)吗? 直接把Bash抛一边去, 压根表达不了, 这可是它的硬伤啊。

时刻 5:字符串处理鬼画符

# 截取字符串

NAME=""

打出, 把${NAME%_*}的内容显示出来, # hello (在去掉最后一个 _ 之后)。

echo ${NAME#*_} # world

echo ${NAME//l/L} #

echo ${NAME:6:5} # world

# 这种语法写一遍能记住的我服你

同样的事:

name = ""

将name进行通过下划线来划分为多段的操作, 然后输出划分后的结果打印出来, # hello。

打印, 名字, 通过下划线进行分割, # 世界。

print(name.("l", "L")) #

print(name) # world

哪个易读哪个难读,不用我多说。

那个 200 行 Bash 改成 之后

来对比一下我那段代码的核心部分。

#!/bin/bash

# Bash 版:部署 + 失败回滚

set -euo

="myapp"

="/opt/${}"

="$1"

# 备份当前版本

=${}除以2, 将结果赋值, 若出现错误信息重定向到/dev/null不可见文件, 否则输出空字符串。

if

-z "$"

; then

echo "首次部署"

fi

# 拉新版本

mkdir -p ${}//${}

wget -q "${}/${}.tar.gz" \

-O /tmp/${}.tar.gz || {

echo "下载失败"; exit 1

tar, 将 /tmp/ 下以 ${} 命名的.tar.gz 文件解压, 解压到 ${} 下的 ${}的目录中。

# 切换软链接

ln -sfn ${}//${} ${}/.new

mv -T ${}/.new ${}/

# 重启服务

${} || {

echo "重启失败,回滚..."

if

-n "$"

; then

ln -sfn $ ${}/

${}

fi

exit 1

# 健康检查

sleep 5

for i in {1..5}; do

如果, 使用curl工具, 以静默模式, 访问端口8080的路径, 其输出结果重定向到/dev/null设备中, 那么。

echo "部署成功"

exit 0

fi

sleep 2

done

echo "健康检查失败,回滚"

# ... 重复的回滚代码

exit 1

版:

#!/usr/bin/env

"""部署脚本,失败自动回滚"""

time

.

from Path

sys

= "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 = / ""

= / ".new"

if .():

.()

.()

.(link)

def ():

run(

"", "",

def (=5, =2) -> bool:

time.sleep(3)

for i in range():

try:

with ..(":8080/", =2) as r:

if r. == 200:

True

as e:

(“空格健康检查第, 我加一的结果次失败冒号太空格, e”)。

time.sleep()

False

def (prev: str):

print(f"回滚到 {prev}...")

link = / ""

if link.():

link.()

link.( / "" / prev)

()

def main():

if len(sys.argv) < 2:

print("用法: .py ")

sys.exit(1)

= sys.argv

prev = ()

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 rm

2. 环境较简单之地域, 容器入口还有所涉及的系统启动脚本, 仅 Bash 层面便可满足相关所需啦!

3. 一次性的运维操作:临时跑一下就丢,没必要写 。

4. 位于CI/CD步骤当中的那个被称作「胶水」的东西, 它存在于CI里多个步骤的组合之内, 把Bash直接书写在yaml里面是最为便利的。

判断的标准是, 这段脚本在以后会不会遭受反复修改, 会不会需要展开测试, 会不会有别人接手去维护呢? 倘若这三者之中有一个答案为“要”, 那就予以考虑。要是这三者的答案均为“不要”, 那么Bash是完全没问题的。

一些迁移技巧

Bash 转 的过程中,有几个等价替换很常用:

# Bash: ls *.txt

glob

files = glob.glob("*.txt")

# 或更现代

from Path

# Bash: rm -rf /tmp/foo

.("/tmp/foo", =True)

对打开文件“file”中的每一行, 若该行中存在“ERROR”, 则对其进行1的累加, 最后求总和。

使用Bash, 查找。匹配名为“*.log”的文件, 文件修改时间大于7天。

from Path

time

now = time.time()

=

当下的时间减去, 进程状态统计信息如果大于, 七乘以八万六千四百秒这样的情况。

用.open("out.tar.gz""w:gz")这种方式, 以t作为操作对象。

t.add("dir/")

# Bash: curl

., json

对..("")进行读取操作, 将读取结果通过json.loads函数处理后赋值给data。

对所有shell操作而言, 标准库基本都做了覆盖了。刚开始写的时候要多写一点, 等习惯之后就没问题。

收尾

那次, 完成了从 Bash 的更改之后, 我们组, 其他几个 Bash 脚本, 也就是数据库备份脚本、日志归档脚本、监控采集脚本, 也都相继迁移过来了。

虽然迁移成本并非处于较低水平, 平均每个脚本需要耗费两至三个小时, 然而之后维护成本会呈断崖式下降, 菜鸟也能够上手, 尽管没人乐意去学习Bash那些怪异扭曲的语法, 不过一周时间便能够写得像模像样。

最后引用一句我所看到的话语: 「Bash的语法乃是1970年代的设计, 而另个语法是1990年代的设计。倘若你在2025年依旧运用1970年代的事物去书写复杂逻辑, 那么你或许在浪费自身的时间。」。

改为 100 行的 Bash 代码, 进行修改, 或许这就是你为自己未来三年减轻负担的, 最具性价比的一项投资?

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

URP渲染流程定制:用Renderer Feature打造美漫风格角色

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 18:53:03

基于GLM 5.2的Hugging Face模型安全审计实践:防御秘密模型攻击

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 18:50:41

89C51最小系统AD设计:原理图PCB工程实践指南

简介&#xff1a;本资源是一套面向单片机初学者与硬件开发者的89C51最小系统开发板完整AD设计工程&#xff0c;解决入门者缺乏规范参考设计、难以理解经典51架构硬件实现的核心痛点。压缩包共9个文件&#xff0c;包含Altium Designer原生原理图&#xff08;.SchDoc&#xff09;…

作者头像 李华
网站建设 2026/9/4 18:47:46

Unity消融效果Shader实现:动态着色与边缘高亮全解析

怪物溶解、角色被烧成灰烬、物体随时间剥落&#xff0c;这类效果在项目里通常需要一段“消融”过渡来衔接死亡与残留物出现的节奏。之前我带的小组做技能演示时&#xff0c;需求方给的关键词就三个&#xff1a;Unity、动态着色、消融效果。听起来简单&#xff0c;真做起来发现难…

作者头像 李华
网站建设 2026/9/4 18:47:25

RSTP协议PA机制详解:快速收敛原理与华为交换机配置实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 18:46:34

手把手:论文的图表与正文怎么分步对应编排

图表和正文对不上号&#xff0c;是论文返修的高频理由&#xff1a;辛辛苦苦画好的图&#xff0c;要么放错了位置&#xff0c;要么正文压根没提到&#xff0c;要么正文写的数字和图里对不上。这篇不讲空道理&#xff0c;按 7 步给你一套能直接照做的编排流程——从要不要配图、选…

作者头像 李华