在讨论软件工程能力时,许多人首先想到的是开发速度、代码质量或持续集成的效率。然而,这些能力背后隐藏着一个至关重要但常常被忽视的因素,那就是依赖关系管理。尽管依赖关系表面上看似简单,甚至显得微不足道,但精准且全面的依赖关系管理是构建高效、稳定的软件工程能力的基石。无论是在代码编写、构建过程,还是最终的产品交付中,依赖关系的管理贯穿于整个软件开发生命周期。然而,许多团队对这一关键点重视不足,导致工程能力削弱,项目质量不稳定。

依赖关系管理的关键要素

在软件构建过程中,依赖关系管理是不可忽视的核心因素,尽管团队往往不会在这个问题上投入足够的时间和精力。当提到依赖时,人们通常会想到软件类库和源码等。这些依赖的变化可能对最终的软件产物产生显著影响,不仅在功能和性能方面,还可能表现在诸如目录名称、时间戳等与功能无关的方面。如果依赖管理不当,可能导致软件制品无法正确构建或出现严重问题。

现代软件开发通常涉及多种编程语言,每种语言都有其特定的依赖关系管理工具和方法,这带来了跨语言依赖管理的挑战。例如,一个 Java 程序可能调用由 C++ 开发的动态库,而该动态库又依赖于 Rust 开发的类库。如何在这些不同语言和工具之间建立起全面且准确的依赖关系,是软件工程中必须解决的难题。

除了语言间的依赖关系,还有一些依赖关系是通过构建过程建立起来的,例如通过构建脚本。这些依赖关系通常不会由依赖关系声明文件来维护,而是隐藏在构建过程中的某些步骤中。此外,构建工具和环境等因素通常不被视为依赖,但实际上,它们的变化同样会影响软件制品的稳定性。如果工具缺失、配置错误或存在缺陷,软件制品可能无法正确构建,甚至质量会显著下降。因此,这些传统上不被视为依赖的开发工具也应纳入依赖管理的范畴。

从广义的依赖关系角度来看,依赖管理不仅包括软件库和源码,还涵盖编译器、构建工具、操作系统、系统类库、环境变量、构建脚本等所有可能影响构建结果的要素。基于这一视角,依赖锁文件(lock 文件)需要为每次构建生成,确保每一个构建产物都有其对应的依赖锁文件。这种全面记录所有影响因素的做法,类似于对软件制品进行全面的“白盒化”描述。如果依赖关系保持一致,构建结果也应保持一致;反之,若两个构建产物存在差异,它们的依赖声明必定有所不同。

依赖关系的准确性和全面性直接决定了构建工程的成败。错误的依赖关系可能导致构建失败,而未能全面管理的依赖关系则可能引发构建的不稳定性问题(如常见的“在我电脑上没问题”现象)。构建稳定的工程能力需要掌握准确而全面的依赖关系清单,以简化和优化复杂且成本高昂的工程实践,显著提升软件工程的整体质量和效率。

构建工程中的常见误区

依赖关系的最重要的一个应用场景就是构建。然而,许多团队往往忽视构建工程的重要性,轻视依赖关系管理,从而未能给予构建工程应有的关注。构建工程常被认为是简单且低技术含量的任务,因此通常交由新手程序员负责。然而,由于缺乏经验,这些新人难以为构建工程设计出合理的架构或实现高质量的解决方案。这种对依赖关系管理和构建工程的轻视,直接导致了构建脚本维护成本高、质量低下,流水线不稳定,构建过程频繁出错,甚至难以复现缺陷。即便是经验丰富的开发者,如果不重视构建脚本的质量,认为脚本的编写质量不如 Java、C++ 等代码重要,依然会导致上述问题的出现。工程能力不足的团队通常表现为构建时间过长、构建失败率高、难以定位和解决问题,且无法有效扩展和维护构建系统。这些问题的积累最终显著削弱了团队的整体工程能力。

此外,许多团队过度关注持续集成和流水线管理,甚至为适应流水线而调整构建工程。这种做法实际上是本末倒置。流水线的实现因持续集成环境和基础设施的不同而异,强行让构建去适应流水线只会增加对现有基础设施的耦合度,导致构建工程复杂化、维护成本上升,并使构建质量变得不稳定。相比之下,工程能力强的团队通常由经验丰富的开发者维护构建脚本。这些开发者深刻理解依赖关系管理的重要性,并具备全局视角,能够在设计和实现上做出合理的架构决策。这种做法不仅保障了构建工程的稳定性和高效性,还确保了依赖关系管理的精准性和全面性,从而提升了整体工程能力。此外,优秀的构建工程也具有很高的兼容性和可迁移性,能够轻松适应不同的持续集成流水线以及构建环境。

构建工程的核心角色

软件构建工程是整个软件工程能力的核心,决定了从源码到最终产品的全过程,包括代码检查、分析、编译、测试、打包和发布等任务。例如,在使用 Gradle 的情况下,构建任务涵盖了从代码检查 (lint)、编译 (compile, compileJava)、测试 (check, test) 到打包 (jar, war, package) 和发布 (publish, deploy) 的各个环节。这些任务之间存在紧密的依赖关系,而流水线的实现则是在调用这些构建任务,通过将任务分配给不同的流水线节点,实现高性能、高效率的流水线。

构建工程的质量直接影响着其他工程实践的有效性和质量。持续集成、流水线管理、构建集群、缺陷管理、增量构建、分布式构建、自动化测试和发布管理等工程实践都围绕着构建工程展开。如果构建中的测试极其不稳定或总是失败,即使流水线运行得再高效,其实际价值也会大幅降低。

依赖关系管理的实际应用

当通过精准且全面的依赖关系管理构建出稳定且可重复的构建时,可以在此基础上将其应用于更多场景。例如,可以根据不同的 CI 服务器自动生成构建流水线,实现高精度的增量构建;在代码提交时实时监测依赖变更,甚至在不实际构建的情况下精确对比不同构建产物之间的差异,从而快速识别依赖关系变化的来源。此外,这种依赖关系管理还可以用于漏洞追溯,精准识别漏洞的影响范围和链路,甚至能在秒级时间内分析出一个已知漏洞影响了哪些客户的哪些产品。这种管理方式同样适用于分析架构设计与实现的差异,进一步优化系统设计。

流水线节点的配置实际上反映了构建工程中的任务依赖关系,因此可以通过构建工程中的任务依赖关系推导出流水线结构,甚至自动优化流水线节点的配置。这不仅简化了流水线的设计过程,还确保了其与构建工程的紧密配合,从而最大限度地提高整体工程效率和稳定性。

结语

精准且全面的依赖关系管理不仅包括第三方库或源码的引用,还涵盖操作系统、编译器、构建环境等所有可能影响构建结果的因素。依赖关系是软件构建工程的核心,而软件构建工程又是整个软件工程能力的核心。只有通过正确理解和管理依赖关系,才能构建出高效、稳定的软件工程能力。从依赖关系的精准且全面的管理开始,确保一切有序进行,这正是高效、稳定的软件工程能力的基石。

写过很多 Bash 脚本的人都知道,Bash 的坑不是一般的多^。所以大家都认可应该用 Python 而不是 Bash 的一个理由就是 Bash 脚本不可靠,不仅运行不稳定,而且还很难维护。关于 Bash 脚本很难维护,在《关于 Bash 的 10 个常见误解》里面有提到,其实是很多人对于 Bash 的语法就根本不熟悉。连最常用的 if 表达式的语法都不甚了解,更何谈什么进程替代啊,或者来做Bash 脚本的单元测试呢?没有人会认为使用一个自己不熟悉的语言来做开发维护工作是一件容易的事情。

