如何在Bash中安全地做事

2021-04-04 01:17:10

永久性链接,如C或驾驶汽车,当代壳牌语言需要一些知识和纪律来安全使用,但不是说它可以' t完成。

Shellharden建议,并且可以应用,以删除壳牌中的脆性。这符合Shellcheck和Bashpitfalls - Shellharden不同意这些。

问题是,并非所有脚本都将只是删除他们的脆性,因为这是他们的工作原理,并且必须相当不同。这些循环中的人类的需要和整体方法。

这是这种方法的目标和实现,即将Bash脚本可以重写成伟大的脚本,这是一种没有那些语言实际上施加的惯用错误的表示。这是因为糟糕的语言特征是有限的,每个都有一个替代品。

不幸的是,当它不是看似简单的时,很难捍卫做某事的正确方法。在这态度,Python宣言(Python3 -c'导入这个'),说应该只有一个明显的做事方式,而且#34;显式比隐含的和#34更好;,发挥了很多意义。这说明了一些不可能让广大用户采用的人采用的东西安全方法,仍然可以为那些关心的人。

鱼是一种缓解 - 易于使用,但(仍然)缺乏严格的模式。 →取消资格。

这原本原则上是错误的问题。始终使用Job™.shellscript语言的正确工具是用于运行程序的语言,并且使用该块作为构建块。这是一个自己的域。

这绝不是Shellscripting.shellscripts继续写入,这就是如何安全地完成。然而,有一个更大的罪,而不是写出显而易见的东西。当你知道你有一个shellscript,你知道担心的是,您可以带来正确的专业知识,并且您拥有shell linters的完整阿森纳。如果隐含地调用Quoting不正确地调用shell,那么不多。

如果有类似驾驶员和#39;安全性Bash编码的许可证,它必须是Bashpitfalls的规则零:始终使用引号。

一个未引发的变量将被视为武装炸弹:它在与空格和通配符联系后爆炸。是的,"爆炸"与将字符串分成阵列一样。具体而言,像$ var一样的可变扩展,也像$(cmd)一样,以$(cmd)相同的命令替换,其中字符串在特殊$ ifs变量中的任何字符上拆分,默认情况下是空格。此外,生成单词中的任何通配符(*)用于扩展这些单词以匹配文件系统上的文件(间接路径名扩展)。这大多是隐形的,因为大多数情况下,结果是一个1元素数组,它与原始字符串值无法区分。

引用不需要例外情况,但由于引用永远不会伤害,并且当您看到一个未引用的变量时,一般规则是害怕的,因为为了读者,追求非明显的例外是值得怀疑的。它看起来不对,错误的做法足以提出怀疑:用损坏的文件处理频繁处理文件名常常避免的空格来写入足够的脚本...

只有在讨论风格中的例外情况 - 欢迎忽略它们。为了风格中立,Shellharden确实尊重一些例外:

