st: 为写 shell 工具而写的 shell 工具

如题,本文介绍的 st, 是帮助编写、管理 linux shell scripts 而开发的工具,如果读者本身就对写 shell 脚本感到为难或嗤之以鼻,这篇文章可能对你没有多少帮助,所以请慎读。

痛点

自从系统地学习了 shell 脚本之后,感觉一扇虚掩的新世界大门被打开,我惊叹这些操作令人无法呼吸之余,也动手把笔记里记录的各种 XX 安装步骤、XX 环境配置,都变成了一个 XX 安装.sh、XX 环境.sh。

之后痛点来了,每次我需要在某台机器上跑某个脚本做做事时,都需要费力思索那个 .sh 放哪儿了,然后翻箱倒柜。即使有幸找到了,再上传到机器上,已经过去大半天了。

脑洞

我见过一些讨巧的做法,就是将一系列复杂的操作写成脚本后,挂在网上,再告诉用户,要做 XXX 其实非常简单,只需要一条命令:

curl -sSL http://xxx.xxx/xxx.sh | sh -

这里解释一下,shell 脚本有三种执行方式,第一种是赋予脚本文件可执行权限,直接运行该脚本,系统靠文件第一行的 shebang 来确定解释器。第二种是运行解释器,并将脚本文件名作为参数传入。第三种是启动解释器,解释器从标准输入读取内容并执行(如果这个标准输入是用户键盘,那就是所谓的“交互式执行”)。

上面那条命令就是通过第三种方式执行脚本,从 url 获取脚本内容,并利用管道方式做为解释器 sh 的标准输入,脚本内容就开始执行了。

受其启发,我也曾想过将我的脚本挂到网上,这样我就可以方便地在任何一台机器上(当然要能访问外网),“在线执行”我的脚本了,然而问题是 url 可能会太长,各种脚本数量可能会太多,还是记不住哇——所以我想还得写个脚本,这个脚本可以帮我看看当前有哪些脚本可以“在线执行”,对应的 url 是什么——不不,最好是我只要说名字就行,它自己去找对应的 url 是什么,并自动执行……

Shell Tools

基于这个想法,我写了 st,取 shell Tools 的首字母。

st 的原理与上述脑洞还有些不同,主要在于 st 被加载的方式上:

. <(curl st.wolfogre.com)

这句命令很长,看起来很怪,事实上,我已经让它尽可能得短了,所以看起来才会怪一些,如果将它展开,是这样的:

curl st.wolfogre.com -o st
source st
rm -f st

source 命令与 . 命令(没错,就是一个点)是等价的,它和脚本解释器一样,也会读取一个 shell 脚本的文件,执行其内容。为什么要用 source 来执行脚本,这里只简单粗暴得解释一下:就是我们的终端也是一个解释器,比如通常是 bash,我们在终端里执行一个脚本,事实上是又启动一解释器(可能是 bash、sh,也可能是 python、perl、ruby,取决于 shebang 或敲入的命令),而子解释器执行时定义的函数、变量是不会出现在父解释器中的,export 也不行(吐槽一下,export 的功能是声明这个变量需要继承给子进程,而不像网上一些文章说的:export 可以将变量带给父进程,实验一下就能证明)。而 source 不同,它事实上是告知当前解释器,也就是让当前终端去执行文件内容,所以文件里定义的函数、变量是会出现在当前终端里的。

正是利用这一点,. <(curl st.wolfogre.com) 从 st.wolfogre.com 获取了一份脚本命令并执行,在当前终端的定义了一个函数,或者说命令,这个命令就叫“st”。之所以用定义函数的方式声明 st,而不是往 $PATH 路径里添加一个可执行文件,是为了 st 能随着会话结束自动消失,不会在系统里残留什么东西影响别的用户(如果有的话)。正因为这样 st 能召之即来,挥之即去。

有了 st 这个命令,就可以开始嗨了:

image

这些命令都是我为了解决平时学习、工作中碰到的问题,而编写的脚本,且持续更新,有了什么新脚本,加进去就是。

原理

st 的基本原理我在开头已经介绍了,而其具体实现,可以直接阅读源码(不是装逼,让我用通顺流畅的汉语取翻译直观明了的代码有点难为人了)。代码在 https://github.com/wolfogre/st 可以找到。

