最小的安全Bash脚本模板

2020-12-15 19:41:11

Bash脚本。几乎任何人迟早都要写一个。几乎没有人说“是的,我喜欢写”。这就是为什么几乎每个人在编写它们时都会注意的不多的原因。

我不会尝试让您成为Bash专家(因为我也不是),但是我将向您展示一个最小的模板,它将使您的脚本更安全。您不需要感谢我,您未来的自我将会感谢您。

相反,就像骑自行车一样就像用bash编程一样。这是一个短语,它意味着无论您执行多少次操作,都必须每次都重新学习它。

-杰克·沃顿(@JakeWharton)2020年12月2日

但是Bash与另一种广受欢迎的语言有一些共同点。就像JavaScript一样,它不会轻易消失。虽然我们可以希望Bash不会成为所有内容的主要语言,但它总是遥遥无期。

Bash继承了Shell宝座,几乎可以在所有Linux(包括Docker映像)上找到。这是大多数后端运行的环境。因此,如果您需要脚本化服务器应用程序启动,CI / CD步骤或集成测试运行的脚本,那么Bash可以满足您的需求。

为了将几个命令粘合在一起,将输出从一个传递到另一个,然后仅启动一些可执行文件,Bash是最简单,最原生的解决方案。尽管用其他语言编写更大,更复杂的脚本是很有意义的,但您不能指望Python,Ruby,fish或您认为最好的其他解释器随处可见。在将其添加到产品服务器,Docker映像或CI环境之前,您可能应该三思而后再考虑。

但是Bash远非完美。语法是一场噩梦。错误处理很困难。到处都有地雷。我们必须处理它。

#!/ usr / bin / env bashset -Eeuo pipefailcd" $(dirname" $ {BASH_SOURCE [0]}")" > / dev / null 2& 1陷阱清除SIGINT SIGTERM ERR EXITusage(){cat<< EOFUsage:$(basename" $ 0")[-h] [-v] [-f ] -p param_value arg1 [arg2 ...]此处的脚本描述。可用选项:-h,--help打印此帮助并退出-v,--verbose打印脚本调试信息-f,--flag一些标志说明-p ,--param某些参数说明。EOF出口} cleanup(){陷阱-SIGINT SIGTERM ERR EXIT#脚本清理此处} setup_colors(){如果[[-t 2]]&& [[--z" $ {NO_COLOR-}" ]]&& [[" $ {TERM-}" !="哑巴" ]];然后NOCOLOR =' \ 033 [0m' RED =' \ 033 [0; 31m' GREEN =' \ 033 [0; 32m' ORANGE =' \ 033 [0; 33m' BLUE =' \ 033 [0; 34m' PURPLE =' \ 033 [0; 35m' CYAN =' \ 033 [0; 36m' YELLOW =' \ 033 [1; 33m' else NOCOLOR ='' RED ='' GREEN ='' ORANGE ='' BLUE ='' PURPLE =''青色='' YELLOW ='' fi} msg(){echo>& 2 -e" $ {1-}"} die(){local msg = $ 1 local code = $ {2-1}#默认退出状态1 msg" $ msg"退出" $ code"} parse_params(){#从params设置的变量的默认值flag = 0 param =''而:做案例" $ {1-}"在-h | --help)用法;; -v | --verbose)set -x ;; --no-color)NO_COLOR = 1 ;; -f | --flag)#示例标志flag = 1 ;; -p | --param)#示例命名参数param =" $ {2-}"移-?*)die"未知选项:$ 1" ;; *)打破;; esac shift done args =(" $ @")#检查所需的参数和参数[[-z" $ {param-}" ]]&& die"缺少必需参数:param" [[$ {#args [@]} -eq 0]]&&死"缺少脚本参数"返回0} parse_params" $ @" setup_colors#脚本逻辑此处msg" $ {RED}读取参数:$ {NOCOLOR}" msg"-标志:$ {flag} " msg"-参数:$ {param}" msg"-参数:$ {args [*]-}"

