Bash陷阱

2022-02-17 14:06:11

或者,y';知道吗,停止使用expr。通过使用参数展开,您可以完成expr所做的一切。什么';上面那东西想干什么?删除单词的第一个字母?这可以在POSIX shell中使用PE或子字符串扩展来实现:说真的,有';除非你';我们在Solaris上安装了不符合POSIX的/bin/sh.It';这是一个外部过程,所以它';它比进程中的字符串操作慢得多。因为没有人使用它,所以没有人知道它是什么';这样一来,你的代码就变得很模糊,很难维护。一般来说:Unix UTF-8文本不使用BOM。纯文本的编码由语言环境、mime类型或其他元数据决定。虽然BOM的存在通常不会损坏仅用于人类阅读的UTF-8文档,但在任何旨在通过脚本、源代码、配置文件等自动化过程进行解释的文本文件中,BOM都是有问题的(通常在语法上是非法的)。以BOM开头的文件应与以MS-DOS换行符开头的文件一样被视为外来文件。在shell脚本中:';当UTF-8在8位环境中透明使用时,BOM的使用将干扰任何协议或文件格式,这些协议或文件格式在开始时需要特定的ASCII字符,例如使用";#" 在Unix shell脚本的开头' http://unicode.org/faq/utf_bom.html#bom5没有';这个表达式没有任何错误,但是您应该知道命令替换(所有形式:`,$(…)$(<;文件),`<;文件`,和${。。。;}(ksh))删除所有尾随的换行符。这通常是无关紧要的,甚至是可取的,但是如果您必须保留文字输出,包括任何可能的尾随换行符,这会变得很棘手,因为您无法知道输出中是否有它们或有多少。一个丑陋但可用的解决方法是在命令替换中添加后缀,并在外部删除它:一个可移植性较差但可以说更漂亮的解决方案是使用带空分隔符的读取Ksh(或启用lastpipe的bash 4.2+)readlink-fn--34$迪鲁路";|IFS=read-rd'' 绝对路径

这种方法的缺点是,除非命令输出一个NUL字节,导致只读取部分流,否则读取总是返回false。获取命令退出状态的唯一方法是通过PIPESTATUS。您还可以故意输出一个NUL字节,强制read返回true,并使用pipefail。set-o pipefail{readlink-fn--";$dir#u path";&;printf';\0';}IFS=read-rd'' 绝对路径设置+o管道故障

这在一定程度上是一个可移植性混乱,因为Bash同时支持pipefail和PIPESTATUS,ksh93只支持pipefail,只有mksh的最新版本支持pipefail,而早期版本只支持PIPESTATUS。此外,为了使读取停止在NUL字节,需要一个前沿ksh93版本。防止程序将传递给它们的文件名解释为选项的一种方法是使用路径名(参见上面的陷阱3)。对于当前目录下的文件,名称的前缀可以是相对路径名。/。对于像**这样的图案然而,问题可能会出现,因为它与表单的字符串匹配/文件名。在一个简单的情况下,您可以直接使用glob来生成所需的匹配项。但是,如果需要单独的模式匹配步骤(例如,结果已预处理并存储在数组中,需要进行过滤),可以通过在模式中考虑前缀:[[$file!=./*.]]或从匹配中剥离模式来解决Bash shopt-s nullglob表示路径in./*;执行[${path##*/}!=*.]&&;rm"$路";完成#或者更好地归档*;执行[[$file!=*.]]&&;rm"/$文件";完成#或更好地在*中归档;做rm"/$文件";完成

另一种可能是用一个参数来表示选项的结束。(同样,请参见#pf3)。shopt-s nullglob,用于*中的文件;执行[[$file!=*.]]&&;rm--"$文件";完成

这是迄今为止最常见的涉及重定向的错误,通常由希望将stdout和stderr都指向文件或管道的人执行,他们会尝试这样做,但不理解为什么stderr仍然出现在他们的终端上。如果你';如果你对此感到困惑,你可能不会';我不明白重定向或文件描述符是如何工作的。在执行命令之前,将从左向右计算重定向。这个语义不正确的代码本质上意味着:";首先将标准错误重定向到标准输出当前指向的位置(tty),然后将标准输出重定向到日志文件";。这是倒退。标准错误已经发送到tty。改为使用以下内容:请参阅更深入的解释、已解释的复制描述符和BashGuide-重定向。$?仅当需要检索上一个命令的确切状态时才需要。如果只需要测试成功或失败(任何非零状态),只需直接测试命令即可。e、 g:对照备选方案列表检查退出状态可能遵循如下模式:cmd status=$?案例$status in 0)echo success>&;2.1) echo和#39;必须提供一个参数,正在退出' >&;2出口1;;*)echo和#34;未知错误$状态,正在退出" >&;2出口"$地位";以撒

给定给算术展开或复合命令的代码经过初始的展开和替换过程,以生成要作为算术表达式进行分析和计算的文本。这件事必须小心处理。例如,通过将一个代码片段扩展为另一个代码片段,将此表达式缝合在一起。$x='$(日期>;&;2)和#39;#重定向只是为了让我们可以看到一切发生的情况$y=$(数组[";$x";])#引用don';我帮不上忙。阵列没有';甚至不必存在2014年6月2日星期一10:49:08美国东部夏令时

接下来,将扩展字符串传递给算术处理器,算术处理器需要获取shell中数组变量的引用';使用查找函数解析变量';s";姓名";。这个名称解析程序采用一个字符串数组[$(date>;&;2)],由名称组成,包括索引和括号内的所有文字代码,就像read或printf-v do一样,变量名作为参数传递。变量解析器执行扩展,包括命令替换,以解析索引。大多数情况下,在算术展开中不需要使用任何类型的展开。尽可能直接在表达式中使用变量名(no$)(即位置参数和POSIX";特殊变量";)除外)。在使用变量之前验证变量,并确保扩展只生成数字文本——大多数问题都会自动避免。转义任何展开式,将其传递到表达式中,而不首先展开:#典型任务是将某些列读入关联数组。排版-A arr printf-v偏移量#39;%(%s)T'-1而IFS='' read-rxy;执行[$x$y==+([0-9])+([0-9])]]#验证输入(请参见下一个陷阱)((arr[\$(date-d";@$x";+%F)]=y-offset))#转义替换逐字传递整个表达式。完成

另一种选择是将let与单引号参数一起使用。((expr))相当于let";expr";(双引号args)。在算术上下文中使用num之前,请始终验证您的输入(请参阅BashFAQ/054),因为它允许代码注入。尽管看起来难以置信,POSIX要求将IFS作为字段终止符,而不是字段分隔符来处理。在我们的例子中,这意味着如果有';如果输入行末尾有一个空字段,它将被丢弃:空字段去了哪里?它之所以被食用是因为历史原因(因为它一直都是这样的)。这种行为不是bash独有的;所有共形壳都能做到这一点。一个非空字段被正确地扫描:那么,我们如何处理这些废话呢?事实证明,在输入字符串的末尾附加一个IFS字符将迫使扫描工作。如果有尾随的空字段,则额外的IFS字符";终止";这样就可以扫描了。如果有尾随的非空字段,IFS字符将创建一个新的空字段,该字段将被删除。$输入=";a、 b和#34;$如果s=,则读取-ra字段<<<"$输入,";$declare-p字段declare-a字段=';([0]=";a";[1]=";b";[2]=";";)'

不要导出CDPATH。在中设置CDPATH。bashrc不是问题,但导出它将导致您运行的任何bash或sh脚本(碰巧使用cd)可能会改变行为。有两个问题。执行以下操作的脚本:可以将目录改为~/myProject/some/dir,而不是/some/dir,具体取决于当时存在的目录。因此,cd可能会成功,并将脚本带到错误的目录,以下命令现在运行在与预期不同的目录中,可能会产生有害影响。第二个问题是,当cd在捕获输出的上下文中运行时:作为设置CDPATH的副作用,cd将向stdout输出类似于/home/user/some/dir的内容,以指示它通过CDPATH找到了一个目录,而CDPATH反过来将与某个命令的预期输出一起出现在输出变量中。脚本可以通过总是预加前缀使自己不受从环境继承的CDPATH的影响。/到相对路径,或在脚本开头运行unset CDPATH,但不要';不要以为每个编剧都考虑过这个陷阱,所以不要';t导出CDPATH。直接分配一个变量';临时变量的s值为';光靠它还不足以恢复它的状态。即使初始变量未设置,赋值始终会产生一个集合但为空的临时变量。对于IFS来说,这是一个特殊的问题,因为空IFS与未设置IFS的含义完全不同,将IFS设置为一个或两个命令的临时值是一个常见要求。一个简单的解决方法是指定一个前缀来区分set和unset VAR,然后在完成时将其剥离。如果可能的话,通常最好使用局部变量。次壳是另一种可能性。使用原始$(…)填充数组是不安全的命令替换。该命令的输出将经历分词(在所有空格上,甚至在引号内的空格上),然后进行全局搜索。如果有';这是一个像*或者呃?或[abc]在结果中,它将根据当前工作目录中的文件名展开。要选择替换,您需要知道该命令是将其输出写入单行还是多行。如果是';只有一行:如果是';它有多行(而且你的目标是bash 4.0或更高版本):如果它';s多行(并且您希望与bash 3.x兼容,或者希望您的命令的退出状态反映在读取操作的成功或失败中,而不依赖于仅在bash 4.4及更新版本中可用的行为):这将防止全局绑定。它仍然赢了';如果需要避免在引用的空格上拆分,则无法提供帮助,但不幸的是,bash无法处理这种情况。对于通用CSV(逗号分隔值)文件处理,您确实需要切换到具有专用CSV输入库的语言。GNU xargs支持并行运行多个作业-其中n是并行运行的作业数。seq 100 | xargs-n1-P10回波和#34$a";|grep 5 seq 100 | xargs-n1-P10 echo和#34$a">;我的输出。txt

这在许多情况下都可以正常工作,但有一个欺骗性的缺陷:如果$a包含超过8192个字符(限制取决于平台和版本),回送可能不是原子的(它可能被拆分为多个write()调用),并且存在两行混合的风险。$perl-e';打印和#34;a";x10000和#34;\n"' >;foo$strace-e write bash-c';read-r foo<;傅;echo和#34$34岁' >/dev/null write(1,";aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa+++

显然,如果有多个对echo或printf的调用,就会出现同样的问题:slowprint(){printf';Start-%s';";$1";sleep";$1";printf';%s-End\n';";$1";}export-f slowprint seq 10 | xargs-n1-I{}-P4 bash-c";慢打印{}";#与没有并行化相比,seq 10 | xargs-n1-I{}bash-c";慢打印{}";#确保在下一个陷阱中看到警告!

并行作业的输出混合在一起,因为每个作业由两个(或更多)单独的write()调用组成。如果您需要不混合的输出,因此建议使用确保输出序列化的工具(例如GNU Parallel)。有关更多详细信息,请参阅混合问题的演示。此命令包含代码注入漏洞。find找到的文件名被注入shell命令并由sh解析;或美元(。。。)然后文件名可以由`sh';作为代码执行;。34岁;慢打印";前一个陷阱中的例子是,如果输入不是';t保证是整数。更准确地说,POSIX find并没有指定是否扩展包含多于{}的参数。GNU find允许这种代码注入发生。其他实现选择了更安全的路径:#uname-a HP-UX imadev B.10.20 a 9000/785 2008897791双用户许可证#find/dev/null-exec sh-c';回声{}';\;{}

正确的方法是将filename参数与script参数分开:在执行命令之前执行重定向。通常情况下,这并不意味着';没关系,但是对于sudo,我们有一个命令是作为不同于重定向的用户执行的。如果重定向必须以sudo授予的权限执行,那么您需要一个包装器:而不是一个可以使用tee的包装器:如果mycmd有很多引用,这可能更容易编写。这与之前的陷阱非常相似。Globbing也在命令执行之前完成。如果目录不是';如果您的普通用户权限无法读取,那么您可能需要在具有sudo授权权限的shell中完成全局绑定:不要将stdin、stdout或stderr作为"关闭;速记";用于重定向到/dev/null。正确地写出来。为什么?考虑当程序试图向STDRR写入错误消息时会发生什么。如果stderr已被重定向到/dev/null,那么写操作将成功,并且您的程序可以自由地继续运行,因为它已经认真地报告了错误情况。但是如果stderr已经关闭,那么写操作将失败。在这一点上,你的程序可能会做一些不可预测的事情。它可能会继续并忽略故障,或者考虑到执行环境已被破坏,无法安全地继续,它可能会立即退出。或者当程序的世界变成反乌托邦地狱时,程序员决定程序应该做的任何事情。所有程序都应确保stdin、stdout和stderr将存在,并以适当和合理的方式可读写。关闭其中一个,就违反了你对这个项目的承诺。这是不可接受的。当然,一个更好的解决方案是将错误记录在某个地方,这样你就可以返回并阅读它们,找出什么';这是不对的。xargs在空白处拆分。这是不幸的,因为文件名中允许空白,GUI用户通常使用空白。xargs还治疗';和";特别是,这也可能导致问题:这里xargs警告:#不要做这个$find-f | xargs wc xargs类型:不匹配的单引号;默认情况下,除非使用-0选项,否则引号对xargs是专用的

在这里,xargs根本没有警告:#不要这样回显*|xargs wc find*著名*-类型f | xargs wc find*4*-类型f | xargs wc

而是使用xargs-0:#这样做而不是printf';%s\0';*|xargs-0wc-find-f型——名称和#39*著名的*'-打印0 | xargs-0 wc查找-f型——名称和#39*4*' -exec wc{}+

如果使用-0并不简单,另一种方法是使用GNU Parallel,它会在\n上拆分。虽然文件名中也允许使用\n,但除非您的用户是恶意的,否则它们永远不会出现。在任何情况下:如果使用不带-0的xargs,请在代码中添加注释,解释为什么在特定情况下这样做是安全的。将索引数组元素传递给unset时,需要将其引用。否则,它可能会被视为glob,并根据当前目录中的文件展开。如果碰巧有一个名为a0的文件,那么glob将扩展为a0,最后执行unset a0。多次给date打电话是个坏主意。想象一下,如果第一次通话发生在4月30日午夜前一毫秒,而第二次通话发生在5月1日午夜后一毫秒,会发生什么。最终的结果是月=04,日=01。它';最好只调用一次date,在一次调用中检索所有需要的字段。一个常见的习语是:或与bash';s(4.2或更高版本)printf builtin:记住,月或日的名称是依赖于区域设置的,因此在%A或%B附近加引号是为了避免日或月的名称包含空格或其他特殊字符的区域设置出现问题。或者,您可以检索历元格式的时间戳(自1970年初起的秒数),然后根据需要使用该时间戳生成人类可读的日期/时间字段现在需要bash 4.2或更高版本printf-v';%(%s)T'-1#Or now=$EPOCHSECONDS在bash 5.0中#-1可以在4.3或更高版本的printf-v month';%中省略(%m)T'"$现在";printf-v day';%(%d)T'"$现在";

如果你的系统';s strftime()不';如果不支持%s,您可以使用以下命令获得大纪元时间:强制基数10解释仅适用于无符号数字。只要$i包含一个没有前导或+的数字串,一切都很好。但是,如果$i可能是负数,那么这种转换可能会失败,要么是响亮地(带有错误消息),要么更糟,是无声地(只是产生错误的结果)。如果有';如果$i有可能是负数,请用这个来代替:有关解释,请参阅算术表达式。在脚本开始时启用这些选项有很多缺陷。errexit(set-e)尝试在发生错误时中止脚本,这一点一开始听起来不错,但它有非常复杂的规则来决定何时在发生错误时中止。errexit的一些主要问题是';shell实际上不可能检测到错误。它只需要一个命令';s退出状态。当命令失败时,它们通常返回非零退出状态,但许多命令也使用退出状态来传递真/假值。此类命令的示例有test、[、[。。。 ]], ((...)), 还有格雷普。

当使用if或&&;或者| |是一个函数,set-e忽略该函数中命令的非零退出状态。考虑一个函数,比如如果CD命令失败,你肯定不知道39。我不想让rm命令运行,在启用errexit时只需简单地使用该函数,情况正好是这样的:set-e cleanup(){cd";$1";printf';Oops!\n';}cleanup/no/more/there#scriptname:cd:/no/more/there:没有这样的文件或目录

但后来您决定添加一条自定义错误消息cleanup/no/more/there | |{printf>;&;2';cleanup failed\n'

......