当前位置: 首页 > 科技观察

写出靠谱的shell脚本的八个建议

时间:2023-03-14 08:53:10 科技观察

这八个建议是主要作者在过去几年写shell脚本的一些经验和教训中得出的。其实我开始写的时候,不止这几条。想了想,去掉了一些无关紧要的项目,最后有八个项目。毫不夸张的说,每一件都是精挑细选的,虽然有几点是陈词滥调。1.指定bashshell脚本的第一行,#!之后应该是什么?如果你问别人这个问题,不同的人可能会有不同的答案。我见过/usr/bin/envbash、/bin/bash、/usr/bin/bash、/bin/sh和/usr/bin/envsh。这也算是编程界的“分字四种写法”。在大多数情况下,以上五种写法是等价的。但是,写过程序的人都知道,“小案子”往往隐藏着意想不到的坑。如果系统默认的shell不是bash怎么办?例如,在某个版本的Linux发行版中,默认的sh不是bash。如果系统的bash不在/usr/bin/bash中怎么办?我推荐使用/usr/bin/envbash和/bin/bash。前者通过env增加一个中间层,让env在$PATH中搜索bash;后者是官方认可的,习惯的bash位置/usr/bin/bash只是指向它的符号链接。2.set-e和set-xOK,经过一番讨论,现在第一行定下来了。是时候开始写第二行了吗?等待!在开始构思和编写具体的代码逻辑之前,插入一行set-e和一行set-x。set-x会在执行每行shell脚本时输出执行的内容。它可以让你看到当前的执行情况,所涉及的变量将被替换为实际值。set-e会在出现错误时结束程序,就像其他语言中的“抛出异常”一样。(准确的说并不是所有的错误都会结束程序,见下面的注释)注意:set-e结束程序的条件比较复杂。在manbash中,一个段落用于描述各种场景。大多数执行都会因错误而退出,除非shell命令位于:管道的非最终部分,例如error|ok复合语句的非最终部分,例如ok&&error||other语句序列的非最终部分,例如错误;ok位于判断语句中,包括test、if、while等,这两个结合使用可以为你在调试时节省很多时间。出于防御性编程的原因,有必要在编写第一行具体代码之前插入它们。问问自己,写代码的时候,一次能写对多少次?大多数代码在提交之前通常会经过反复调试和修改。与其在走投无路的时候引入这两个配置,还不如一开始就留有调试的余地。在代码最终可以提交之后,再考虑是否保留它们也不迟。3.带上shellcheck,现在我有三行(boilerplate)代码,而且我还没有写一行具体的业务逻辑。是时候开始写作了吗?等一下!工欲善其事,必先利其器。这次给大家介绍一个shell脚本的神器:shellcheck,说来惭愧,虽然我写了几年的shell脚本,但是有些语法还是记不住。这时候就必须要依靠shellcheck来引导了。除了提醒语法问题,shellcheck还可以检查出shell脚本中常见的错误代码。本来在我的N条建议中,还有几条与这些坏代码有关,但考虑到shellcheck可以完全发现这些问题,我才勉强全部排除。毫无疑问,使用shellcheck使我的shell编写技能有了巨大的飞跃。正所谓“站在巨人的肩膀上”,虽然我们这些新人的技能不如老兵,但是在装备上我们是可以赶上对方的!动手安装一下,就能结识一位善于教学的“老师”,何乐何乐而不为呢?顺便说一下,shellcheck实际上是用haskell编写的。谁说haskell只能用来装逼?4.变量在shell脚本中展开,偶尔可以看到这样的做法:echo$xxx|awk/sed/grep/剪切...看起来是一张大图,其实只是想修改一个变量的值。为什么要用大锤杀鸡?bash内置的变量扩展机制足以满足你的各种需求!还是老方法,读他妈的手册!manbash并搜索ParameterExpansion,以下是您想要的技术。keyer也写了一篇相关的文章,希望能有所帮助:玩转Bash变量5.注意,随着你写的本地代码越来越多,你开始将重复的逻辑提炼成函数。您很可能会陷入bash的困境。在bash中,如果不添加局部限定符,变量默认为全局变量。变量默认是全局的——这点类似于js和lua;但相比之下,很少有bash教程从一开始就告诉你这个事实。在顶级范围内,变量是否是全局的并不重要。但是在函数内部,声明一个全局变量会污染其他作用域(尤其是在您甚至没有注意到它的情况下)。因此,对于在函数内声明的变量,请记住添加局部限定符。6.Trapsignal如果你写过稍微复杂一点的后台运行的程序,你应该知道posix标准中的“信号”是什么。不知道的直接看下一段。与其他语言一样,shell支持处理信号。trapsighandlerINT可以在收到SIGINT时调用sighandler函数。捕获其他信号的方式类似。但是trap的主要应用场景并不是捕获哪个信号。trap命令支持“捕获”许多不同的进程——具体来说,允许用户将函数调用注入特定进程。最常用的是trapfuncEXIT和trapfuncERR。trapfuncEXIT允许在脚本末尾调用函数。由于注册的函数无论是正常退出还是异常退出都可以调用,所以在需要调用清理函数的场景下,我用它来注册清理函数,而不是简单的在脚本末尾调用清理函数。trapfuncERR允许在发生错误时调用函数。一个常用的技巧是使用全局变量ERROR来存储错误信息,然后在注册的函数中根据存储的值完成相应的错误报告。将原本零散的错误处理逻辑集中在一个地方有时会产生奇迹。但是要记住,当程序异常退出时,EXIT注册的函数和ERR注册的函数都会被调用。7.三思而后行。以上是具体的建议,剩下的两个比较务实。这条建议被称为“三思而后行”。其实不管写什么代码,哪怕只是一个辅助脚本,也要三思而后行,切忌大意。不,在编写脚本时记住这一点更为重要。毕竟,很多时候,一个复杂的脚本都是从几个小命令开始的。最初编写此脚本的人可能认为这只是一次性任务。难免在代码中对一些外部条件做出一些假设,这在当时可能是正常的,但是随着外部环境的变化,这些就成了暗礁。更糟糕的是,几乎没有人会测试脚本。除非你去运行它,否则不知道它是否仍然有效。要减缓脚本代码的腐烂,需要在编写时明确哪些依赖会发生变化,哪些是脚本正常运行不可或缺的。必须有适当的抽象和可变的代码;同时,要有防御性编程意识和自己代码的护城河。8.扬长避短有时候,用shell写脚本,意味着移植困难,错误难以统一处理,数据处理难整齐。虽然使用外部命令可以方便快捷地实现各种复杂的功能,但作为硬币的反面,我们不得不依靠grep、sed、awk等工具将它们粘合在一起。如果有兼容多平台的需求,就得小心避免BSD和GNUcoreutils、bash版本差异等诡异陷阱。由于缺乏完整的数据结构和一致的API,shell脚本无法处理复杂的逻辑。解决特定问题需要使用正确的工具。知道何时使用shell以及何时切换到另一种更通用的脚本语言(例如ruby??/python/perl)也是编写可靠的shell脚本的诀窍。如果你的任务可以通过组合常用的命令来完成,并且只涉及简单的数据,那么shell脚本就是锤子。如果你的任务包含比较复杂的逻辑和复杂的数据结构,那么你需要用像ruby/python这样的语言编写脚本。