如何写出更"好"的shell脚本
在工作当中shell,是不可避免的工具,但是因为shell沉重的历史包袱,如何写出高质量的shell脚本确实比较麻烦.
本文并不讨论shell常见的混乱易错的地方,而是尝试从编程方法角度来探讨这个问题.
##背景
一般如果使用C或者Java之类,开始写程序,我们会认真规划,划分模块和层级结构,写出专门的函数.
最终组织成一个可用的不管是大型或者小型的程序.
编译器会帮助我们进行编译检查,从而最终编译通过,链接,最终构成可执行程序.
我们在分别在不同的场景/输入的情况下,进行测试,检查是否有各种缺陷,然后修复它们,最终得到”最终”版本的程序.
后续我们还会不断使用这个程序,得到更多的bug反馈,我们再进一步修复这些bug.
但是在使用shell编程的时候,我们似乎就瞬间忘记了这些原则或者方法.我们只是简单的把命令或按照顺序,或按照输入输出(Pipe)串接在一起,就认为工作完成了.但是这个时候除非是最简单的shell脚本,绝大多数情况下,这个shell脚本都是有缺陷的.
之所以会出现这种情况,主要有下面几个原因.
脚本解释执行,没有编译检查
因为脚本是解释执行的,因此可以立即执行.这种跳过了编译检查,脚本之间模块/函数的连接往往都是通过字符串进行的.而没有各种类型检查,这其实就可能放松了程序的约束.
这种问题其实对一些动态语言也是存在的,不过没有shell那么严重.
脚本是”一次性”的
脚本是简单的,一次性的,这种观念其实潜藏在我们的观念里.因为写一个可以运行,够用的脚本实在是太容易的.我们就忽视了其他没有测试的例子.最终可能还不小心轰烂了自己的腿.
脚本没有”显式的”提供严谨的特性
我们其实很多时候没有意识到shell其实提供一些可以更好的组织的特性,这有助于我们写出更好的程序.
但是很多人都没有想过去应用他们.
which shell?
我们知道shell其实有许多种,最为常见的是bash.
可是系统默认的shell一般是sh.sh很多情况下是软链接到dash.
二者之间其实还是存在很多微秒的细节的.因此在shell的最开始通过shebang,就是以#!开始的那一行声明使用的是哪个shell.
我们后面的讨论也是基于bash的这个假设进行的.
引号
我们知道一个最为常见的缺陷,就是无法处理带有空格的字符串.(或者文件名).
实际上这个bash提供的”特性”.
func $param
如果param这个变量中含有空格,那么param就会展开,从而作为func的第一个参数和第二个参数,(也可以更多,按照空格划分).
可惜的是这个特性在非常多的情况下,是我们不想要的.这个时候就是使用引号来避免这种展开.
func "$param"
不要小看这个微不足道的细节,这个缺陷在海量的脚本中存活着.以至于大家发明了另外一个技巧里避免这个缺陷:
文件夹的命名不要使用空格
不过毕竟我们不能控制所有的脚本都没有缺陷,这个好习惯还是保留吧.
函数
bash中也有函数的概念,而这个函数其实非常的原始,基本上只有参数展开的功能.
不过幸好,我们在函数内部还是可以local修饰.这样可以是的变量在不同的scope中,有着各自的生命周期.
而默认情况下则不是这样的.默认所有的变量都是global,这是多么可怕,几乎不用说了.
函数还提供了一层抽象,里避免重复的代码.这是非常有意义的.当我们的代码有重复的部分的时候,用函数来优化吧.
##source
很多的shell脚本都是只有一个文件.可是shell脚本也是在不断的膨胀的.终于有那么一天也许你需要多个shell脚本来容纳它.这个时候就可以考虑使用source了.
使用source可以在”当前”的shell执行环境中引入/执行这个脚本.
bash的初始化的过程中一系列的bashrc之类,就是通过source执行的.
我们可以将通用的shell函数放在一个脚本中,然后在不同的地方source.
这种方法避免了膨胀的单文件,划分了模块,同时我们未来还可能复用部分shell代码.
NOTE:
真的要"复用"shell脚本吗?哈哈,可以把这个当做一个笑话.
数组和关联数组
使用数组,我们可以直接在数组上循环操作事物.在shell中我们也可以这样做的.
而且bash 4.0+版本还支持了关联数组(或者说map)这种结构.从而我们可以写出更为复杂的程序了.