不过呢,就算是我们对 Bash 语法不熟悉,但我们只是来实现一个简单的功能。Bash 脚本也没写几行,甚至在命令行都一条一条的验证过,放到 Bash 脚本里面运行起来就是会莫名其妙的出错。有些在服务器上的出错根本没法儿在本地重现,本地测试的好好的,刚部署上去也好好的,一到晚上或者假期就是各种出错。所以,有人说这是因为 Bash 这个语言其实不严禁,导致其运行不可靠,不稳定,所以得用 Python。但同样的功能用 Python 实现起来的代码量比 Bash 多,所以只是为了个简单的功能,还是直接写到 Bash 脚本里面吧,在 Python 脚本里面用 os.command 把很多命令一个一个包装起来也是挺烦人的。

从一个简单的例子开始

下面我们就以一个简单的例子来看一下为什么 Bash 脚本会如此不可靠。用 Bash 脚本为 GNU/Linux 实现一个很常见的、也很简单的功能,重启 Tomcat 服务。

Tomcat 是 Java 开发里面很常见的一个服务器应用,安装和使用都很简单。只要系统上已经安装了对应的 JDK,配置好 JAVA_HOME 环境变量,把 Tomcat 下载回来后解压就可以使用了。在 Tomcat 的安装目录里面执行 ./bin/startup.sh 就可以启动 Tomcat 服务,运行 ./bin/shutdown.sh 就可以停止 Tomcat 服务。

如果你读到这里,请稍微暂停来思考一下:如果由你来实现「重启 Tomcat」这个脚本,需要考虑哪些情况呢?

重启 Tomcat 服务,无非就是先「停止」再「启动」就可以了。我们在命令行的 Tomcat 目录下执行了 ./bin/startup.sh 后,打开浏览器验证了 Tomcat 已经启动。然后再执行 ./bin/shutdown.sh,再次打开浏览器发现无法访问了。所以要重启 Tomcat 就是先后执行这两个命令就可以了。

但是有一个要考虑的,如果 Tomcat 已经停止了,再次执行命令来停止 Tomcat 会不会出错呢?我们在命令行下测试了反复执行 ./bin/shutdown.sh 之后,发现除了打印出一个无关紧要的可以被忽略的出错信息后,执行过程并不会出错。

看起来实现重启 Tomcat 这个功能的 Bash 脚本无非就是要做如下几步:1、切换到 Tomcat 的安装目录;2、停止 Tomcat;3、启动 Tomcat。

#!/bin/bash
export JAVA_HOME=/opt/jdk1.8.0_262
export PATH="$JAVA_HOME/bin:$PATH"
cd /home/chaifeng/apache-tomcat-8.5.57
./bin/shutdown.sh
./bin/startup.sh

为了防止 Bash 脚本没有获取到 JAVA_HOME 这个环境变量的设置,我们在脚本里面还额外设置了一下,这又能避免一些奇怪的场景下无法获得这个变量的问题。看起来这个脚本很完美了,在本地也执行了一下,确实如我们期望的 Tomcat 已经重启了。用 ps 命令也确认了重启前后的 java 进程 ID 也发生了变化。

但是当我们把这个脚本部署到服务器上以后,情况却不一样了,大部分时候这个脚本都无法重启 Tomcat。明明 Tomcat 已经运行,但是服务端口处于无法访问的状态,甚至还会出现好几个 Tomcat 进程。难道是 ./bin/shutdown.sh 不起作用吗?当我们登录到服务器上,用 kill -9 杀掉这些 java 进程后,手动执行命令启动 Tomcat,验证没有问题。然后再手动运行命令停止 Tomcat,发现也没问题。更诡异的是,不管我们在命令行反复测试多少次,这两个启动和停止命令都正常工作。

明明在命令行下测试过都正常工作的命令,只有放到 Bash 脚本里面执行才有问题。看来 Bash 脚本的执行确实有时候不知何故的会出问题。难道真的是 Bash 是个不严禁的语言,运行不稳定吗?

为什么连这个简单的 Bash 脚本都运行不稳定?

为什么在命令行下一条一条的执行 Bash 脚本中的命令都没有问题,但是放到一个脚本中执行却会出问题呢?这两种方式下的命令都一样,除了用 Bash 语言不严禁这个理由来解释外,好像也没什么其他原因了。

实际上,有一个很重要的因素很容易被我们所忽略,就是:两条命令执行之间的时间差

在命令行上手工执行 Tomcat 的停止命令 ./bin/shutdown.sh 后,到输入了启动命令 ./bin/startup.sh 准备按下回车键执行之间,已经过去了好几秒。即使我们是快速复用命令行的历史命令,也要耗费2、3秒。而放到一个脚本里面执行,前一个命令执行结束到下一个命令开始运行,中间大概只有几个毫秒的时间差。几秒与几毫秒的时间差的区别就是这个简单的重启 Tomcat 脚本运行不稳定的根本原因。

当执行了 ./bin/shutdown.sh 来停止 Tomcat,实际上只是给 Tomcat 服务器发送了一个「停止」的信号,然后命令就退出。但此时,Tomcat 收到了「停止」信号后开始运行停止前的清理任务,包括但不限于等待尚未结束运行的客户端连接、释放数据库资源、网络资源、清理临时文件等。所以 Tomcat 还在运行,并没有立刻停止。这个停止运行前的清理工作的耗费时间依据我们的项目不同可能会差别很大,从零点几秒到几十秒,甚至几分钟都有可能。所以,这里是第一个关键的地方,./bin/shutdown.sh 命令执行结束后,Tomcat 可能还在后台进行清理工作,而没有停止运行!

等我们输入了 ./bin/startup.sh 后,时间已经过去了几秒,这个时间差已经足够让 Tomcat 在大多数的情况下真正停止运行了。所以 Tomcat 就又正常启动了。

放在脚本中运行,由于两条命令之间的时间差可能只有几毫秒,这么短的时间不足以让 Tomcat 停止运行。当我们在本地测试运行这个 Bash 脚本时,停止命令和启动命令都已经先后结束运行后。此时在后台,前一个老 Tomcat 进程还尚未结束运行,一个新的 Tomcat 进程就已经启动。但这个新的 Tomcat 可能还需要几秒钟来初始化资源,才能够开始绑定服务端口,这又为老 Tomcat 进程的停止争取到宝贵的几秒时间。当这个新的 Tomcat 结束了资源初始化后开始绑定服务端口的时候,老 Tomcat 可能已经释放了这个服务端口并停止运行。所以在本地测试时,这个重启 Tomcat 的 Bash 脚本表现的非常完美。

当部署到服务器上以后,Tomcat 服务可能会因为资源使用很多而需要更久的时间来释放资源,仅仅几秒是不够的。这就使得当一个新的 Tomcat 结束了资源初始化后,由于老的 Tomcat 服务还没有结束资源释放且没有释放服务端口,导致新的 Tomcat 会因为服务端口被占用而启动失败。

这就是为什么这么简单的一个重启 Tomcat 的 Bash 脚本会出现运行不稳定的原因。而且也不会因为我们用 Python 重写这个 Bash 脚本就可以解决这个问题。

让这个简单的例子变的稳定

既然找到了原因,那就好说了,启动 Tomcat 之前先把现有的 Tomcat 进程杀掉。因为 Tomcat 本身是一个 Java 应用,所以也就是杀掉 java 进程。