想法是不要使它太长。我不想滚动500行到脚本逻辑。同时,我希望为任何脚本打下坚实的基础。但是Bash缺少任何形式的依赖项管理,这使它变得不那么容易。

一种解决方案是使用包含所有样板代码和实用程序功能的单独脚本,并在开始时执行它。不利之处是必须始终将第二个文件附加到任何地方,从而一路失去“简单的Bash脚本”的想法。因此,我决定只在模板中放入我认为是最小的模板,以使其尽可能短。

传统上,脚本是从shebang开始的。为了获得最佳兼容性,它直接引用/ usr / bin / env,而不是/ bin / bash。虽然,如果您阅读链接的StackOverflow问题中的注释,有时甚至会失败。

set命令更改脚本执行选项。例如,通常Bash不在乎某些命令是否失败,返回非零退出状态代码。它只是高兴地跳到下一个。现在考虑这个小脚本:

如果backups目录不存在,将会发生什么?的确,您会在控制台中收到一条错误消息,但是在您能够做出反应之前,第二条命令已经删除了该文件。

有关正确设置哪些选项的详细信息-Eeuo pipefail更改以及它们如何保护您,请参考我几年来在书签中看到的文章。

下一行会尽力定义脚本的位置目录,然后我们对其进行cd。为什么?

因为我们的脚本通常在相对于脚本位置的路径上运行,所以复制文件并执行命令(假设脚本目录也是工作目录)。而且,只要我们从脚本目录执行脚本即可。

那么我们的脚本不是在项目目录中运行,而是在CI工具的某些完全不同的工作目录中运行。我们可以通过执行脚本之前转到目录来修复它:

但是在脚本方面解决这个问题要好得多。它可以为我们节省一些调试(甚至只是修复和重新运行作业),还可以使我们从所需的任何位置执行脚本。

