以下 Bash 函数给出了不一致的结果:
# $1 Path to ZIP archive.
# Exits with 0 status iff it contains a “.mp3” or “.flac” file.
mp3_or_flac_in_zip() {
local archive=${1:?No archive given.}
(
set -o pipefail
unzip -l "$archive" | grep -iqE '.\.(flac|mp3)$'
)
}
当在同一个包含音乐的 ZIP 上连续运行n次时,它会随机报告其中没有音乐(大约 1-5% 的时间,但在不同的 ZIP 之间差异很大)。
切换到中间变量而不是管道(使用&&
而不是set -o pipefail
仍然确保unzip
运行良好)解决了不一致问题:
# $1 Path to ZIP archive.
# Exits with 0 status iff it contains a “.mp3” or “.flac” file.
mp3_or_flac_in_zip() {
local archive=${1:?No archive given.}
local listing
listing=$(unzip -l "$archive") &&
grep -iqE '.\.(flac|mp3)$' <<< "$listing"
}
那里可能存在什么问题?还有什么情况下管道不是个好主意?
它基本上看起来像https://stackoverflow.com/questions/19120263/why-exit-code-141-with-grep-q的另一个案例,我通过查看我收到的返回状态 (141) 发现了这一点。
在某些运行中,
unzip
似乎有“足够的时间”来完成其工作,而在其他运行中,它会被杀死,grep
因为grep
很快找到了匹配项。由于pipefail
处于打开状态,这会将错误状态与整个命令套件相关联。因此,几乎任何与的组合foo | bar
都可能遇到类似的问题。foo | grep -q
pipefail
ZIP 之间的失败概率差异可能是由于这些 ZIP 中的文件数量(较短列表与较长列表)以及这些列表中第一个 grep 匹配文件的位置造成的。
该
foo_output=$(foo)
方法确保foo
始终有机会完成其工作而不会被杀死,但当然它会导致较低的性能(grep -q
在第一次匹配后杀死输入命令是故意的,可以节省时间和资源)。可以说是无力的妥协
仍然使用管道,但不使用
pipefail
,并捕获stderr
以检查它是否为空,并假设没有错误或警告意味着unzip
运行良好。我看到的主要问题是,由于与特定 ZIP 的非致命怪癖相关的基本警告,它可能导致失败,但在某种程度上,这也是问题中的其他方法的情况,我认为:(这让我想知道是否有更好的方法来检查ZIP 中是否存在特定类型的文件。这似乎很有可能。)
编辑:更现实的方法
正如该页面的讨论所指出的那样,在这种情况下的实用性
pipefail
充其量是值得怀疑的,因为无论如何,失败unzip
都不会(在标准输出上)输出任何满足 的内容grep
。此外,如果出现不愉快的情况unzip
,原因可能会通过 stderr 显示。更有趣的是,非致命问题(根据 往往会产生非零状态man unzip
)也不会阻止我们通过这种方式查找文件。最后的想法
恕我直言,这又是另一个理由,说我们不应该在没有意识到潜在后果的情况下一直切换无数的 Bash 标志,即使一些多用途“脚本模板”宣传
set -o pipefail
和set -e
(除其他外)作为神奇地避免错误的方法。对于他们避免的每个问题,如果您不留意,它们通常会产生双倍的潜在问题(在公司环境中,您无法强迫每个人都花一天时间阅读,情况会变得更糟man bash
)。在编写脚本多年后,我仍然偶尔会遇到此类警告。在目前的情况下,我以为使用( … )
子 shell 尽可能地将效果保持pipefail
在本地可以保证我的安全,结果却在子 shell 中发现了一个问题,就在我眼皮底下。您可以执行以下操作:
为了
grep
读取整个输入(找到所有匹配并报告它们,我们会丢弃它们,即使我们只关心是否至少有一个匹配)并避免在第一个匹配后退出bsdtar
时被 SIGPIPE 杀死。grep
我已将其替换
unzip
为libarchive的bsdtar
CLI 界面,因为unzip
它无法处理任意文件路径,因为它会解释通配符¹。这也意味着我们可以处理其他类型的档案。我已经将
archive
变量赋值移到了子 shell 内部,所以我们不需要非标准的local
,并且${var?error}
只退出该子 shell,因此是函数而不是整个脚本。我已经将其替换
.
为[^/]
,以避免匹配,path/to/.flac
因为我推测这是您的意图.
。无论如何,如果您想检查
unzip
/bsdtar
和grep
是否成功,因为例如,即使.mp3
可以找到某些文件,您也希望能够检测到损坏的档案,那么您需要让unzip
/bsdtar
运行到最后,因此您需要使用其所有输出。除了从 切换
grep -q
到之外grep > /dev/null
,作为一种更通用的方法,您还可以执行以下操作:bsdtar
除了获取/unzip
打印档案成员的完整列表并用 进行处理的替代方法外grep
,您还可以使用它bsdcpio
来仅列出您想要的成员,然后检查是否有一些输出:或者:
现在,该方法的一个问题是,即使所有名称以
.mp3
或结尾的文件都是目录.flac
类型,它也会返回 true 。该方法会排除那些在列表中的文件,目录类型的文件在其名称后附加。无论使用符号链接的方法如何,都存在类似的问题。bsdtar|grep
bsdtar
/
请注意,作为子 shell 的替代,自 bash 4.4 起,您可以使用
local -
Almquish shell 中的类似功能或像的set -o localoptions
来zsh
将选项设置更改(仅限由 设置的更改set
,而不是由 设置的更改shopt
)设置为函数本地设置:与 zsh 相同:
或者 ksh93(该
pipefail
选项最初来自这里):只要您使用 Korn 风格的函数定义,对选项的更改始终是局部的。还请注意,在 ksh93 中,这些选项的更改(局部变量也是如此)不会传播到内部调用的其他函数,因此您的函数可以安全地调用期望选项关闭的
$archive
其他函数,而无需自行执行。pipefail
set +o pipefail
¹ 即使在类 Unix 系统中
*
,其中 、?
与文件路径中的任何字符一样有效,并且文件名生成由 shell 完成,如果没有 、 和 则无法打开文件,也会尝试使用或附加来unzip
读取文件,并且 unlike不支持不可查找的文件;它更像是一个 MSDOS 程序。.zip
.ZIP
bsdtar
zipinfo
,通常与 一起提供unzip
,如果加上它的-1
选项,那么也是一个更好的选择,尽管它也存在与 相同的问题unzip
。如果左侧命令的完成很重要,您可以
grep
不执行-q
,而只需将输出重定向到即可/dev/null
。退出状态应该相同(但 grep 当然在这里做了一些不必要的工作)。如果左侧是否完成或因管道关闭而终止并不重要,那么您可以专门检查对应于 SIGPIPE 的退出状态并将其视为零。
在 Bash 中,您还可以检查
$PIPESTATUS
数组以检查哪个命令失败以及处于何种状态。