现在的问题是「如何杀掉进程」。有人会说这很简单啊,用 kill -9 就可以杀掉进程。没错,这样的确可以。而且如果大家去搜索「Linux 杀掉进程」,很多的搜索结果都提到要用 kill -9 来杀掉进程。为什么要使用选项 -9 呢?有人会说,因为有时候直接用 kill 命令杀不掉进程,反复执行也杀不掉,用了选项 -9 之后就可以了。

事实上,kill -9是对系统危害最大的命令之一。也有人会说,我一直用 kill -9 来杀进程,也从来没破坏过系统啊。这是典型的幸存者偏差,就跟开车不系安全带、骑车不带头盔一样,确实有很多人不系安全带不带头盔,也没见过出事儿。但是只要发生一次,那就是严重伤害。

直接用 kill 命令去杀一个进程,默认是给进程发送了 SIGTERM 信号,当进程收到这个终止信号后,可能会开始终止前的清理工作。当清理工作结束后,该进程就会自行终止,如果不需要清理就直接结束了。如果清理时间稍微长一些,就会让我们产生 kill 命令不工作的错觉。但我们可能只要稍微多等待几秒,进程就会结束了。而使用 kill -9 则类似于一枪爆头,没有给进程在自杀前善后工作的机会。如果进程打开了很大的文件,或者正在写入数据库。暴力终止进程的执行是很有可能会导致文件或者数据库的损坏。

作为一名称职的司机,我们一定会系安全带的。作为一名称职的运维工程师,一定不会滥用 kill -9 这个命令的。现在抬头看看你的周围,有多少不称职的运维工程师呢?

既然在执行了停止 Tomcat 的命令后,不能立即强行终止 java 进程,那就需要等待。很显然,我们不能预计要等待多久。如果固定的等待一个比较长的时间,而 Tomcat 早已经停止,这就会让别人觉得这个 Bash 脚本的性能太差了。现在我们修改一下这个重启 Tomcat 的 Bash 脚本:

#!/bin/bash
export JAVA_HOME=/opt/jdk1.8.0_262
export PATH="$JAVA_HOME/bin:$PATH"
cd /home/chaifeng/apache-tomcat-8.5.57
./bin/shutdown.sh
for ((i=0;i<60;i++)); do
    pgrep java || break
    sleep 1
done
pgrep java | xargs --no-run-if-empty kill -9
./bin/startup.sh

新增了一个等待循环,每秒钟都检查一下 java 进程,如果没找到就终止循环,最多等待 60 秒。退出循环后,再次查找 java 进程,这里用了 GNU xargs 的选项 -—no-run-if-empty 可以避免 xargs 没有从管道接受了数据而去执行 kill -9 命令,导致无谓的出错。

到这里,这个简单的重启 Tomcat 的 Bash 脚本看上去没啥问题了,不管是本地还是服务器上都可以正常的稳定的工作了。而且我们应该也要注意到,讨论的这个问题不管我们用 Python 还是 Bash 还是其他什么语言来实现这个功能,都同样是需要考虑的。要承认,这个造成 Bash 脚本运行不稳定的原因与 Bash 一点儿关系都没有。

这个简单的例子还没有结束

看到这里,或许有人会疑惑还需要考虑什么呢?停止 Tomcat 后,如果在 60 秒内 Tomcat 还没有停止就会强制停止,然后再启动 Tomcat。完美,没什么要考虑的了。

如果在系统上除了 Tomcat 这个 java 进程外,还有其他的 Java 进程,我们怎样确保这个重启脚本不会杀掉其他无关的 Java 进程呢?

如果 Tomcat 是以某个特定用户来运行,如何阻止其他用户无意中执行这个脚本呢?

如果系统上运行了多个 Tomcat 实例,如果确保这个重启脚本只会重启特定的 Tomcat 而不是其他的 Tomcat 服务呢?

如何能够让不同的 Tomcat 复用同一个重启脚本,而不用是为每个 Tomcat 的安装实例复制多份脚本呢?

如何让这个脚本自动判断 Tomcat 版本并选择对应的 JDK?以避免使用错误的 JDK 来启动 Tomcat 呢?

如何让这个重启脚本自动判断当前目录是否位于一个 Tomcat 安装目录中,运行后重启的就是这个 Tomcat,这样就避免了每次使用可能需要传入 Tomcat 目录作为参数了。万一输入了错误的目录呢?

如何避免使用特权用户执行这个脚本来重启 Tomcat?否则不仅会引入额外的安全风险,也会让因一些文件所有者发生变化而导致非特权用户以后无法再管理 Tomcat。

如何让每次重新开机后可以自动执行这个脚本来重启 Tomcat?如果要做开机自动启动脚本,是不是还要考虑 System V 还是 Systemd ?

作为开机的自动启动脚本时,如何自动判断 Tomcat 的所属用户,并自动切换到该用户的权限下执行?这样可以避免无谓的启动配置。

如果 Tomcat 上部署的服务依赖其他的服务,比如 MySQL,如何让这个脚本自动判断依赖的服务已经运行?避免 Tomcat 启动后服务运行后失败

如何让重启脚本判断 Tomcat 重启后服务已经成功运行,否则将以某种方式发出通知?这样就避免了重启不成功,但是我们不知道。

如何避免这个脚本互斥运行?以防止这个脚本在不同的终端上分别同时运行而导致的运行冲突。

如何避免远程登录到一个服务器上运行了脚本,并确认 Tomcat 运行正常,结果退出登录后,Tomcat 服务也会终止运行的问题。

等等……等等……

为了写出一个稳定、可靠、易用的 Tomcat 重启脚本,我们有很多要考虑的事情。列出了这么多可能需要考虑的问题,有哪个问题是和 Bash 有关的?很显然没有。如果我们用 Python 去开发这样一个功能,是否也需要考虑这些问题呢?答案是肯定的。

结论,Bash 脚本不稳定的原因其实就是我们自己。用 Python 重写 Bash 脚本后,原先该有的问题还是存在,只是我们不能再怪罪是 Python 这个语言不严谨,Python 运行不稳定了。只能乖乖从我们自身去寻找问题,然后慢慢得以解决。

如果一开始我们就能够清楚的意识到我们对于一些基础技能的欠缺,就可以提前避免很多不该有的问题。比如对 Bash 的语法不熟悉,也存在很多的误解,那我们怎么能写好 Bash?缺乏对于操作系统的一些功能理解,比如滥用 SIGKILL 信号,不明白什么时候该忽略 SIGHUP 信号等等,那我们怎么能管理好系统?其次,我们缺乏一个系统的运维知识体系,认为运维工作就是去使用一个又一个的工具,不理解这些工具在运维知识体系中的位置。那我们如何能够有效的利用这些不同工具的组合来解决问题呢?再者,我们缺乏从全局系统角度去思考问题,解决了一个又冒出另一个。或者就没明白这些不同的问题之间可能存在一个共同的根因,总是就问题解决问题,那我们要浪费多少时间和人力才可以维护好一个系统?最后,做好运维工作,提升运维技能不只是运维工程师才需要掌握的。对于开发工程师也有提升自己工作效率,更有效的与运维团队协作和沟通,无论是对自己、对团队、对项目都有好处。

要想了解运维工作的知识体系以及技术脉络请看DevOps 技术栈。再看看你是否也有关于 Bash 的 10 个常见误解?要想写出可靠稳定的 Bash 脚本,那一定要写Bash 脚本的单元测试

为什么要为 Bash 脚本写单元测试?