陷阱清除SIGINT SIGTERM ERR EXITcleanup(){陷阱-SIGINT SIGTERM ERR EXIT#此处的脚本清理}

考虑一下陷阱,就像脚本的finally块一样。在脚本末尾–由错误或外部信号引起的正常–将执行cleanup()函数。例如,在这里,您可以尝试删除脚本创建的所有临时文件。

只要记住,不仅可以在最后调用cleanup(),还可以让脚本完成工作的任何部分。您尝试清除的所有资源不一定都将存在。

usage(){cat<< EOFUsage:$(basename" $ 0")[-h] [-v] [-f] -p param_value arg1 [arg2 ...]此处的脚本描述。 ... EOF出口}

由于use()相对接近脚本的顶部,它将以两种方式起作用:

向不知道所有选项并且不想遍历整个脚本来发现它们的人显示帮助,

作为当有人修改脚本时的最小文档(例如,您两周后甚至根本不记得写过脚本)。

我不争辩说这里记录所有功能。但是,最低限度必须是简短而优美的脚本用法消息。

setup_colors(){如果[[-t 2]]&& [[--z" $ {NO_COLOR-}" ]]&& [[" $ {TERM-}" !="哑巴" ]];然后NOCOLOR =' \ 033 [0m' RED =' \ 033 [0; 31m' GREEN =' \ 033 [0; 32m' ORANGE =' \ 033 [0; 33m' BLUE =' \ 033 [0; 34m' PURPLE =' \ 033 [0; 35m' CYAN =' \ 033 [0; 36m' YELLOW =' \ 033 [1; 33m' else NOCOLOR ='' RED ='' GREEN ='' ORANGE ='' BLUE ='' PURPLE =''青色='' YELLOW ='' fi} msg(){echo>& 2 -e" $ {1-}"}

首先,如果您仍然不想在文本中使用颜色,请删除setup_colors()函数。之所以保留它,是因为我知道,如果我不必每次都用Google代码搜索颜色,就会使用更多颜色。

其次,这些颜色只能与msg()函数一起使用,而不能与echo命令一起使用。

msg()函数用于打印所有不是脚本输出的内容。这不仅包括错误,还包括所有日志和消息。引用了出色的12因素CLI应用文章:

因此,在大多数情况下,您都不应为标准输出使用颜色。

使用msg()打印的消息将发送到stderr流,并支持特殊序列,例如颜色。如果stderr输出不是交互式终端或通过了标准参数之一,则无论如何都会禁用颜色。

要在stderr不是交互式终端时检查其行为,请在脚本中添加如上的一行。然后执行它,将stderr重定向到stdout并将其管道传输到cat。管道操作使输出不再直接发送到终端,而是直接发送到下一个命令,因此现在应禁用颜色。

$ ./test.sh 2>& 1 | cat这是一条非常重要的消息,但不是脚本输出值!

parse_params(){#从params设置的变量的默认值flag = 0 param =''而:做案例" $ {1-}"在-h | --help)用法;; -v | --verbose)set -x ;; --no-color)NO_COLOR = 1 ;; -f | --flag)#示例标志flag = 1 ;; -p | --param)#示例命名参数param =" $ {2-}"移-?*)die"未知选项:$ 1" ;; *)打破;; esac shift done args =(" $ @")#检查所需的参数和参数[[-z" $ {param-}" ]]&& die"缺少必需参数:param" [[$ {#args [@]} -eq 0]]&&死"缺少脚本参数"返回0}

如果脚本中有什么对参数化有意义的,我通常会这样做。即使仅在单个位置使用脚本。它使复制和重新使用它变得更容易,这通常早于迟发生。另外,即使需要硬编码,通常在更高的级别上比Bash脚本更好。

CLI参数主要有三种类型-标志,命名参数和位置参数。 parse_params()函数支持所有这些功能。

唯一的公共参数模式(此处未处理)是连接的多个单字母标志。为了能够将两个标志(而不是-a -b)传递为-ab,将需要一些其他代码。

while循环是一种手动解析参数的方法。在每种其他语言中,您应该使用内置解析器或可用的库之一,但是,这就是Bash。

模板中包含示例标志(-f)和命名参数(-p)。只需更改或复制它们即可添加其他参数。并且不要忘了以后再更新usage()。

重要的是,当您仅将第一个google结果用于Bash参数解析时通常会丢失,这是在未知选项上引发错误。脚本收到未知选项的事实意味着用户希望它执行脚本无法完成的操作。因此,用户期望和脚本行为可能完全不同。最好在不良情况发生之前完全阻止执行。

Bash中有两种解析参数的方法。是getopt和getopts。有赞成和反对使用它们的论点。我发现这些工具不是最好的,因为默认情况下,macOS上的getopt表现完全不同,而且getopts不支持长参数(例如--help)。

好吧,实际上,这是很诚实的建议。使用Bash,没有通用的npm安装等效项。

parse_params()中的参数–保留--help和--no-color,但替换示例:-f和-p

我在MacOS(默认,古老的Bash 3.2)和几个Docker映像上测试了该模板:Debian,Ubuntu,CentOS,Amazon Linux,Fedora。有用。

显然,它不能在缺少Bash的环境(如Alpine Linux)上使用。作为一种极简主义的系统,Alpine使用非常轻巧的灰(Almquist外壳)。

您可以提出一个问题,那就是使用兼容Bourne shell的脚本(几乎可以在任何地方使用)是否更好。至少在我看来,答案是否定的。 Bash更安全,功能更强大(但仍不容易使用),因此我可以接受缺乏对我很少需要处理的Linux发行版的支持。

使用Bash或其他更好的语言创建CLI脚本时,有一些通用规则。这些资源将指导您如何使小型脚本和大型CLI应用程序可靠:

我不是创建Bash脚本模板的第一个,也不是最后一个。 一个很好的选择是这个项目,尽管对于我的日常需求来说有点太大了。 毕竟,我尝试使Bash脚本尽可能小(并且很少)。 编写Bash脚本时,请使用支持ShellCheck linter的IDE,例如JetBrains IDE。 这将阻止您执行可能会适得其反的一系列操作。 如果您发现模板有任何问题,或者您认为缺少重要的内容,请在评论中告诉我。