双括号之间的神奇背景([[和]) - 这是自己的语言。

虽然可以正确使用此样式,但它更难:反击需要在嵌套时逃逸,并且野外的示例比不是不正确的报价。

总是在理论上始终使用大括号,但在您的作者中没有受伤,但在您的经验中,不必要使用括号和正确使用引号之间存在强烈的负相关性 - 几乎每个人都选择"坏和兼容#34;而不是"好但冗长"形式!

害怕错误的事情:而不是担心真正的危险(丢失的报价),初学者可能担心命名为$前缀的变量会影响" $ prefix_postfix&#34的扩展; - 这根本不是它的工作原理。

该决定是为了禁止不必要的括号使用:Shellharden将把所有这些变体重写为最简单的良好形式。

因为shellharden不是风格格式化器,因为它不应该改变正确的代码。这是"好(串联)&#34的真实;示例:就贝拉图界而言,这是圣洁(规范纠正)的形式。

Shellharden目前根据需要添加和删除括号:在错误的例子中,Var1与括号内插,但即使在良好(插值)案例中也不会在Var2上接受括号,因为它们永远不会在A结束时所需的细绳。后一种要求可能会被提升。

与普通标识符变量名称不同(在Regex中:[_A-ZA-Z] [_ A-ZA-Z0-9] *),编号参数需要大括号(字符串插值或不)。 Shellcheck说:

这被视为太微妙地修复或忽略:如果看到这一点,贝拉德将打印大错误消息和保释。

为了能够引用所有变量,您必须使用真实数组,当您需要的东西时,而不是空白分隔字符串。

语法是冗长的,但克服它。此Bashism单手中取消了POSIX Shell以获取本指南的目的。

files =(a b)flocicates =()for f中的" $ {files [@]}&#34 ;;如果cmp - " $ f"其他/" $ f&#34 ;;然后重复+ =(" $ f")fidoneif [" $ {#duplicates [@]}" -gt 0];然后rm - " $ {副本[@]}" fi

文件=" \ a \ b \"重复= f for f f for $文件;如果cmp - " $ f"其他/" $ f&#34 ;;然后重复+ =" $ f" fidoneif! [" $副本" ='' ];然后rm - $ duplicatesfi

看看这两个例子是多么相似:使用真实阵列而不是字符串之间没有算法差异,因为(坏)替换为(坏)替换物。对于不需要的行持续,将奖励点转到阵列语法,使得这些行成为评论。它们是可能的当然不是等同的,作为"坏"示例使用空白分隔的字符串,一旦文件名包含空格,并且删除错误文件的风险就会删除。

是第二个例子可固定吗?理论上,是的;在实践中,编号是可以在字符串中表示列表,即使是已知合适的分隔符,也可以转移,它变为毛茸茸(逃逸和未分隔的分隔符),以便掌握100%.worse,将其恢复到数组中表格无法抽出(尝试设置 - ABC在功能中)。最终的打击是,如果您可以选择不同的语言,那么毫无意义地争取这种抽象失败。

阵列是没有正确编程的荒谬不切实际的功能。这就是为什么:

您需要一些数据结构,可以采用零个或多个值,以便干净地传递零个或多个值。

特别是,命令参数基本上是数组。提示:shell脚本是关于命令和参数的。

无论如何,所有POSIX Shell都暗中支持阵列,以参数列表" $ @"

因此,本指南的建议必须是第一个思想的Posix兼容性。特此声明Posix Shell标准宣布不适合我们的目的。很可悲的是,对于Don' t支持阵列的划线和灰烬的简约Posix兼容壳。 zsh,它支持zh,它支持zhash' s阵列语法的超集,所以它很好。

缺乏阵列支持的简约外壳是嵌入式计算机的虚拟机,其中发货另一种语言是成本敏感的,但对安全的期望很高。 Busybox对你的小尺寸令人印象深刻,而是作为它的一部分,你得到灰烬,这是头发拉皮。

这适用于除NUL之外的任何分隔字节(无UTF-8或多字符分隔符串)。为了使其在NUL工作,纠正文字$' \ 0'代替$ SEP。

将分隔符附加到结束的原因是字段分隔符真的是字段终止者(Postfix,而不是infix)。区分对最终空场的概念很重要。如果您的输入已终止,请跳过此操作。

ReadArray获得了一个小的减号,仅用于使用ASCII分隔符(仍然没有UTF-8)。

如果分隔符由多个字节组成,则可以通过字符串处理(例如通过参数替换)正确地执行此操作。

否则邪恶的ifs变量在read命令中有一个合法用途,在没有调用间接路径名扩展的情况下,它可以用作另一种方式来分隔字段。通过请求多个变量或使用阵列选项来读取的间接路径名扩展。禁用分隔符-D''我们一直阅读到底。因为读取返回nonzero遇到结尾时,如果启用了,它必须防范错误(|| true)。

Tab,NewLine和Space - 当IFS设置为其中一个时,3个角案例 - 读取丢弃空字段!因为这通常很有用,但此方法使推荐列表的底部而不是取消资格。

直观的修复 - 管道进入循环 - 并不总是很酷,因为管道运营商' s的右操作数成为一个子shell.not这对这个愚蠢的例子很重要,但它会惊讶地发现这个循环可以' T操纵外部变量:

为避免未来的惊喜,大部分代码通常不应该是子屏幕。这是正确的:

#!/ usr / bin / env bashif test" $ bash" ="" || " $ bash" -uc" a =();真\" \ $ {a [@]} \"" 2> / dev / null;然后#bash 4.4,zsh set -euo pipefailelse#bash 4.3和旧扼流圈与set -u的空阵列上。 set -eo pipefailfishopt -s nullglob globstarrequire(){哈希" $ @" ||退出127;要求......要求......要求......

Hashbang:可移植性考虑:Env的绝对路径可能比Bash的绝对路径更便携。案例在点:nixos。 POSIX任务是env的存在,但Bash不是POSIX的事情。

安全考虑:在这里,没有语言味道选项 - 这里!在使用ENV重定向时实际上并不是可能的,但即使您的哈希班始于#!/ bin / bash,它不是影响脚本含义的选项的正确位置,因为它可以被覆盖,这将是有可能以错误的方式运行脚本。但是,Don' t影响脚本的含义的选项,例如set-x将是一个奖励,以便更过度地(如果使用)。

我们需要从Bash' s的非官方严格模式,set -u后面的特征检查。我们不需要所有的bash' s严格的模式,因为符合shellcheck / shellharden兼容的意味着引用一切,这是一个超出严格模式的级别。此外,SET -U不得在BASH 4.3及更早版本中使用。因为在这些版本中,在这些版本中将空阵列视为未设置,这使得对于本文描述的目的来说是不可用的阵列。随着阵列是本指南中的第二个最具不风化的建议(引用)和我们&#39的唯一原因;重新牺牲POSIX兼容性,即当然是不可接受的:如果使用SET -U,请使用BASH 4.4或使用BASH 4.4或另一个Sane壳牌ZSH。如果有可能有人可以用过时的Bash运行脚本,则这比完成这一点更容易。幸运的是,与set -u的适用也将在没有(与set -e不同)工作。因此,为什么将其放在一个特征检查后面是SANE。请注意,使用Bash 4.4兼容shell发生测试和开发(因此脚本的SET -U方面进行了测试)。如果此问题涉及您,您的其他选项将放弃兼容性(如果特征检查失败,则通过失败)或放弃SET -U。

shopt -s nullglob是在* .txt与零文件匹配时正确工作的f in * .txt。默认行为(aka。passglob) - 通过模式,如果它发生匹配是什么 - 由于几个原因是危险的。至于Globstar,可以实现递归的Globbing。 Globbing比找到的更容易使用。所以用它。

断言命令依赖项已安装。声明您的依赖关系有很多好处,但直到这变得静止验证,在此处集中在罕见的命令。您的动机应该是防止最终失败的长期运行脚本,以及预防Make -J" $(nproc)&#34等不端行为成为一枚叉子炸弹。为此目的使用HASH的好处是它的低开销,并且它在故障情况下为您提供错误消息。它不做什么' t检查是间接依赖关系和兼容性级别,但在那时,我们想要包管理。

将内部字段分隔符设置为空字符串禁用Word Splitting。听起来像圣杯。可悲的是,这是引用变量和命令替换的完全替代,并且鉴于您将使用引号,这会给您带来任何引号。您必须使用引号的原因是,否则,空字符串变为空数组(如测试$ x =""),间接路径扩展仍处于活动状态。此外,使用此变量的搞乱也会使用像读取的命令使用它,打破Cat / etc / fstab等构造读取-r dev mnt fs opt dump pass; printf'%s \ n' " $ fs&#34 ;;完成'

禁用通配符扩展:不仅仅是一个臭名昭着的间接,而且也是未解决的直接的一个,那我说你应该想要使用。所以这是一个艰难的卖。而这也应该完全不必要地掌握shellcheck / shellharden符合物的脚本。

作为nullglob的替代方案,如果匹配零匹配,则vereglob失败。虽然这对大多数命令进行了意义,例如RM - * .txt(因为大多数采用文件参数的命令Don'当然,无论如何都会被零呼叫,但显然,只有在您的情况下只能使用FAILGLOB能够假设零匹配胜利' t发生。这只是意味着您大多数人赢得了'除非您可以假设相同,否则将在命令参数中放置通配符。但是总是可以完成的,是使用nullglob,让模式扩展到一个可以采用零参数的构造中的零参数,例如for循环或数组分配(txt_files =(*。txt))。

有一种错误的方法来结束一个bash脚本:让用作条件的命令是执行的最后一个命令,使脚本"失败" IFF最后一个条件是假的。这可能遇到脚本可能是正确的,它是一种编码看起来意外的退出状态的方法,并且通过添加或删除代码来易于破坏到结尾。

这里的正确标准是最后一个陈述遵循" errexit基础"以下。在疑问时,以显式退出状态结束脚本:

背景:如果未使用的命令作为条件返回非零,则解释器在该点退出。

Don' t shimp上的if-stand。你可以'&&作为没有总是使用的速记IF-陈述||作为别人分支。否则,脚本如果条件为false则终止。

要在使用它作为条件时捕获命令' s输出,请使用赋值作为条件(但在下面请在不使用当地的作业上):

如果在所有使用退出状态变量$ $?使用errexit,当然没有替代指挥成功的直接检查(否则,您的脚本在非零时直播,以查看此变量)。推论:失败的情况是扩展退出状态变量$唯一有意义的地方吗? (因为成功只有一个退出状态,我们正在检查)。第二个陷阱是,如果我们将命令作为检查的一部分否定命令,则退出状态将是否定命令的,这是一个删除的有用信息的布尔值。

如果errexit执行它的事情,请使用它来设置任何必要的清理以在退出时发生。

这是一个很好的被削弱的叉子炸弹,我学到了艰难的方式 - 我的构建脚本在各种开发人员机器上工作了很好,但带来了我的公司' s buildserver到膝盖:

有时,Posix是残酷的。如果呼叫者正在检查其成功,则在函数,组命令甚至子屏幕中忽略errexit。尽管所有的理智,这些例子都印刷了不可达和巨大的成功。

这使得Bash与errexit实际上是无法分散的 - 可以包装errexit函数,以便他们仍然工作,但它可以节省的努力(超出显式错误处理)变得可疑。请考虑拆分成完全独立的脚本。

在双括号内部[[]],未引入的变量和命令替换是安全的(来自字分离和间接路径名扩展)。 '我们是一个问题的部分解决方案,我们在本指南之后遵循本指南,暗示在任何地方都没有开始。如果你是,你在攻击你的脚本之后,你应该ann' t。

让'脱离了这个神话。如有疑问,请询问类型命令:

> type testtest是一个shell内置>类型[[[是shell内置>类型[[[[[[是shell关键字

此参数两种方式:[[有更宽容的语法,因为它是语法,而不是命令。其他地方需要引用。较少的例外,令人震惊的混乱。如果要进行教育,请使用测试命令 - 它是诚实的是命令,而不是语法。

模式匹配([[$ path == * .png || $ path == * .gif]]):这是什么情况。

逻辑运营商:通常的嫌疑人和amp;&和||工作只是精细 - 外部命令 - 并且可以与组命令分组:如果{true ||错误的; }&&真的;然后回声1;否则回声0; FI。

检查是否存在变量([-varName]]):是的,这可能是一个杀手争论,但考虑始终设置变量的编程样式,所以您需要检查它们是否存在。

正确的方法是不是惯用POSIX / BASH脚本的特征。考虑始终设置变量尽可能避免问题,因此您需要检查它们是否存在。

通过给出变量默认值,您可以获得长路。这甚至在BusyBox中运行:

但是,如果您必须知道,则Bash 4.2(也验证了ZSH 5.6.2)的正确方法是否存在了检查变量是否存在。

如果使用此问题,有人可能会尝试使用早期的BASH版本运行脚本,请记住早期失败。特征测试方法将在脚本的开头进行测试,对于我们所知道的变量,并且如果结果是错误的,则终止。在这种情况下,我们在早期版本中获得语法错误,并免费终止,因此可以将其添加到开头部分以下:

最后,不要使用像[-n $ var]或[-z $ var]这样的构造。它们基本上与空字符串的串串比较,只能更少可读。但是,本节的重要事项是他们的功能批评:

字符串比较可以' t区分从空的一个未命令变量。更不用说辨别它可以为空的方式:环境变量只是字符串,因此它们可能是空字符串,但正常的shell变量是真正的数组 - 它们可以是空字符串或空字符串的空阵列(您认为是空字符串与一个元素阵列无法区分)。

与任何命令一样,必须有一种方法来控制其选项解析以防止它将其解释为选项。标准的方式来表示选项结束的标准方法是具有双重划线的。

因此,GNU VERSIO

......