因为 Bash 脚本通常都是在执行一些与操作系统有关的操作,可能会对运行环境造成一些不可逆的操作,比如修改或者删除文件、升级系统中的软件包等。所以为了确保 Bash 脚本的安全可靠,在生产环境中部署之前一定需要做好足够的测试以确保其行为符合我们的预期。

如何能够安全可靠的去测试 Bash 脚本呢?有人可能会说我们可以用 Docker 容器。是的,这样做即安全又方便。在容器隔离出来的环境中不用担心脚本会破坏我们的系统,而且也能非常简单的快速重建出一个可用的测试环境。

不过呢,请考虑以下的几个常见的场景:

场景一:在执行 Bash 脚本测试前,我们需要需要事先安装好所有在 Bash 脚本中会用到的第三方工具,否则这些测试将会因为命令找不到而执行失败。例如,我们在脚本中使用了 Bazel 这个构建工具。我们必须提前安装并配置好 Bazel,而且不要忘记为了能够正常使用 Bazel 还得需要一个支持使用 Bazel 构建的工程。

场景二:测试结果的稳定性可能取决于脚本中访问的第三方服务的稳定性。比如,我们在脚本中使用 curl 命令从一个网络服务中获取数据,但这个服务有时候可能会访问失败。有可能是因为网络不稳定导致的,也可能是因为这个服务本身不稳定。再或者如果我们需要第三方服务返回不同的数据以便测试脚本的不同分支逻辑,但我们可能很难去修改这个第三方服务的数据。

场景三:Bash 脚本的测试用例的执行时间取决于脚本中使用的命令的执行时间。例如,如果我们中脚本中使用了 Gradle 来构建一个工程,由于不同的工程大小 Gradle 的一个构建可能要执行3分钟或者3个小时。这还只是一个测试用例,如果我们还有20个或者100个测试用例呢?我们是否还能在几秒内获得测试报告呢?

即使使用了容器来执行 Bash 脚本测试,也一样无法避免上面的几个问题。环境的准备过程可能会随着测试用例的增多而变的繁琐,测试用例的稳定性和执行时长取决于第三方命令和服务的稳定性和执行时长,还可能很难做到使用不同数据来覆盖不同的测试场景。

对于测试 Bash 脚本来说,我们真正要验证的是 Bash 脚本的执行逻辑。比如在 Bash 脚本中可能会根据传入的参数来组合出内部所调用的命令的选项和参数,我们要验证的是这些选项和参数确实如我们预期的。至于调用的命令在接受了这些选项和参数后由于什么原因而失败,可能我们并不关心这所有的可能原因。因为这会有更多的外部影响因素,比如硬件和网络都是否工作正常、第三方服务是否正常运行、构建工程所需的编译器是否安装并配置妥当、授权和认证信息是否都有效、等等。但对于 Bash 脚本来说,这些外部原因导致的结果就是所调用的命令执行成功或者失败了。所以 Bash 脚本只要关注的是脚本中调用的命令是否能够成功执行,以及命令输出了哪些,并决定随后执行脚本中的哪些不同分支逻辑。

如果说我们就是想知道这个命令搭配上这些选项参数是否能按我们预期的那样工作呢?很简单,那就单独在命令行里面去执行一下。如果在命令行中也不能按预期的工作,放到 Bash 脚本里面也一样不会按预期的工作。这种错误和 Bash 脚本几乎没什么关系了。

所以,为了尽量去除影响 Bash 脚本验证的那些外部因素,我们应该考虑为 Bash 脚本编写单元测试,以关注在 Bash 脚本的执行逻辑上。

什么样的测试才是 Bash 脚本的单元测试?

首先,所有存在于 PATH 环境变量的路径中的命令都不应该在单元测试中被执行。对 Bash 脚本来说,被调用的这些命令可以正常运行,有返回值,有输出。但脚本中调用的这些命令都是被模拟出来的,用于模拟对应的真实命令的行为。这样,我们在 Bash 脚本的单元测试中就避免了很大一部分的外部依赖,而且测试的执行速度也不会受到真实命令的影响了。

其次,每个单元测试用例之间都应该是独立的。这意味着,这些测试用例可以独立执行或者被任意乱序执行,而不会影响验证结果。

最后,这些测试用例可以在不同的操作系统上执行,且都应该得到相同的验证结果。比如 Bash 脚本中使用了只有 GNU/Linux 上才有的命令,对应的单元测试也可以在 Windows 或者 macOS 上执行,且结果一致。

怎样为 Bash 脚本写单元测试?

与其他编程语言一样,Bash 也有多个测试框架,比如 BatsShunit2 等,但这些框架实际上并不能隔离所有 PATH 环境变量中的命令。有一个名为 Bach Testing Framework 的测试框架是目前唯一一个可以为 Bash 脚本编写真正的单元测试的框架。

Bach Testing Framework 的最独特的特性就是默认不会执行任何位于 PATH 环境变量中的命令,因此 Bach Testing Framework 非常适用于验证 Bash 脚本的执行逻辑。并且还带来了以下好处:

  • 简单
    什么也不用安装。我们就可以执行这些测试。比如可以在一个全新的环境中执行一个调用了大量第三方命令的 Bash 脚本。

  • 因为所有的命令都不会被真正执行,所以每一个测试用例的执行都非常快。
  • 安全
    因为不会执行任何外部的命令,所以即使因为 Bash 脚本中的某些错误导致执行了一个危险的命令,比如 rm -rf *。Bach 会保证这些危险命令不会被执行。
  • 与运行环境无关
    可以在 Windows 上去执行只能工作在 GNU/Linux 上的脚本的测试。

由于操作系统和 Bash 的一些限制,Bach Testing Framework 无法做到:

  • 拦截使用绝对路径调用的命令
    事实上我们应该避免在 Bash 脚本中使用绝对路径,如果不可避免的要使用,我们可以把这个绝对路径抽取为一个变量,或者放入到一个函数中,然后用 @mock API 去模拟这个函数。
  • 拦截诸如 >>><< 等等这样的 I/O 重定向
    是的,无法拦截 I/O 重定向。我们也同样可以把这些重定向操作隔离到一个函数中,然后再模拟这个函数。

Bach Testing Framework 的使用

Bach Testing Framework 需要 Bash v4.3 或更高版本。在 GNU/Linux 上还需要 Coreutils 和 Diffutils,在常用的发行版中都已经默认安装好了。Bach 在 Linux/macOS/Cygwin/Git Bash/FreeBSD 等操作系统或者运行环境中验证通过。
Bash v4.3+
Coreutils (GNU/Linux)
Diffutils (GNU/Linux)

安装 Bach Testing Framework

Bach Testing Framework 的安装很简单,只需要下载 https://github.com/bach-sh/bach/raw/master/bach.sh 到你的项目中,在测试脚本中用 source 命令导入 Bach Testing Framework 的 bach.sh 即可。

比如:

source path/to/bach.sh

一个简单的例子

与其它的测试框架不同,Bach Testing Framework 的每一个测试用例都是由两个 Bash 函数组成,一个是以 test- 开头的测试执行函数,另一个是同名的以 -assert 结尾的测试验证函数。

比如在下面的例子中,有两个测试用例,分别是
test-rm-rf
test-rm-your-dot-git

一个完整的测试用例:

#!/usr/bin/env bash
set -euo pipefail

source bach.sh # 导入 Bach Testing Framework

