在 Bash 中,双方括号 [[ ]] 是否比单方括号 [ ] 更可取?

Are double square brackets [[ ]] preferable over single square brackets [ ] in Bash?

提问人:Leonard 提问时间:3/21/2009 最后编辑:Peter MortensenLeonard 更新时间:10/14/2023 访问量:388339

问:

一位同事最近在代码审查中声称,该结构在诸如[[ ]][ ]

if [ "`id -nu`" = "$someuser" ] ; then
     echo "I love you madly, $someuser"
fi

他无法提供理由。有吗?

bash if-statement 语法

评论

33赞 rook 8/16/2013
要灵活,有时允许自己听取建议,而不需要其深入的解释:)至于它的代码是好的和清晰的,但请记住,有一天你会用默认的 shell 将你的脚本作品移植到系统上,而不是 或 ,等等。 更丑陋,更麻烦,但在任何情况下都可以工作。[[bashksh[AK-47
402赞 Josip Rodin 4/5/2016
@rook 当然,你可以在没有深入解释的情况下听取建议。但是,当您要求解释但没有得到解释时,这通常是一个危险信号。“信任,但要验证”等等。
19赞 codeforester 9/2/2018
另请参阅:Unix 和 Linux SE 上的 Bash 运算符 [[ vs [ vs ( vs ((?
30赞 111 4/2/2020
@rook换句话说,“按照你的吩咐去做,不要问问题”
30赞 5/25/2020
@rook 这毫无意义。

答:

71赞 anon 3/21/2009 #1

[[ ]]具有更多功能 - 我建议您查看高级 Bash 脚本指南以获取更多信息,特别是第 7 章中的扩展测试命令部分。测试

顺便说一句,正如指南所指出的,它是在 ksh88(1988 年版的 KornShell)中引入的。[[ ]]

评论

8赞 Henk Langeveld 8/31/2012
这远非抨击主义,它最初是在 Korn shell 中引入的。
11赞 Charles Duffy 10/4/2014
@Thomas,ABS实际上在许多圈子里被认为是一个非常糟糕的参考;虽然它有大量准确的信息,但它往往很少注意避免在其示例中展示不良做法,并且花费了大量时间进行维护。
2赞 Thomas 10/14/2014
@CharlesDuffy感谢您的评论,您能说出一个好的选择吗?我不是 shell 脚本方面的专家,我正在寻找一个可以参考的指南,大约每半年编写一次脚本。
13赞 Charles Duffy 10/14/2014
@Thomas,我使用的是 Wooledge BashFAQ 和相关的 wiki;它们由 Freenode #bash 频道的居民积极维护(他们虽然有时很棘手,但往往非常关心正确性)。BashFAQ #31 mywiki.wooledge.org/BashFAQ/031 是与此问题直接相关的页面。
23赞 f3lix 3/21/2009 #2

哪个比较器、测试、支架或双支架最快?

双括号是“复合 命令“,其中作为测试和单 括号是 shell 内置的(和 实际是相同的命令)。因此 单支架和双支架 执行不同的代码。

测试和单括号是 最便携的,因为它们以 单独的外部命令。 但是,如果您远程使用任何 现代版的BASH,双 支持括号。

评论

11赞 Jens 5/13/2012
对 shell 脚本中最快的痴迷是怎么回事?我希望它最便携,并且不在乎可能带来的改进。但是,我是一个老派的老屁:-)[[
1赞 dubiousjim 5/31/2012
我认为许多 shell 证明了内置版本,即使外部版本也存在。[test
4赞 michael 8/1/2013
@Jens总的来说,我同意:脚本的全部目的是(曾经?)可移植性(否则,我们会编码和编译,而不是脚本)......我能想到的两个例外是:(1)制表符补全(补全脚本可能会变得非常长,有很多条件逻辑);(2)超级提示(和/或);对于这些,我不希望脚本的执行有任何明显的延迟PS1=...crazy stuff...$PROMPT_COMMAND
2赞 Ron Burk 5/17/2017
对最快的一些痴迷只是风格。在其他条件相同的情况下,为什么不将更高效的代码构造合并到默认样式中,特别是如果该构造也提供了更高的可读性?就可移植性而言,bash 适合的许多任务本质上是不可移植的。例如,如果距离上次运行已超过 X 小时,我需要运行。当人们可以将可移植性从已经太长的代码约束列表中剔除时,这是一种极大的解脱。apt-get update
0赞 Gabriel Staples 10/14/2023
@Jens和其他人,以下是我刚刚在 [[[ 之间所做的一些速度测试。我知道你说你不在乎,但这很有趣,有时很重要。
862赞 Johannes Schaub - litb 3/21/2009 #3

[[意外较少,使用起来通常更安全。但它不是可移植的 - POSIX 没有指定它的作用,只有一些 shell 支持它(除了 bash,我听说 ksh 也支持它)。例如,您可以做

[[ -e $b ]]

以测试文件是否存在。但是有了 ,你必须引用 ,因为它分裂了参数并扩展了诸如(从字面上理解它的地方)之类的东西。这也与如何成为一个外部程序并像其他程序一样正常地接收其参数有关(尽管它也可以是内置的,但它仍然没有这种特殊的处理)。[$b"a*"[[[

[[还有一些其他不错的功能,比如正则表达式与运算符匹配,就像它们在类 C 语言中一样。这里有一个很好的页面:test、[[[ 和 Bash 测试有什么区别=~

评论

42赞 guns 3/22/2009
考虑到 bash 现在无处不在,我倾向于认为它非常便携。对我来说,唯一常见的例外是在 busybox 平台上 - 但考虑到它运行的硬件,您可能希望为它做出特别的努力。
30赞 Lightness Races in Orbit 8/21/2011
@guns:确实如此。我建议你的第二句话反驳你的第一句话;如果将软件开发视为一个整体,“Bash 无处不在”和“Busybox 平台除外”是完全不兼容的。Busybox 在嵌入式开发中非常广泛。
15赞 dubiousjim 5/31/2012
@guns:即使 bash 安装在我的盒子上,它也可能没有执行脚本(我可能会将 /bin/sh 符号链接到一些破折号变体,这是 FreeBSD 和 Ubuntu 的标准)。Busybox 还有额外的奇怪之处:现在使用标准编译选项,它会解析但将其解释为与 .[[ ]][ ]
84赞 Nick Matteo 3/5/2013
@dubiousjim:如果你使用仅限 bash 的构造(比如这个),你应该有 #!/bin/bash,而不是 #!/bin/sh。
26赞 anthony 2/17/2016
这就是为什么我让我的脚本使用,但一旦我依赖一些 BASH 特定功能,就会将它们切换为使用,以表示它不再是 Bourne shell 可移植的。#!/bin/sh#!/bin/bash
-1赞 unix4linux 11/25/2010 #4

简而言之,[[ 更好,因为它不会分叉另一个进程。没有括号或单括号比双括号慢,因为它会分叉另一个进程。

评论

13赞 A B 7/7/2011
test 和 [ 是 bash 中同一内置命令的名称。尝试使用来查看此内容。type [
1赞 Tim Gilbert 11/16/2012
@alberge,这是真的,但是,与 不同的是,是由 bash 命令行解释器解释的语法。在 bash 中,尝试键入 .unix4linux 是正确的,尽管经典的 Bourne-shell 测试分叉出一个新进程来确定真值,但语法(由 bash、zsh 等借用 ksh)却没有。[[[type [[[[[
2赞 A B 11/18/2012
@Tim,我不确定你说的是哪个 Bourne shell,但它内置于 Bash 和 Dash(在所有 Debian 衍生的 Linux 发行版中)。[/bin/sh
2赞 Tim Gilbert 11/29/2012
哦,我明白你的意思了,这是真的。我正在考虑类似的东西,比如说,在较旧的 Solaris 或 HP/UX 系统上使用 /bin/sh,但当然,如果您需要与那些您也不会使用的 [[ 兼容。
1赞 apaderno 7/18/2017
@alberge Bourne shell 不是 Bash(又名 Bourne Again SHell)。
0赞 scavenger 3/19/2013 #5

[[ ]] 在某些版本的 SunOS 下不支持双括号,并且完全不支持以下函数内部声明:

GNU Bash,版本 2.02.0(1)-release (sparc-sun-solaris2.6)

评论

1赞 michael 8/1/2013
非常真实,而且一点也不无关紧要。必须考虑旧版本之间的 bash 可移植性。人们说“bash 是无处不在的和可移植的,除了可能(在这里插入深奥的操作系统)”——但根据我的经验,solaris 是必须特别注意可移植性的平台之一:不仅要考虑较新操作系统上的旧 bash 版本、带有数组、函数等的问题/错误;但是,即使是像 tr、sed、awk、tar 这样的实用程序(在脚本中使用)在 Solaris 上也有奇怪的地方,你必须解决这些问题。
2赞 scavenger 8/4/2013
你说得太对了......Solaris 上有如此多的非 POSIX 实用程序,只需查看“df”输出和参数......太阳的耻辱。 希望它正在一点一点地消失(除了在加拿大)。
12赞 peterh 10/25/2013
Solaris 2.6 真的吗?它于 1997 年发布,并于 2006 年结束支持。我想如果你还在使用它,那么你就会有其他问题!顺便说一句,它使用了 Bash v2.02,它是引入双括号的版本,因此即使在这样古老的东西上也应该有效。2005 年的 Solaris 10 使用 Bash 3.2.51,2011 年的 Solaris 11 使用 Bash 4.1.11。
1赞 peterh 10/25/2013
Re Script 可移植性。在 Linux 系统上,每个工具通常只有版本,那就是 GNU 版本。在 Solaris 上,您通常可以选择 Solaris 原生版本或 GNU 版本(例如 Solaris tar 和 GNU tar)。如果你依赖于GNU特定的扩展,那么你必须在你的脚本中声明它,以便它是可移植的。在Solaris上,你可以用“g”作为前缀,例如,如果你想要GNU grep,可以用'ggrep'来做到这一点。
6赞 Vicente Bolea 3/22/2015 #6

无法使用的典型情况是在 autotools configure.ac 脚本中。括号有特殊和不同的含义,所以你将不得不使用代替或 -- 注意,test 和 是同一个程序。[[test[[[[

评论

3赞 Gordon 2/4/2018
既然 autotools 不是 POSIX shell,为什么还要被定义为 POSIX shell 函数呢?[
0赞 vy32 6/13/2018
因为 autoconf 脚本看起来像一个 shell 脚本,它生成一个 shell 脚本,并且大多数 shell 命令都在其中运行。
397赞 Ciro Santilli OurBigBook.com 11/30/2017 #7

行为差异

Bash 4.3.11 上的一些差异:

  • POSIX vs Bash 扩展:

  • 常规命令与魔法

    • [只是一个名字很奇怪的常规命令。

      ]只是 的最后一个参数。[

      Ubuntu 16.04 实际上有一个由 coreutils 提供的可执行文件,但 Bash 内置版本优先。/usr/bin/[

      Bash 解析命令的方式不会发生任何变化。

      特别是,是重定向,并连接多个命令,生成子 shell,除非转义为 ,并且单词扩展照常发生。<&&||( )\

    • [[ X ]]是一个单一的结构,可以神奇地解析。、 和 被特殊对待,分词规则不同。X<&&||()

      还有进一步的区别,如 和 。==~

    在 Bashese 中:是一个内置命令,是一个关键字:shell builtin 和 shell 关键字有什么区别?[[[

  • <

  • &&||

    • [[ a = a && b = b ]]:true、logical
    • [ a = a && b = b ]:语法错误,解析为 AND 命令分隔符&&cmd1 && cmd2
    • [ a = a ] && [ b = b ]:POSIX可靠的等价物
    • [ a = a -a b = b ]:几乎是等价的,但被 POSIX 弃用,因为它很疯狂,并且对于某些值失败 or like or 将被解释为逻辑操作ab!(
  • (

    • [[ (a = a || a = b) && a = b ]]:假。没有它将是真的,因为比( )[[ && ]][[ || ]]
    • [ ( a = a ) ]:语法错误,被解释为子 shell()
    • [ \( a = a -o a = b \) -a a = b ]: 等效项,但 、 和 被 POSIX 弃用。没有它将是真的,因为比()-a-o\( \)-a-o
    • { [ a = a ] || [ a = b ]; } && [ a = b ]未弃用的 POSIX 等效项。然而,在这种特殊情况下,我们可以只写: ,因为 和 shell 运算符具有相等的优先级,这与 和 和 和 不同[ a = a ] || [ a = b ] && [ a = b ]||&&[[ || ]][[ && ]]-o-a[
  • 扩展时的单词拆分和文件名生成 (split+glob)

    • x='a b'; [[ $x = 'a b' ]]:真。不需要引号
    • x='a b'; [ $x = 'a b' ]:语法错误。它扩展到[ a b = 'a b' ]
    • x='*'; [ $x = 'a b' ]:如果当前目录中有多个文件,则出现语法错误。
    • x='a b'; [ "$x" = 'a b' ]:POSIX等价物
  • =

    • [[ ab = a? ]]:true,因为它可以进行模式匹配(很神奇)。不会 glob 扩展到当前目录中的文件。* ? [
    • [ ab = a? ]:glob 扩展。因此,它可能是 true 或 false,具体取决于当前目录中的文件。a?
    • [ ab = a\? ]:false,而不是 glob 扩展
    • =和 在 和 中相同,但是一个 Bash 扩展。==[[[==
    • case ab in (a?) echo match; esac:POSIX等价物
    • [[ ab =~ 'ab?' ]]:false,在 Bash 3.2 及更高版本中失去魔力,并且未启用与 Bash 3.1 的兼容性(如''BASH_COMPAT=3.1)
    • [[ ab? =~ 'ab?' ]]:真
  • =~

    • [[ ab =~ ab? ]]:真。POSIX 扩展正则表达式匹配,不进行 glob 扩展?
    • [ a =~ a ]:语法错误。没有 Bash 等效项。
    • printf 'ab\n' | grep -Eq 'ab?':POSIX 等效项(仅限单行数据)
    • awk 'BEGIN{exit !(ARGV[1] ~ ARGV[2])}' ab 'ab?':POSIX等价物。

建议:始终使用 []

我见过的每个结构都有 POSIX 等价物。[[ ]]

如果您使用:[[ ]]

  • 失去可移植性
  • 迫使读者了解另一个 Bash 扩展的复杂性。 只是一个名字很奇怪的常规命令,不涉及特殊语义。[

感谢 Stéphane Chazelas 的重要更正和补充。

评论

2赞 Wlad 9/5/2020
这是否成立:“在POSIX中工作的所有内容在BASH中都有效,但反之亦然。
2赞 Ciro Santilli OurBigBook.com 9/5/2020
@Wlad Bash 极大地扩展了 POSIX,因此任何 Bash 扩展都不会是 POSIX。相反,我不是 100%,但感觉很有可能(除了当 Bash 扩展覆盖 POSIX 语法时,例如,在 posix 中也可能是一个常规命令)。相关新闻: askubuntu.com/questions/766270/...[[
1赞 PePa 10/6/2021
bash 的独特功能应该是使用它们的原因。如果你正在编写 POSIX shell,好吧,使用任何东西。我编写 bash 脚本,并尝试使用该语言的所有合理功能。是的,它们不是便携式的,但没关系,Bash 并不难掌握。特别是使用 [[ ]] 在许多方面更安全、更万无一失。
15赞 siride 3/28/2022
有趣的是,与相当明智的行为相比,阅读所有奇怪的行为,并建议仅基于可移植性(现在很少关注)使用,并且人们必须学习bash版本的“复杂性”。这更像是你必须学习旧版本的荒谬复杂性。bash 版本可以满足您对大多数其他编程语言的期望。[[[[
2赞 William Pursell 8/27/2022
这是一个很好的答案!我会补充我认为的主要失败之处。也就是说,当您错误地进行整数比较时,您不会收到错误消息。例如:vs .当 时,这保证了您得到的 一个很大的尖叫错误消息。[[[[ $n -gt 0 ]][ "$n" -gt 0 ]n=foo[
23赞 crizCraig 6/26/2019 #8

如果您喜欢遵循 Google 的风格指南

test、 和[ … ][[ … ]]

[[ … ]]优于 和 。[ … ]test/usr/bin/[

[[ … ]]减少错误,因为 和 之间不会发生路径名扩展或单词拆分。此外,允许正则表达式匹配,而不允许。[[]][[ … ]][ … ]

# This ensures the string on the left is made up of characters in
# the alnum character class followed by the string name.
# Note that the RHS should not be quoted here.
if [[ "filename" =~ ^[[:alnum:]]+name ]]; then
  echo "Match"
fi

# This matches the exact pattern "f*" (Does not match in this case)
if [[ "filename" == "f*" ]]; then
  echo "Match"
fi
# This gives a "too many arguments" error as f* is expanded to the
# contents of the current directory
if [ "filename" == f* ]; then
  echo "Match"
fi

有关血腥的细节,请参阅 E14 http://tiswww.case.edu/php/chet/bash/FAQ

评论

3赞 JamesThomasMoon 9/14/2019
谷歌写道:“没有路径名扩展......发生“,但返回 true(这意味着已扩展为 )。我认为谷歌的写作应该更精确。[[ -d ~ ]]~/home/user
2赞 maoizm 9/14/2020
@JamesThomasMoon1979这是波浪号扩展,而不是谷歌文本中提到的路径名扩展
0赞 PePa 10/6/2021
无需在 [[ ]] 中将“filename”括起来。
21赞 Mark Reed 7/14/2020 #9

在一个标题中明确包含“in Bash”的标记为“bash”的问题中,我对所有回复都说你应该避免......因为它只在 bash 中工作![[]]

的确,可移植性是主要的反对意见:如果你想编写一个在 Bourne 兼容的 shell 中工作的 shell 脚本,即使它们不是 bash,你应该避免......(如果你想在一个更严格的 POSIX shell 中测试你的 shell 脚本,我建议;尽管它是一个不完整的 POSIX 实现,因为它缺乏标准所需的国际化支持,但它也缺乏对 bash、ksh、zsh 等中发现的许多非 POSIX 结构的大多数(但不是全部)的支持。[[]]dash

我看到的另一个反对意见至少在 bash 的假设中适用:......有自己特殊的规则,你必须学习,而......就像另一个命令一样。这又是正确的(桑蒂利先生带来了显示所有差异的收据),但差异是好是坏是相当主观的。我个人发现双括号结构让我使用......用于分组、布尔逻辑、比较和未加引号的参数扩展。它就像它自己的封闭小世界,表达式的工作方式更像传统的非命令 shell 编程语言。[[]][]()&&||<>

我没有看到提出的一点是,这种行为......与 POSIX 指定的算术扩展构造 ...完全一致,并且还允许不带引号的括号以及布尔和不等式运算符(此处执行数字而不是词法比较)。从本质上讲,每当您看到双括号字符时,您都会获得相同的引号屏蔽效果。[[]]$(())

(Bash 和它的现代亲戚也使用 ...(不带前导)作为 C 样式的循环头或执行算术运算的环境;这两种语法都不是 POSIX 的一部分。(())$for

因此,有一些很好的理由更喜欢......;还有一些原因可以避免它,这些原因可能适用于您的环境,也可能不适用于您的环境。至于你的同事,“我们的风格指南是这么说的”是一个有效的理由,但就目前而言,我也会从理解为什么风格指南推荐它所做的事情的人那里寻找背景故事。[[]]

2赞 William Pursell 8/27/2022 #10

使用时有一个重要的注意事项,请考虑:[[ ]]

$ # For integer like strings, [ and [[ behave the same
$ 
$ n=5 # set n to a string that represents an integer
$ [[ $n -ge 0 ]] && printf "\t> $n is non-negative\n"
        > 5 is non-negative
$ [ "$n" -ge 0 ] && printf "\t> $n is non-negative\n"
        > 5 is non-negative
$ 
$ # But the behavior is different in several cases:
$ n=something # set n to some non-numeric string
$ [ "$n" -ge 0 ] && printf "\t> $n is non-negative\n"
-bash: [: something: integer expression expected
$ [[ $n -ge 0 ]] && printf "\t> $n is non-negative\n"
        > something is non-negative
$ n=5foo
$ [[ $n -ge 0 ]] && printf "\t> $n is non-negative\n"
-bash: 5foo: value too great for base (error token is "5foo")

为了澄清 的行为不一致,请考虑:[[

$ for n in 5foo 5.2 something; do [[ $n -ge 0 ]] && echo ok; done
-bash: 5foo: value too great for base (error token is "5foo")
-bash: 5.2: syntax error: invalid arithmetic operator (error token is ".2")
ok
$ for n in 5foo 5.2 something; do [ "$n" -ge 0 ] && echo ok; done
-bash: [: 5foo: integer expression expected
-bash: [: 5.2: integer expression expected
-bash: [: something: integer expression expected

将一些非数字字符串解释为 0 并且当不是有效整数时,行中的错误消息不一致(或完全没有错误消息!)会使得用于整数比较不安全。出于这个原因,我强烈建议不要进行数值比较。[[ $n -ge 0 ]]$n[[[[

评论

0赞 arkan 4/7/2023
[[ $n -gt 0 ]]并且是严格等效的,不应返回错误消息。它们都返回一个退出代码(如果条件为 true,则为 0,如果条件为 false,则为 1) 我不确定您要提出的问题是什么。[ $n -gt 0 ]
0赞 William Pursell 4/7/2023
@arkan如果扩展到字符串,则应生成类似 的错误消息。这是一个非常有用的错误消息,但不提供。$nfoo[ $n -gt 0 ]invalid integer ‘foo’[[
0赞 William Pursell 4/7/2023
@arkan但也许更重要的是,其行为与 .前者返回 0,而后者返回非零。IMO,将非整数(如字符串)解释为 0 是一个致命的缺陷。[[ foo -lt 5 ]][ foo -lt 5 ]
0赞 arkan 4/8/2023
谢谢你的澄清,我提交了一个编辑,应该避免像我这样的人错过你的观点。恕我直言,“致命缺陷”或“无法使用”有点过头了(尤其是谈论 bash 一堆遗留的黑客)。 做没有的事情,所以尽管有这个缺陷,人们可能还是想使用它。此外,它还会返回错误代码“1”。[[ ]][ ]
0赞 William Pursell 4/8/2023
@arkan 感谢您的编辑。我做了一些更改,这可能有助于进一步澄清。在原始示例中,您的注释“它确实返回错误代码'1'”是正确的,但表明您错过了一个重要的细节,因此我已将示例更改为其中并表现不同的示例。[[[
3赞 Gabriel Staples 10/14/2023 #11

一位同事最近在代码审查中声称,该结构优先于
......
他无法提供理由。有吗?
[[ ]][ ]

是的,速度和效率。

我在这里没有看到任何地方提到“速度”这个词,但由于是 Bash 内置语法,它不需要生成新进程。 另一方面,是命令,运行它会生成一个新进程。因此,应该比语法更快,因为它避免了每次遇到 a 时都会生成一个新进程。[[ ]][test[[ ]][ ]test[

是什么让我认为生成了一个新进程,而不是内置的 Bash?
好吧,当我运行它时,它告诉我它位于 .请注意,这只是命令的最后一个参数。
[which [/usr/bin/[][

此外,我认为功能更丰富。[[ ]]

我传统上喜欢 over,因为它更便携,但最近我想我可能会切换到 over,因为它更快,而且我写了很多 Bash。[ ][[ ]][[ ]][ ]

速度测试结果(越低越好)

以下是我在 200 万次迭代中的结果: 比 快 1.42 倍[[ ]][ ]

enter image description here

正在测试的代码非常简单:

if [ "$word1" = "$word2" ]; then
    echo "true"
fi

与。

if [[ "$word1" == "$word2" ]]; then
    echo "true"
fi

其中 和 只是以下常量,所以从不运行word1word2echo "true"

word1="true"
word2="false"

如果要自己运行测试,代码如下。我已将一个相当复杂的 Python 程序作为 heredoc 字符串嵌入到 Bash 脚本中以进行绘图。

如果将比较字符串常量更改为以下内容:

word1="truetruetruetruetruetruetruetruetruetruetruetruetruetruetruetruetru"
word2="truetruetruetruetruetruetruetruetruetruetruetruetruetruetruetruetrufalse"

...然后你会得到这些结果,其中 Bash 内置 () 仅比命令 () 快 1.22 倍[[ ]]test[ ]

enter image description here

在第一种情况下,字符串仅在 1 个字符后就不同,因此比较可以立即结束。在后一种情况下,字符串在 67 个字符后不同,因此比较需要更长的时间才能确定字符串不同。我怀疑字符串越长,速度差异就越小,因为大部分时间差最初是生成新进程所需的时间,但随着字符串匹配时间的延长,进程生成时间就不那么重要了。无论如何,这是我的怀疑。[

speed_tests__comparison_with_test_cmd_vs_double_square_bracket_bash_builtin.sh从我的eRCaGuy_hello_world存储库:

#!/usr/bin/env bash

# This file is part of eRCaGuy_hello_world: https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world

# ==============================================================================
# Python plotting program
# - is a Bash heredoc
# References:
# 1. My `plot_data()` function here:
#    https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/python/pandas_dataframe_iteration_vs_vectorization_vs_list_comprehension_speed_tests.py
# 1. See my answer here: https://stackoverflow.com/a/77270285/4561887
# ==============================================================================
python_plotting_program=$(cat <<'PROGRAM_END'

# 3rd-party imports
import matplotlib.pyplot as plt
import pandas as pd

# standard imports
import os
import sys

assert sys.argv[0] == "-c"
# print(f"sys.argv = {sys.argv}")  # debugging

# Get the command-line arguments
FULL_PATH_TO_SCRIPT = sys.argv[1]
NUM_ITERATIONS = int(sys.argv[2])
single_bracket_sec = float(sys.argv[3])
double_bracket_sec = float(sys.argv[4])

# Obtain paths to help save the plot later.
# See my answer: https://stackoverflow.com/a/74800814/4561887
SCRIPT_DIRECTORY = str(os.path.dirname(FULL_PATH_TO_SCRIPT))
FILENAME = str(os.path.basename(FULL_PATH_TO_SCRIPT))
FILENAME_NO_EXTENSION = os.path.splitext(FILENAME)[0]

# place into lists
labels = ['`[ ]` `test` func', '`[[ ]]` Bash built-in']
data = [single_bracket_sec, double_bracket_sec]

# place into a Pandas dataframe for easy manipulation and plotting
df = pd.DataFrame({'test_type': labels, 'time_sec': data})
df = df.sort_values(by="time_sec", axis='rows', ascending=False)
df = df.reset_index(drop=True)

# plot the data
fig = plt.figure()
plt.bar(labels, data)
plt.title(f"Speed Test: `[ ]` vs `[[ ]]` over {NUM_ITERATIONS:,} iterations")
plt.xlabel('Test Type', labelpad=8)  # use `labelpad` to lower the label
plt.ylabel('Time (sec)')

# Prepare to add text labels to each bar
df["text_x"] = df.index # use the indices as the x-positions
df["text_y"] = df["time_sec"] + 0.06*df["time_sec"].max()
df["time_multiplier"] = df["time_sec"] / df["time_sec"].min()
df["text_label"] = (df["time_sec"].map("{:.4f} sec\n".format) +
                    df["time_multiplier"].map("{:.2f}x".format))

# Use a list comprehension to actually call `plt.text()` to **automatically add
# a plot label** for each row in the dataframe
[
    plt.text(
        text_x,
        text_y,
        text_label,
        horizontalalignment='center',
        verticalalignment='center'
    ) for text_x, text_y, text_label
    in zip(
        df["text_x"],
        df["text_y"],
        df["text_label"]
    )
]

# add 10% to the top of the y-axis to leave space for labels
ymin, ymax = plt.ylim()
plt.ylim(ymin, ymax*1.1)

plt.savefig(f"{SCRIPT_DIRECTORY}/{FILENAME_NO_EXTENSION}.svg")
plt.savefig(f"{SCRIPT_DIRECTORY}/{FILENAME_NO_EXTENSION}.png")

plt.show()

PROGRAM_END
)

# ==============================================================================
# Bash speed test program
# ==============================================================================

# See my answer: https://stackoverflow.com/a/60157372/4561887
FULL_PATH_TO_SCRIPT="$(realpath "${BASH_SOURCE[-1]}")"

NUM_ITERATIONS="2000000" # 2 million
# NUM_ITERATIONS="1000000" # 1 million
# NUM_ITERATIONS="10000" # 10k

word1="true"
word2="false"

# Get an absolute timestamp in floating point seconds.
# From:
# https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/bash/timestamp_lib_WIP.sh
seconds_float() {
    time_sec="$(date +"%s.%N")"
    echo "$time_sec"
}

single_bracket() {
    for i in $(seq 1 "$NUM_ITERATIONS"); do
        if [ "$word1" = "$word2" ]; then
            echo "true"
        fi
    done
}

double_bracket() {
    for i in $(seq 1 "$NUM_ITERATIONS"); do
        if [[ "$word1" == "$word2" ]]; then
            echo "true"
        fi
    done
}

run_and_time_function() {
    # the 1st arg is the function to run
    func_to_time="$1"

    # NB: the "information" type prints will go to stderr so they don't
    # interfere with the actual timing results printed to stdout.

    echo -e "== $func_to_time time test start... ==" >&2  # to stderr
    time_start="$(seconds_float)"

    $func_to_time

    time_end="$(seconds_float)"
    elapsed_time="$(bc <<< "scale=20; $time_end - $time_start")"
    echo "== $func_to_time time test end. ==" >&2  # to stderr
    echo "$elapsed_time"  # to stdout
}

main() {
    echo "Running speed tests over $NUM_ITERATIONS iterations."

    single_bracket_time_sec="$(run_and_time_function "single_bracket")"
    double_bracket_time_sec="$(run_and_time_function "double_bracket")"

    echo "single_bracket_time_sec = $single_bracket_time_sec"
    echo "double_bracket_time_sec = $double_bracket_time_sec"

    # echo "Plotting the results in Python..."
    python3 -c "$python_plotting_program" \
        "$FULL_PATH_TO_SCRIPT" \
        "$NUM_ITERATIONS" \
        "$single_bracket_time_sec" \
        "$double_bracket_time_sec"
}

# Determine if the script is being sourced or executed (run).
# See:
# 1. "eRCaGuy_hello_world/bash/if__name__==__main___check_if_sourced_or_executed_best.sh"
# 1. My answer: https://stackoverflow.com/a/70662116/4561887
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
    # This script is being run.
    __name__="__main__"
else
    # This script is being sourced.
    __name__="__source__"
fi

# Only run `main` if this script is being **run**, NOT sourced (imported).
# - See my answer: https://stackoverflow.com/a/70662116/4561887
if [ "$__name__" = "__main__" ]; then
    main "$@"
fi

样品运行和输出:

eRCaGuy_hello_world$ bash/speed_tests__comparison_with_test_cmd_vs_double_square_bracket_bash_builtin.sh
Running speed tests over 2000000 iterations.
== single_bracket time test start... ==
== single_bracket time test end. ==
== double_bracket time test start... ==
== double_bracket time test end. ==
single_bracket_time_sec = 5.990248014
double_bracket_time_sec = 4.230342635

引用

  1. 我在这里的功能,用于如何制作带有条形上方复杂文本的条形图:https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/python/pandas_dataframe_iteration_vs_vectorization_vs_list_comprehension_speed_tests.pyplot_data()
    1. 我用这段代码回答: 如何在 Pandas 中遍历 DataFrame 中的行
  2. 我的答案:如何获取当前正在执行的python文件的路径和名称?
  3. 我的回答:如何获取正在运行或获取的任何脚本的完整文件路径、完整目录和基本文件名......
  4. 我的 Bash 库:获取以浮点秒为单位的绝对时间戳:https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/bash/timestamp_lib_WIP.sh
  5. 如何制作 heredoc:https://linuxize.com/post/bash-heredoc/

另请参阅

  1. 这个答案链接到另一个超级简单的速度测试