容易维护的shell脚本

前言

最近需要面对一些年代久远的shell脚本,顺带一些臃肿的Makefile和滥用的AWK程序,在这里说说遇到哪些问题,学到哪些知识。

目录结构混乱

比如一个shell脚本,会产生文件A_201405.txt,以后每个月产生一个文件,但总有一个A_current.txt指向最新的文件。

时间久了之后就会有大量的历史文件,而一个shell脚本往往是输入文件有多个,输出文件有多个,日志文件有多个。。。最后就是一个滚绣球。

如果能够分别为不同的输入文件、输出文件、日志文件建立对应的子目录,再加上一个archive、pack目录,就可以免去许多烦恼。

在程序设计的时候不考虑这些问题,只图自己爽、方便、省事,就会让后来维护的人骂娘。

程序日志聊胜于无

有一部分程序会考虑留下日志,聊胜于无,但即使有一部分日志功能,也是提供的信息只言片语,往往记录什么是错误的,但并没有显示凭啥说这是错误的。

这种没有指示性的日志唯一的功能就是混淆视听,既然提供了日志,就要能够提供尽量多一些关键性的信息,这些信息最好要形成一定的脉络,能够起到帮助定位的功能。

变量的定义位置随意

一般程序最开始部分都会有一些描述性的信息,然后就是一些变量的定义。

对于变量应该相关联的集中定义在一个地方,这样在修改的时候能够修改彻底,最忌讳在中间或者角落定义变量;变量集中在程序前半部分定义好,后半部分专心处理逻辑。

对于路径相关的变量,一定要判断是否定义,以免出现删除根目录的悲剧。

对于变量的定义,从最基本的变量,到复杂的变量,应该有一个递进的关系,从需要修改的变量到不需要修改的变量一目了然,逻辑处理部分只关心不需要修改的变量。

如果变量是经常变化或者多处共用,最好作为配置文件来加载,如source config-xxx.sh

Makefile的恶梦:AWK乱入

Makefile很好用于工程管理,也可以作为批处理脚本执行。

Makefile适合用于作为命令执行的管理,但不适合在里面写具体的脚本内容,比如嵌入大量的AWK、Sed、Shell脚本的大杂烩。这种形式的程序很难维护,当要调试或修改的时候,要直接在Makefile里面写awk程序或shell命令,而且还要转义特殊字符。

可能是出于习惯,Makefile里面很少给予注释,哪怕是命令规则之间跳来跳去、变量此起彼伏。

对于awk、sed程序超过三个命令都应该独立出来,而Makefile里面压根就不应该以awk、sed嵌入的方式工作。

函数定义注释

一个函数的定义,应该有一定的注释,对参数有一定的说明;程序往往会告诉有几个参数,但对参数应该是什么值往往只字未提。

参数在函数定义后立即本地作用域定义一次。

比如:

1
2
3
4
5
function example () { # arg1, args: this is a example
local arg1=$1 #eg: 201404
local arg2=$2 #eg: Y/N
#...
}

执行入口和help信息

很多时候可以使用getopt等来判断选项和参数,从而执行相应的命令。

虽然函数调用会有消耗,但从长远来讲,程序不应该不带任何参数就执行,即使执行,也应该是打印帮助信息;这样可以避免误操作而引起不必要的改变。

程序入口完全可以使用类似:

1
2
3
4
5
function help(){ # print help usage
cat $0 | grep "^\ *function" | sed "s/[(){}]//g;s/function//g"
}
eval $*

这样可以单独执行特定函数,但存在两个问题:

  • 使用者如何知道执行顺序,因此要提供main等入口函数,函数说明信息要明确,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function main() { # arg1, arg2: the main function
    #....
    }
    function foo() { # arg1, arg2: step1----do the foo....
    #....
    }
    function bar() { #arg1, arg2: step2-----do the bar....
    #....
    }
  • eval执行任何输入,应该添加检查机制,如:

    1
    help | sed "s/\ *//g" | cut -d"#" -f1|egrep -x $* >/dev/null && eval $* || echo try \"$0 command, for example: $0 help\"

这样做还有一个好处,就是可以将工作不断分解,易于rerun、dryrun、debug等。

可dryrun

很多时候去调试程序,想要一步步执行,但只能一步步打印出来看,如果具备dryrun功能就方便很多。

简单的dryrun方法是设置变量$CMD,是dryrun的时候,CMO=echo,否则CMO=eval,然后在所有执行的命令前面添加$CMD即可。

这是一种经济实用的做法。

自检测、可恢复

很多程序会依赖于某个文件大小、更新日期或者其他信息,但程序本身又会改变该文件的大小、更新日期等信息。

一旦程序运行到一半出现问题,就无法保证能够下次运行成功。比如一个程序根据文件A有多少行来决定下一步怎么做,而文件A是每次去网站curl一个文件并简单处理得到的。结果某次网络出现故障,导致文件A无法联网就成为了空文件,于是cronjob就失败了。

这只是一个例子,程序应该具备自检测功能,在启动的时候,能够检测自己依赖的文件是否有效。否则,不管不顾就跑起来,未必不会破坏生产环境的文件。

一旦文件破坏,程序应该具备恢复功能,这种恢复可以是从备份文件中恢复,也可以从默认内容中恢复。

README

如果是一个软件项目,也许都会加上一个readme,说这个软件叫啥,是谁写的;但对于一个系统里面的脚本程序,却很少有人写点说明信息。

比如这个程序是为了解决什么问题而编写的,和其他程序之间有怎样的联系,应该注意什么问题,有哪些是需要进一步完善的,有怎样的更新历史。。。。。。

这方面,缺少的不是readme,而是forgive me, forget me, or kill me

后记

最喜欢的就是挖坑,当到处都是坑的时候,难免自己也掉进坑里,好自为之。

吴羽舒 wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!