test-rm-rf() {
    # Bach 的标准测试用例是由两个方法组成
    #   - test-rm-rf
    #   - test-rm-rf-assert
    # 这个方法 `test-rm-rf` 是测试用例的执行

    project_log_path=/tmp/project/logs
    sudo rm -rf "$project_log_ptah/" # 注意,这里有个笔误!
}
test-rm-rf-assert() {
    # 这个方法 `test-rm-rf-assert` 是测试用例的验证
    sudo rm -rf /   # 这就是真实的将会执行的命令
                    # 不要慌!使用 Bach 测试框架不会让这个命令真的执行!
}

test-rm-your-dot-git() {
    # 模拟 `find` 命令来查找你的主目录下的所有 `.git` 目录,假设会找到两个目录

    @mock find ~ -type d -name .git === @stdout ~/src/your-awesome-project/.git \
                                                ~/src/code/.git

    # 开始执行!删除你的主目录下的所有 `.git` 目录!
    find ~ -type d -name .git | xargs -- rm -rf
}
test-rm-your-dot-git-assert() {
    # 验证在 `test-rm-your-dot-git` 这个测试执行方法中最终是否会执行以下这个命令。

    rm -rf ~/src/your-awesome-project/.git ~/src/code/.git
}

Bach 会分别运行每一个测试用例的两个方法,去验证两个方法中执行的命令及其参数是否是一致的。比如,第一个方法 test-rm-rf 是 Bach 的测试用例的执行,与之对应的测试验证方法就是 test-rm-rf-assert 这个方法

在第二个测试用例 test-rm-your-dot-git 中使用了 @mock API 来模拟了命令 find ~ type d -name .git 的行为,这个命令用来找出用户目录下的所有 .git 目录。模拟之后,这个命令并不会真的执行,而是利用了 @stdout API 在标准终端上输出了两个虚拟的目录名。

然后我们就可以执行真正的命令了,将 find 命令的输出结果传递给 xargs 命令,并组合到 rm -rf 命令之后。

在对应的测试验证函数 test-rm-your-dot-git-assert 里面就验证是 find ~ -type d -name .git | xargs -- rm -rf 的运行结果是否等同于命令 rm -rf ~/src/your-awesome-project/.git ~/src/code/.git

@mock 是 Bach Testing Framework 中很重要的一个 API,利用这个 API 我们就可以模拟 Bash 脚本中所使用的任意命令的行为或者输出。

比如

@mock curl --silent google.com === \
    @stdout "baidu.com"

模拟了命令 curl --silent google.com 的执行结果是输出 baidu.com。在真实的正常场景下,我们是无法做到访问 google.com 得到的是 baidu.com。这样模拟之后就可以用来验证 Bash 脚本中处理一个命令不同响应时的行为了。

@mock API 甚至还支持更复杂的行为模拟,我们可以自定义一个复杂的模拟逻辑,比如:


@mock ls <<\CMD
    if [[ "$var" -eq 1 ]]; then
        @stdout one
    else
        @stdout others
    fi
CMD

在这个模拟中,会根据变量 $var 的值来决定命令 ls 的输出 one 还是 others

@mock API 模拟的命令在任何时候执行的时候都是同样的行为。但如果要模拟同一个命令重复执行的时候要返回不同的值,Bach Testing Framework 还提供了一个 @@mock 这个 API,比如:

@@mock uuid === @stdout aaaa-1111-2222
@@mock uuid === @stdout bbbb-3333-4444
@@mock uuid === @stdout cccc-5555-6666

这三个模拟命令模拟了 uuid 在重复执行三次的时候都返回不同的结果,按照模拟的先后顺序分别输出对应的模拟输出。如果在执行完所有的模拟输出后,再重复执行将会始终输出最后一个模拟的输出。

更详细的 API 介绍请在 Bach Testing Framework 的官网 https://bach.sh 查看。

使用 Bach Testing Framework 还可以让我们更安全方便的练习 Bash 编程。

比如,我们希望实现一个函数 cleanup 用来删除参数上指定的文件。一个实现可能是:

function cleanup() {
    rm $1
}

这个函数的实现其实是有安全问题的,因为对于 Bash 来说,有没有把一个变量用双引号包含起来是非常重要的。在这个实现中,变量 $1 就没有用双引号,这会带来严重的后果。下面我们将使用 @touch API 来创建几个文件,其中将有一个文件名中含有特殊字符 的文件 bar。我们都知道,对于含有特殊字符的文件名是要放入到双引号中的。现在这个这个 cleanup 的实现里面没有使用双引号,但是传参的时候使用了双引号,那是否还会按照我们的预期来执行呢?


function cleanup() {
    rm -rf $1
}

test-learn-bash-no-double-quote-star() {
    # 创建了三个文件,其中有一个名为 "bar*" 的文件
    @touch bar1 bar2 bar3 "bar*"

    # 要删除这个错误的文件名 bar*,而不删除其他文件,使用了双引号来传参,这是正确的
    cleanup "bar*"
}

test-learn-bash-no-double-quote-star-assert() {
    rm -rf "bar*"
}

这个测试用例将会失败,从验证结果中我们可以看到,期望只删除文件 bar,但是在函数 cleanup 里面,因为遗漏了双引号,会导致变量被二次展开。实际执行的命令是 rm -rf "bar*" bar1 bar2 bar3

现在修复函数 cleanup,把变量 $1 放入双引号:

function cleanup() {
    rm -rf "$1"
}

再次执行测试,会发现确实执行的是命令 rm -rf "bar*"

Bach Testing Framework 目前已经在宝马集团和华为内部使用了。在宝马集团的一个有数千人规模的大型项目里,Bach Testing Framework 保证了数个非常重要的构建脚本的维护。这些脚本的可靠性和稳定性决定了数千人团队的工作效率,现在就可以在本地快速验证这些构建脚本的执行逻辑,也避免了在本地很难复现一些构建集群中的特殊场景的问题。

如果大家在使用上有啥问题和建议可以去Bach 测试框架的项目上留言。

即使为 Bash 脚本写了单元测试后,我们可能还会遇到为什么 Bash 脚本总是不稳定?难道真的是 Bash 不严禁不稳定吗?另外还有《关于 Bash 的 10 个常见误解》,看看你有多少误解。以及在学习和掌握运维技术栈时,除了 Bash 以外可能还要学有很多工具,比如 Python、Ansible、Kubernetes等等。这些工具在运维技术栈里处于什么位置,他们之间的关系又是如何呢?我们是应该学 Bash 还是 Python 还是 Ansible 呢?请看《DevOps 技术栈》这个我认为最重要的一篇文章以建立起我们的运维技术脉络。

09. August 2020 · 2 comments · Categories: Uncategorized · Tags: ,

Bash 大概是程序员们既是最熟悉的而又是最陌生的。说熟悉,Bash 几乎存在于程序员身边的绝大多数电脑上。说陌生,Bash 作为最常用的脚本语言,很少有程序员们了解 Bash 的特性和语法,而且因为不了解还产生了很多的误解。比如什么 Bash 作为一个脚本语言,其语法怪异且不严禁,仅适合编写简单的脚本,我们应该用 Python 代替 Bash。其实这完全是错误的理解。甚至很多程序员并不知道他们当前使用的 Bash 版本是多少,用的是 3.x?还是 4.x?还是 5.x?你知道你用的是哪个版本吗?

其实 Bash 的语法是图灵完备的,也就是说理论上我们可以用 Bash 实现任何其他语言可以实现的功能。话虽如此,但不同的语言有其业务场景的适用性,我们当然应该把 Bash 用在最适合的场景下,这才能发挥出 Bash 的强大威力。

