shell脚本学习指南
以下八点不敢说就能成为你shell脚本学习指南de全部,至少可以让你编写出可靠的shell脚本。
1. 指定bash
shell 脚本的第一行,#!之后应该是什么?
如果拿这个问题去问别人,不同的人的回答可能各不相同。我见过/usr/bin/env bash,也见过/bin/bash,还有/usr/bin/bash,还有/bin/sh,还有/usr/bin/env sh。这算是编程界的“’茴’字四种写法”了。
在多数情况下,以上五种写法都是等价的。但是,写过程序的人都知道:“少数情况”里往往隐藏着意想不到的坑。
如果系统的默认 shell 不是 bash 怎么办?比如某 Linux 发行版的某个版本,默认的 sh 就不是 bash。
如果系统的 bash 不是在 /usr/bin/bash 怎么办?
我推荐使用 /usr/bin/env bash 和 /bin/bash。前者通过env添加一个中间层,让env在$PATH中搜索bash;后者则是官方背书的,约定俗成的 bash 位置,/usr/bin/bash不过是指向它的一个符号链接。
2. set -e 和 set -x
OK,经过一番讨论,现在第一行定下来了。接下来该开始写第二行了吧? 且慢!在你开始构思并写下具体的代码逻辑之前,先插入一行set -e和一行set -x。
set -x会在执行每一行 shell 脚本时,把执行的内容输出来。它可以让你看到当前执行的情况,里面涉及的变量也会被替换成实际的值。
set -e会在执行出错时结束程序,就像其他语言中的“抛出异常”一样。(准确说,不是所有出错的时候都会结束程序,见下面的注)
注:set -e结束程序的条件比较复杂,在man bash里面,足足用了一段话描述各种情景。大多数执行都会在出错时退出,除非 shell 命令位于以下情况:
一个 pipeline 的非结尾部分,比如 error | ok
一个组合语句的非结尾部分,比如 ok && error || other
一连串语句的非结尾部分,比如 error; ok
位于判断语句内,包括 test、if、 while 等等。
这两个组合在一起用,可以在 debug 的时候替你节省许多时间。出于防御性编程的考虑,有必要在写第一行具体的代码之前就插入它们。扪心自问,写代码的时候能够一次写对的次数有多少?大多数代码,在提交之前,通常都经历过反复调试修改的过程。与其在焦头烂额之际才引入这两个配置,不如一开始就给 debug 留下余地。在代码终于可以提交之后,再考虑是否保留它们也不迟。
3. 带上shellcheck
好了,现在我已经有了三行(样板)代码,具体的业务逻辑一行都没写呢。是不是该开始写了? 且慢!工欲善其事,必先利其器。这次,我就介绍一个 shell 脚本编写神器:shellcheck
说来惭愧,虽然写了几年 shell 脚本,有些语法我还是记不清楚。这时候就要依仗 shellcheck 指点一下了。
shellcheck 除了可以提醒语法问题以外,还能检查出 shell 脚本编写常见的 bad code(入门教程qkxue.net)。本来我的N条建议里面,还有几条是关于这些 bad code 的,不过考虑到 shellcheck 完全可以发掘出这些问题,于是忍痛把它们都剔除在外了。毫无疑问,使用 shellcheck 给我的 shell 编写技能带来了巨大的飞跃。
所谓“站在巨人的肩膀上”,虽然我们这些新兵蛋子,技能不如老兵们强,但是我们可以在装备上赶上对方啊!动动手安装一下,就能结识一个循循善诱的“老师”,何乐而不为?
顺便一提,shellcheck 居然是用 haskell 写的。谁说 haskell 只能用来装逼?
4. 变量展开
在 shell 脚本中,偶尔可以看到这样的做法:echo $xxx | awk/sed/grep/cut...。看起来大张形势的样子,其实不过是想修改一个变量的值。
杀鸡何必用牛刀?bash 内建的变量展开机制已经足以满足你各种需求!还是老方法, read the f**k manaul! man bash 然后搜索Parameter Expansion,下面就是你想要的技巧。键者也写过一篇相关的文章,希望能助上一臂之力:玩转 Bash变量
5. 注意local
随着代码越写越多,你开始把重复的逻辑提炼成函数。有可能你会掉到 bash 的一个坑里。在 bash,如果不加 local 限定词,变量默认都是全局的。变量默认全局——这跟 js 和 lua 相似;但相较而言,很少有 bash 教程一开始就告知你这个事实。在*作用域里,是否是全局变量并不重要。但是在函数里面,声明一个全局变量可能会污染到其他作用域(尤其在你根本没有注意到这一点的情况下,腾云科技ty300.com)。所以,对于在函数内声明的变量,请务必记得加上 local 限定词。
6. trap 信号
如果你写过稍微复杂点的在后台运行的程序,应该知道 posix 标准里面“信号”是什么一回事。如果不知道,直接看下一段。像其他语言一样,shell 也支持处理信号。trap sighandler INT可以在接收到 SIGINT 时调用 sighandler 函数。捕获其他信号的方式以此类推。
不过 trap 的主要应用场景可不是捕获哪个信号。trap 命令支持“捕获”许多不同的流程——准确来说,允许用户给特定的流程注入函数调用。其中最为常用的是trap func EXIT和trap func ERR。
trap func EXIT允许在脚本结束时调用函数。由于无论正常退出抑或异常退出,所注册的函数都能得以调用,在需要调用一个清理函数的场景下,我都是用它注册清理函数,而不是简单地在脚本结尾调用清理函数。
trap func ERR允许在运行出错时调用函数。一个常用的技法是,使用全局变量ERROR存储错误信息,然后在注册的函数中根据存储的值完成对应的错误报告。把原本四分五裂的错误处理逻辑集中到一处,有时候会起奇效。不过要记住,程序异常退出时,既会调用EXIT注册的函数,也会调用ERR注册的函数。
稿源:勤快学QKXue.NET