但有些细节还是要说明一下的,否则容易引人误会。

  1. 我在我自己的服务器上部署了一个代理服务,代理域名 st.wolfogre.com,凡是访问 http://st.wolfogre.com,就相当于访问 https://raw.githubusercontent.com/wolfogre/st/master/index.sh,凡是访问 http://st.wolfogre.com/func/xxxx,就相当于访问 https://raw.githubusercontent.com/wolfogre/st/master/func/xxxx
  2. 之所以部署一个代理服务,是为了让 url 更可能短,也能增加访问速度;
  3. 代理服务可以直接用 nginx 做,但我为了提速写了个 stproxy,利用 github webhook 更新缓存,不用每次都访问源站,注意 stproxy 不是必需的,所以不做介绍。
  4. 为了进一步提速,事实上 st 会将脚本缓存到 /tmp 下,避免每次执行都需要网络 IO。

此外,在开发 st 使用了所谓的“高阶脚本”和“自举”。“高阶脚本”意思是一些脚本是使用脚本生成的,仓库根目录的 index.sh —— 也就是声明 st 函数的脚本,便是靠 build.sh 生成,build.sh 会统计 func 目录下的功能性脚本,读取脚本说明(脚本第二行注释内容),生成 index.sh,所以 st help 才会知道有哪些脚本能用,分别是干嘛的,st version 才能知道自己是哪个版本,什么时候更新的。

所谓“自举”是我觉得在开发 st 的过程用也有一些操作可以靠 st 来帮助完成,比如克隆仓库、提交修改、更改版本号等待,这个命令是 st dev,不过它是隐藏的,st help 并不能看到它。

所以经历白手起家的初期开发后,现在我想往 st 里新加命令就很简单了:利用 st dev clone 克隆仓库,往 func 目录下添加我新写的脚本,执行 build.sh 来“编译”,如果我想修改大版本号可以用 st dev version (小版本号会在 build 时自动更新),在 st dev commit 自动提交修改,github 的 webhook 会通知 stproxy 更新缓存。

美滋滋。

冷水

读者读到这儿,可能以为本文是教大家如何使用 st,安利大家使用 st,但是,并不是,至少不完全是。

我不鼓励任何人在没有询问过我的情况下,在生产环境或比较重要的机器上运行 st。我需要提醒大家,运行任何来历不明的脚本是非常不明智且危险的——我当然不会挖坑害人,但是我水平有限,写的时候也没有精力考虑周全,可能我自己运行没有问题,但是换个环境,换个系统却会造成严重的后果。

讲这么多,确不是为了推销 st,那是为了啥?

如果我只是为了推销 st,我不会花这么大篇幅去讲它的原理。

是的,我的目的是为了鼓励大家实现自己的 st,管理自己的脚本。

我不是没想过写个工具帮助大家搭建自己专属的 st,甚至写个能让大家分享脚本的平台,又或者做个网站可以免费托管大家的脚本,再找个盈利点圈圈钱。但是现实是残酷的,我相信大多数人即使会写,也是懒得自己写 shell 脚本的,而那些原意折腾,热衷于写各种鬼畜脚本的同学,又怎屑于用我做好的工具,而不是自行实现一套呢?

能够开箱即用当然是最好的,但是如果我为了能让更多的人能开箱即用,往 st 里添加成百上千的功能以覆盖用户需求,且为了兼容各个 linux 发行版,每个功能都要充分考虑适配性,那将远远超出我的能力极限,也违背了 st 的初衷——方便我自己。

对的,说到底,当前 st 是我开发的,为我一个人服务的工具。

最后

说到这儿,仿佛我是一个狂热的 shell 脚本追随者,甚至会说:“Shell script 是世界上最好的语言”。

必须澄清一下,并不是。事实上我不喜欢 shell 脚本,它的语法太诡异了,功能太老套了。我尝试过用 python、ruby、perl 替换 shell,但是都放弃了,因为我写 shell 脚本的目的是为了快速实现一些 linux 的自动化操作,既然是自动化,我就不能期待机器上安装了 python、ruby 或 perl,以及它们常用的三方包(如果有的话)。我相信这也是为什么网上大多数自动安装脚本都是针对 shell 写的,毕竟它才是真正的胶水语言,最常用的与操作系统交互的语言。

倒霉就倒霉在,怎么偏偏是它。