完全没有道理用自己熟悉的编程语言去比较其他语言,如果不符合自己熟悉的习惯就评价为怪异。作为程序员一定要保持开放的心态去学习。

这篇文章介绍了大家对 Bash 的常见误解,看看你知道多少?

Bash 的 if 语句后面其实不是括号

这应该是被人误解最多的,吐槽最多的一点。比如 Bash 的语法非常怪异,if 后面的括号竟然是方括号,而且括号的前后都要有空格!

事实上,Bash 的 if 语法定义上其实并没有定义括号,而且命令序列。执行命令 help if 可以看到 if 的使用方法

if COMMANDS; then COMMANDS; 
[ elif COMMANDS; then COMMANDS; ]... 
[ else COMMANDS; ] 
fi

实际上所谓的方括号 [ 其实是一个命令,这个命令既是 Bash 的内建命令,也是一个外部命令,根据系统的不同可能位于 /bin 或者 /usr/bin 目录下。比如 macOS 上执行命令 ls -l “/bin/[” 就可以找到。

当我们知道了方括号 [ 是个命令后,这些怪异的语法就变得很容易理解了。因为 if 后面是命令,所以当然也可以用方括号 [ 命令了。既然这是一个命令,那命令的参数和命令本身自然也需要有空格来做分割了。事实上方括号 [ 命令等同于 test 命令,但是要求命令的最后一个参数必须是 ]。哈哈,如果一个括号只有开头而没有结尾,这才是最怪异的呢。

不过还有一种常见的是双方括号 [[,这个是 Bash 的内建关键字。虽然可以把关键字当作命令来看到,但是 Bash 会对关键字做特殊处理。比如在 [[ 与其结尾参数 ]] 之间是可以使用 &&、|| 这样的逻辑操作的,如果我们把 [[ 1 -eq 1 && 2 -eq 2 ]] 当作两条命令来看到,第二个命令是 2,这肯定会执行失败了。这就是 Bash 对关键字做的一个特殊处理。而 [ 是一个命令,不管是内建命令也好外部命令也好,[ 1 -ne 0 && 2 -ne 3 ] 一定会出错的,因为这将执行两条命令,第一条 [ 1 -ne 0 将会因为 [ 命令的最后一个参数不是 ] 而出错,第二条命令 2- ne 3 ] 可能将会因为在 PATH 环境变量中没有找到一个名为 2 的命令而出错。

说到这里,我们可以想想看。假如有一个 Java 程序员竟然对 Java 的 if 语句的语法不熟悉,这个程序员怎么可能写出好的 Java 程序?把 Java 换成 Bash 也是一样的。

真的无法阻止 Bash 中的函数修改全局的变量吗?

这应该是第二个被人误解最多的一个 Bash 特性,也常被用来证明 Bash 的语言多么不可靠。原因是如果在函数中没有使用 local 关键字来声明变量,那函数里的变量将会对全局产生影响。或许看起来有些绕口,请看如下的例子:

#!/usr/bin/env bash
x=42
bar=answer

function foo() {
  local x=999
  bar=world
  echo foo: x=$x bar=$bar
}

foo
echo x=$x bar=$bar

执行的结果是

foo: x=999 bar=world
x=42 bar=world

在函数 foo 中用 local 关键字声明了变量 x,这使得函数内变量 x 的作用域限于函数 foo 内,不同于外部的全局变量 x。而函数内使用变量 bar 时没有用 local 关键字声明,这使得函数内对变量 bar 的赋值实际上是修改了函数之外的变量 bar。这可能会造成我们在编写函数的时候,必须显式的使用 local 关键字来声明局部变量,否则会发生无意中使用了全局的同名变量,导致该全局变量被无意中修改的问题。

这个问题真的很严重,也的确是有些人吐槽 Bash 不严谨的地方之一。事实上,这也恰巧说明了我们可能并不了解 Bash 中定义函数的语法。

大家都知道,在 Bash 中定义函数时,把函数体放到花括号中。看上去这点和很多编程语言也非常类似,比如 C、Java 等。但却不会把变量的作用域限制在花括号的范围内。其实 Bash 中声明函数时,其语法要求函数体是一个组合命令即可。所以,花括号并不是必须的,我们还可以用圆括号。请看下面的例子中函数 foo 的函数体用了圆括号:

#!/usr/bin/env bash
x=42
bar=answer

function foo() ( # <--
  local x=999
  bar=world
  echo foo: x=$x bar=$bar
) # <--

foo
echo x=$x bar=$bar

执行的结果是:

foo: x=999 bar=world
x=42 bar=answer

在第二个例子中,我们把函数体放到了圆括号里面,这使得这个函数将会在子进程中执行。而子进程是无法修改父进程中的任何变量的。所以,即使在函数中没有用 local 关键字声明变量 bar,对这个变量的变更也不会影响到全局的同名变量。

因为在定义 Bash 的函数时需要的是组合命令,所以下面的函数定义也是合法的:

function foo()
for param; do
  echo "=> $param";
done

执行 foo a b c 会得到

=> a
=> b
=> c

是的,出乎大家的意料,定义 Bash 函数的时候连什么括号都可以不要。

所以,我想再吐槽一次。如果有人连 Bash 的语法都不了解不熟悉,有很多的误解。他们是如何得出 Bash 的语法不严禁,功能不如其他语言这些结论的?

Bash 也支持直接执行目录名来切换到该目录中

很多使用 Zsh 的用户在举例 Zsh 与 Bash 的区别时,这是一个常用来对比的特性。很可惜,这不是 Zsh 的独特特性,Bash 其实也支持。实际上 Zsh 下这个特性也是默认关闭的,只是很多人都会使用 Oh My Zsh 这个知名的配置工程,而 Oh My Zsh 默认开启了这个特性。在 Bash 下开启这个特性的命令是 shopt -s autocd,开启之后我们就可以直接执行目录名来切换到该目录了。

Bash 其实也支持 ** 通配符来匹配多级子目录

这个特性也是有些人会误认为 ** 通配符是 Zsh 比 Bash 好用的一个特性,但其实 Bash 也是支持的。

开启命令是 shopt -s globstar

然后我们就可以用 ls **/*.txt 来列出当前目录及其子目录下的所有 *.txt 文件了。

所以,以后也请不要那这个特性来对比 Zsh 和 Bash 了。

Bash 在命令行上其实也支持撤销、粘贴等操作

当然有很多人知道在命令行下如何快速跳转光标、删除单词或者删除到行尾等常用操作,但很少有人会知道的 Bash 在命令行也支持撤销、粘贴等操作,而且粘贴功能事实上比操作系统默认的更强大。

撤销的快捷键是 Ctrl-/,反复按下会继续撤销之前的操作。

粘贴的快捷键是 Ctrl-y。当在命令行使用了 Ctrl-k 删除到行尾、Ctrl-w 删除前一个单词、Alt-d 删除下一个单词、Ctrl-u 删除整行等操作后,Bash 会将删除掉的内容放到自己的剪贴板中。要注意的是这个 Bash 的剪贴板不同于操作系统的剪贴板。删除之后,可以按下 Ctrl-y 在当前光标处粘贴最后一次删除的内容,重复按下会重复粘贴。

如果删除过多次,按下 Ctrl-y 粘贴出最后一次删除的内容后,再紧接着按下 Alt-y 就会替换为前一次的内容,重复按下 Alt-y 会继续替换更早之前删除的历史内容。

Bash 中使用 I/O 重定向的时候,重定向可以不用放在命令的最后

我们经常使用 >、>>、<、<< 来把命令结果保存到文件、或者从文件中读取内容。通常我们都是把重定向放在命令的最后。比如 ls -ld * >/tmp/files.txt

实际上,重定向可以位于一条命令的任何位置,放在中间或者最前面都是可以的。比如以下命令都是一样的

>/tmp/files.txt ls -ld *

ls >/tmp/files.txt -ld *

有时候不把重定向放在命令的最后会给我们快速复用并修改之前的命令非常方便。比如我们只需要快速搜索到之前执行的命令 >/tmp/files.txt ls -ld *,就可以很容易的修改最后一个参数了。

顺便说以下,看上去 Bash 中的 I/O 重定向很简单、很易使用。但事实上Bash 的 I/O 重定向比绝大多数人认为的更强大且复杂。或许我会在另外的文章中来详细介绍一下。

Bash 支持直接访问网络

Bash 的网络功能是很多人不知道的一个特性,不需要什么 nc、socat 这些命令就可以访问网络资源。只需要使用 I/O 重定向直接访问设备文件 /dev/tcp/<host>/<port> 即可,把 <host> 和 <port> 换成你希望访问的网络地址和端口。比如 /dev/tcp/baidu.com/80 就是访问主机 baidu.com 的 80 端口。

请看下面的命令,用 Bash 的内建命令实现了一个简单的 HTTP 客户端功能,访问 baidu.com 并打印出响应。

#!/usr/bin/env bash
exec 6<>/dev/tcp/baidu.com/80;
printf "GET / HTTP/1.1\nHOST: baidu.com\nConnection: close\n\n" >&6;
while read -r -u 6 line; do
  echo "$line";
done;
exec 6>&-

执行结果

HTTP/1.1 200 OK
Date: Sat, 08 Aug 2020 23:31:36 GMT
Server: Apache
Last-Modified: Tue, 12 Jan 2010 13:48:00 GMT
ETag: "51-47cf7e6ee8400"
Accept-Ranges: bytes
Content-Length: 81
Cache-Control: max-age=86400
Expires: Sun, 09 Aug 2020 23:31:36 GMT
Connection: Close
Content-Type: text/html

<html>
<meta http-equiv="refresh" content="0;url=http://www.baidu.com/">
</html>

其他的 Shell,比如 Zsh、Sh 等可能不支持这种直接访问网络的方式,但是 Zsh 的网络支持其实更强大,只是方式不同。

Bash 的受限模式让自动化运维更安全

在启动 Bash 的时候,使用选项 -r 可以让 Bash 进入受限模式。在受限模式下,会有很多的限制,比如:

  • 不能用 cd 命令切换目录
  • 不能修改 PATH 环境变量
  • 不可以通过指定路径的方式来运行命令
  • 不能使用 I/O 重定向
  • ……

Bash 受限模式的一个常见的使用场景是自动化运维。想想看,如果系统会根据一些用户的请求来自动生成并执行一些命令,在这些命令中可能会使用一些用户的输入,这可能会产生类似 SQL 注入的漏洞攻击。再比如,在一些自动化运维的时候难免要临时切换到特权用户下执行命令。为了让系统更安全,在临时的特权用户环境里面只能执行一些特定的命令或者脚本,以尽量减少系统被攻击的可能性。使用 Bash 的受限模式就可以避免非期望的命令或者脚本被执行,让系统更加安全。

有些「Bash 命令」其实和 Bash 无关

或许有的人会把 ls、rm、mkdir、…… 等命令称为 Bash 命令,其实这些命令与 Bash 并没有关系。比如在 GNU/Linux 上,这些命令属于 GNU Coreutils 这个项目。而 Bash 命令应该是 Bash 的内建命令,比如 alias、exec、printf 等等。有些 Coreutils 中的命令因为使用的很多,为了避免无谓的进程创建开销,所以 Bash 也将其内建了,比如 echo、test 等。不过要注意的是,Bash 的内建命令的选项与 Coreutils 中的同名命令可能不同。

比如执行下列命令可能会得到不同的结果:

echo --help
/bin/echo --help

# macOS 上用 brew install coreutils 后的默认路径
/usr/local/opt/coreutils/libexec/gnubin/echo --help

我们也能够为 Bash 写单元测试

Bash 还有一个最易被人诟病的一点就是,很难去测试一个 Bash 脚本。所以这也是很多人说应该用 Python 代替 Bash 的一个理由,因为我们可以为 Python 写单元测试,而不能为 Bash 写单元测试。虽然已经有了一些 Bash 的测试框架,比如 Batsshunit2 等。但这些测试框架都是集成测试框架。这意味着,为了执行 Bash 脚本的测试用例,我们需要事先安装好被测脚本中依赖的各种工具,配置好所需的目录、配置等。比如在我们的脚本中使用了 Bazel 或者 CMake,如果没有安装 Bazel 或者 CMake,很显然这些测试都将失败。

如果可以为 Bash 脚本写单元测试,这就意味着这些测试可以在任意时候、任意的机器上乱序执行任意的测试都应该在秒级的时间内得到相同的执行结果。无论是在 Windows 上执行 GNU/Linux 的 Bash 脚本的单元测试,还是在一个全新的极简的环境中执行一个使用了很多第三方工具的复杂的 Bash 脚本的单元测试,都应该得到稳定的测试结果。

现在有一个名为 Bach 的测试框架(Bach Testing Framework),这是目前唯一的一个真正可以为 Bash 脚本编写单元测试的框架。Bach 测试框架的独特之处在于它不会执行任何在 PATH 环境变量中的命令,而且还提供了丰富的模拟命令用于模拟其它命令的行为。Bach 测试框架非常适合于测试 Bash 脚本的执行逻辑,而不用实现安装并配置好被测脚本中使用到的各个工具。

请看下面的例子:

#!/usr/bin/env bash
set -euo pipefail
source bach.sh

test-rm-rf() {
    # Write your test case

    project_log_path=/tmp/project/logs
    sudo rm -rf "$project_log_ptah/" # 这里写错了变量名,导致了一个严重的错误
}
test-rm-rf-assert() {
    # Verify your test case
    sudo rm -rf /   # 这就是将要执行的命令
                    # 别慌!Bach 不会让这个命令真的执行!
}

Bach 测试框架的每一个测试用例默认是由两个函数来组成,一个是用于编写测试用例,比如上例中的 test-rm-rf,与之对应的是用于验证测试结果的测试验证函数 test-rm-rf-assert。Bach 测试框架会分别运行这两个函数,并对比执行中调用的命令序列是否一致。

我写了一个 Alfred Workflow,用于新建和加入 Zoom 会议。
现在支持免费和付费帐号,如果没有登录也可以直接进入会议。

使用方法:
1、直接在 Alfred 里面粘贴 Zoom 的会议链接,按下回车就可以自动加入会议。如果 Zoom 还没有运行,也会自动运行起来。
2、 直接在 Alfred 里面粘贴 Zoom 的会议链接后,按下 Command 键会 新建一个会议,而不是加入现有的会议。并且会自动把新建的会议的 url 放入剪贴板,我们只要直接粘贴就可以。
3、有 `zm` 关键字,直接按下回车就是新建一个会议;
4、在 `zm` 关键字后面可以继续添加 meeting id,按下回车会自动加入;
5、在 `zm` 关键字后面无论是否有 meeting id,按下 Command 键都会新建一个会议。

源码:https://github.com/chaifeng/alfred-zoomus

下载:https://github.com/chaifeng/alfred-zoomus/releases

This Alfred Workflow is used to start or join a Zoom meeting.

Support free or paid account, you can join a meeting without logged in

Usage:

  1. Paste a Zoom meeting URL in Alfred directly, press Enter, this will join an existing meeting.
  2. Paste a Zoom meeting URL in Alfred, press Command + Enter, it will start a new meeting. And put the URL of this new meeting into your clipboard automatically.
  3. A new keyword “zm” is used to start a new meeting.
  4. Append an existing Zoom meeting ID after the keyword “zm”, it will join this meeting.
    for examples: zm 123-456-789, zm 123456789
  5. Whether or not there is a meeting ID after keyword zm,press Command + Enter will always start a new meeting.

Source code: https://github.com/chaifeng/alfred-zoomus

Download: https://github.com/chaifeng/alfred-zoomus/releases

Dreamhost 的空间就要到期了,实在无法忍受在中国访问的龟速了。

昨天正式迁移到 Linode 东京机房,速度那叫一个爽,基本上带宽全占满。

前天是女性朋友的节日,大早上的写了一个段子,昨天竟然发现上了新浪微博的热门,Cooooool。

兜兜的照片也好久不放了,很纠结,担心万一被坏人利用把我家兜兜给骗走。

把基本情况先简述一下,首先是周六太原大雨,影响了到场率,不过还是有将近40人,而且还看到一些新面孔。其次是两位讲师的话题都很给力,演讲都超时,而且QA环节大家都很踊跃的提问。郭振的话题用了1个半小时才结束,张龙的话题更是达到了2个小时。最后5点50结束本次活动。

以下文绉绉的总结出自专业编辑李洋之手:

7月21日,QClub太原站如期在山西出版传媒集团一楼会议室举行,这已经是QClub第三次来到太原举行活动。今天的龙城太原下起雨来,这给炎炎夏季带来了丝丝凉意;而这场期盼已久的QClub技术社区活动对于技术相对滞后的太原地区而言,也是如同久旱逢甘霖一般及时与酣畅。此次活动中,增添了很多新的面孔,他们为QClub太原站社区活动添加了新的血液与活力。

大雨并没有浇灭前来参加活动人们的热情,在不小的会议室当中坐得是满满当当。技术开发人员也都希望在这场难得的“Android应用开发”的主题讲座中汲取自己所需要的养分并希望借此来解决在工作中所遇到的实际问题。好了,我们现在直入主题,看看讲师们今天会给我们分享那些先进理念与经验以及会带给我们哪些意想不到的惊喜!

首先是来自盛大创新院的高级研究员、乐众ROM项目组总架构师郭振分享的 Android 备份框架的架构与设计,以及如何将自己的服务集成到 Android 系统中。此次活动有一点明显地改善就是参与者的积极性与主动性较前两次有了显著地提高。在互动环节当中,参与者与讲师之间的交流更加自然,提问也是更加踊跃,奖品更是抢手……

接下来是联想集团全球应用开发部的高级工程师、InfoQ翻译团队编辑张龙分享的Android跨进程通信机制与AIDL,介绍了Activity与Service之间的通信原理。张龙这个名字,大家一定不陌生,这已经是第二次来到太原与大家分享经验,算是QClub太原站的老朋友了。大家对于张龙刚刚翻译出版的《Android Web应用高级编程》这本书产生了浓厚的兴趣,这本书涉及到一些跨平台移动开发技术的内容。这本书也是作为活动中的奖品来发放,在互动过程中大家的热情都很高涨,很想要获得这本精美IT图书,因此互动问答过程就格外积极与踊跃!

这已经是山西书海数字网络传媒科技有限责任公司第三次成功举办QClub太原站活动了,书海传媒对于推动太原地区IT技术交流,应该说是功不可没。最后,我们还是要再次感谢QClub太原站活动的本地赞助商——书海传媒的鼎力支持。

from 2002-06-14

 

08. May 2012 · 2 comments · Categories: Uncategorized · Tags:

先说这次活动的问题,一定要自我检讨,这次竟然忘记安排摄影师了,最后只有区区可数的几张照片。

再说高兴的事情,本次活动签到人数首次达到53人,这也是从去年7月开始第一次组织活动以来的最高记录。可能也和这次活动的主题《前端开发与用户体验》有关系吧,在太原的设计师多数都是美眉。从现场照片上也能看出,女生比以往的几次活动明显多了,然后……咳咳……人就来的多了。

然后说说讲师吧,分别是来自赞助商快乐妈妈(www.happymama.cn)的设计师赵敏,和来自阿里云的交互设计专家安勇。本地讲师与外地讲师相比,明显在演讲经验和内容的组织上比较欠缺,希望通过以后的活动可以提高本地讲师的演讲水平,也能够让太原的讲师去外地做分享。

这次活动准备的礼品还是比较多的,十本书和二十张2012QCon北京大会资料光盘。依然还是贯彻只要你参与或者与讲师互动,就能获得小礼物的宗旨。到活动的结束,就剩下了5张光盘。基本上都是在阿里云交互设计专家安勇的演讲结束后的提问环节,大家的热情都很高,一口气就发出了20份的礼物,让安勇都有点快招架不住了。

最后的 Open space 环节,与上次相比,稍显冷清了一点。可能因为设计师美眉们好多都提前离场的原因,然后……咳咳……人就走的更多了。

讲一个有趣的小插曲,本地赞助商快乐妈妈(www.happymama.cn)的技术负责人叫安磊,他在接到安勇后做自我介绍。结果安勇以为对方把他的名字记错了,还更正说“不是安磊,是安勇”。

总结,本次QClub太原站参与人数超出了预期,终于摆脱了参与人数倒数第一的帽子,以后一定要继续吸引IT美眉的参与。活动中大家的参与度还是比较高的,可能是大家都很关心这个主题,也可能是因为外地讲师是中韩混血帅哥。忘记安排摄影师了,重大失误。

最后一定要再次感谢赞助商快乐妈妈(www.happymama.cn),谢谢,这次的活动真的真的很成功。

2012 年第一次QClub太原站技术沙龙在山西出版传媒集团一楼会议室

举行,来自太原的数十名软件从业人员参与了此次活动。本次活动的主题是“Spring框架深度剖析”,主讲人有来自联想集团全球应用开发部高级工程师、InfoQ中文站翻译团队编辑张龙,也有太原本土资深软件开发专家和项目经理李永茂。李永茂讲师深入IoC核心实现,剖析了关键的部分源码;还分享了他在架构原理及设计思想方面的独到见解和一些关于Bean的解析问题。接下来,张龙讲师深入浅出地从源码角度揭示Spring 的架构设计与模式的应用;并分析了Spring对AOP的支持与实现方式及Java技术分析等内容。

讲师们的精彩演讲将本次活动频频推向高潮,所有参加活动的听众都认为不虚此行,觉得受益匪浅。在Open Space中,全场的气氛最为轻松活跃,听众们与讲师融为一体,相互交流经验,谈及一些实际工作中遇到的问题。全部活动结束时,天色已经很晚了,听众们仍然意犹未尽,继续讨论着下一次QClub的主题内容和如何将太原的社区活动搞得更加丰富一些。QClub太原站在交流声、掌声、欢笑声中画上了圆满的句号。

最后,再次感谢第一次QClub太原站活动的本地赞助商山西书海数字网络传媒科技有限责任公司的鼎力支持。

–分割线–

  • 上面的文字是赞助商专业编辑写的。
  • 最后 Open Space 环节,大家都忙着聊天忘记拍照片了。