双方括号[[]]比Bash中的单方括号[]更好吗?

Leo*_*ard 514 syntax bash if-statement

最近在一次代码审查中,一位同事声称该[[ ]]构造比[ ]在类似的构造中更受欢迎

if [ "`id -nu`" = "$someuser" ] ; then 
     echo "I love you madly, $someuser"
fi
Run Code Online (Sandbox Code Playgroud)

他无法提供理由.有吗?

Joh*_*itb 560

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

[[ -e $b ]]
Run Code Online (Sandbox Code Playgroud)

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

[[还有一些其他很好的功能,比如正则表达式匹配=~以及类似C语言的运算符.这是一个很好的页面:测试[[[?之间的区别是什么?Bash测试

  • @dubiousjim:如果你使用bash-only结构(比如这个),你应该有#!/ bin/bash,而不是#!/ bin/sh. (54认同)
  • 考虑到bash现在无处不在,我倾向于认为它非常便携.对我来说唯一常见的例外是在busybox平台上 - 但考虑到它运行的硬件,你可能想要为它做出特别的努力. (19认同)
  • 这就是为什么我让我的脚本使用`#!/ bin/sh`然后在我依赖某些BASH特定功能时将它们切换为使用`#!/ bin/bash`,以表示它不再是Bourne shell可移植的. (15认同)
  • @guns:的确如此.我建议你的第二句反驳你的第一句; 如果你把软件开发视为一个整体,"bash无处不在"和"例外是busybox平台"是完全不兼容的.busybox广泛用于嵌入式开发. (14认同)
  • @guns:即使我的盒子上安装了bash,它也可能没有执行脚本(我可能将/ bin/sh符号链接到某些dash-variant,这是FreeBSD和Ubuntu的标准配置).Busybox还有其他的奇怪之处:现在使用标准编译选项,它解析`[[]]`但是把它解释为与`[]`的含义相同. (11认同)
  • 请注意,如果在FreeBSD上安装bash,它将不是`/ bin/bash`.它将是`/ usr/local/bin/bash`.调用像bash这样的非标准解释器的正确方法是`#!/ usr/bin/env bash`. (4认同)
  • bash 在端口中可用,但在 FreeBSD 上默认不安装。我上次检查的solaris上也有类似情况,但那是很久以前的事了。可能也是 AIX。诚然,安装基数比 Linux 发行版小得多,但它们是真实存在的。 (2认同)
  • Bash 无处不在,但很多情况(比如那些该死的 cron)并不默认使用它。我只是意识到这一点很重要。如果我是的话,我就不会尝试在我的 cron 中使用 [[ ]] :) (2认同)
  • “但是使用 [,你必须引用 $b” 对我来说,如果你展示如何引用它真的会很有帮助。如果 $b 评估为 `a*`,你会像这样引用它:`"$b"`吗? (2认同)

Cir*_*四事件 113

行为差异

关于Bash 4.3.11的一些区别:

  • POSIX vs Bash扩展:

  • 常规命令vs魔法

    • [ 只是一个带有奇怪名称的常规命令.

      ]只是一个参数,[可以防止使用更多的参数.

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

      Bash解析命令的方式没有任何改变.

      具体地讲,<是重定向,&&以及||连接多个命令,( )生成子shell除非逃脱\,和字膨胀发生如常.

    • [[ X ]]是一个X神奇地解析的单一构造.<,&&,||()经特殊处理,以及分词规则是不同的.

      还有其他差异,如==~.

      在Bashese:[是一个内置命令,[[是一个关键字:https://askubuntu.com/questions/445749/whats-the-difference-between-shell-builtin-and-shell-keyword

  • <

  • &&||

    • [[ a = a && b = b ]]:真实,逻辑
    • [ a = a && b = b ]:语法错误,&&解析为AND命令分隔符cmd1 && cmd2
    • [ a = a -a b = b ]:等效,但由POSIX³弃用
    • [ a = a ] && [ b = b ]:POSIX和可靠的等价物
  • (

    • [[ (a = a || a = b) && a = b ]]:假的
    • [ ( a = a ) ]:语法错误,()被解释为子shell
    • [ \( a = a -o a = b \) -a a = b ]:等价,但()POSIX弃用
    • { [ a = a ] || [ a = b ]; } && [ a = b ]POSIX等价5
  • 扩展时的单词拆分和文件名生成(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? ]:a?glob扩展.因此可能是真或假,具体取决于当前目录中的文件.
    • [ ab = a\? ]:false,不是glob扩展
    • ===在双方的同[[[,不过==是一个bash扩展.
    • case ab in (a?) echo match; esac:POSIX等价物
    • [[ ab =~ 'ab?' ]]:假4,失去魔力''
    • [[ ab? =~ 'ab?' ]]:是的
  • =~

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

建议:始终使用[].

对于[[ ]]我见过的每个构造,都有POSIX等价物.

如果您使用[[ ]]:

  • 失去便携性
  • 强迫读者学习另一个bash扩展的复杂性.[它只是一个带有奇怪名称的常规命令,不涉及特殊的语义.

¹灵感来自[[...]]Korn shell中的等效构造

²但是对于某些值ab(例如+index)失败,并进行数字比较,如果ab看起来像十进制整数.expr "x$a" '<' "x$b"适用于两者.

³并且也失败了某些ab类似的值!(.

4在bash 3.2及以上版本中提供与bash 3.1的兼容性未启用(如同BASH_COMPAT=3.1)

5尽管分组(这里与{...;}命令组,而不是(...)这将运行一个不必要的子shell)不是必要的,因为||&&壳运营商(而不是在||&& [[...]]运营商或-o/ -a [经营者)具有相同的优先级.所以[ a = a ] || [ a = b ] && [ a = b ]相同.

  • 有趣的是,与“[[”的相当明智的行为相比,阅读“[”的所有奇怪行为,并得出仅基于可移植性(现在很少有人关心)而使用“[”的建议,并且人们必须学习 bash 版本的“复杂性”。这更像是你必须学习旧版本的荒谬的复杂性。bash 版本可以满足您对大多数其他编程语言的期望。 (17认同)
  • 好的解释.我使用`[`也是出于同样的原因. (4认同)
  • 用孟加拉语:)哈。很好地阐明了使用POSIX的区别和理由。 (2认同)
  • “在 POSIX 中有效的所有内容在 BASH 中都有效,但反之则不然。”这是否成立。? (2认同)
  • @Wlad Bash 极大地扩展了 POSIX,因此任何 Bash 扩展都不是 POSIX。另一方面,我不是 100%,但感觉有可能(除了 Bash 扩展覆盖 POSIX 语法时,例如,在 posix 中 `[[` 也可能是一个常规命令)。相关:https://askubuntu.com/questions/766270/what-is-posix-known-mode-in-linux (2认同)
  • 这是一个很好的答案!我想补充一下我认为“[[”的主要失败之处。也就是说,当您错误地进行整数比较时,您不会收到错误消息。例如 `[[ $n -gt 0 ]]` 与 `[ "$n" -gt 0 ]`。当“n=foo”时,会出现一条巨大的错误消息,您可以使用“[”得到该错误消息。 (2认同)

小智 55

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

顺便提一下,正如指南中所述,[[ ]]是在ksh88(1988年版的Korn shell)中引入的.

  • @Thomas,Wooledge BashFAQ和相关维基是我使用的; 他们是由Freenode #bash频道的居民积极维护的(虽然有时多刺,他们倾向于非常关心正确性).BashFAQ#31,http://mywiki.wooledge.org/BashFAQ/031,是与此问题直接相关的页面. (9认同)
  • @Thomas,ABS在很多圈子里被认为是一个非常糟糕的参考; 虽然它有大量准确的信息,但它往往需要很少注意避免在其示例中展示不良做法,并且花费了大量的生命而没有维护. (6认同)
  • 这远不是一种基础,它最初是在Korn shell中引入的. (5认同)
  • @CharlesDuffy 感谢您的评论,您能说出一个好的替代方案吗?我不是 shell 脚本方面的专家,我正在寻找一个可以参考的指南,用于大约每半年编写一次脚本。 (2认同)

f3l*_*lix 11

哪个比较,测试,支架或双支架,是最快的?(http://bashcurescancer.com)

双括号是一个"复合命令",其中test和单个括号是shell内置命令(实际上是相同的命令).因此,单支架和双支架执行不同的代码.

测试和单个括号是最便携的,因为它们作为单独和外部命令存在.但是,如果您使用任何远程现代版本的BASH,则支持双支架.

  • 在shell脚本中对*fast*的迷恋是什么?我想要它*最便携*并且不关心改进`[[`可能带来.但是,那时,我是一个古老的学校老屁:-) (7认同)
  • @Jens一般我同意:脚本的整个目的是(是?)可移植性(否则,我们编码和编译,而不是脚本)...我能想到的两个例外是:(1)制表符完成(其中)使用大量条件逻辑可以实现完成脚本很长时间; (2)超级提示(`PS1 = ......疯狂的东西......和/或`$ PROMPT_COMMAND`); 对于这些,我不希望在执行脚本时出现任何_peceptible delay_. (4认同)
  • 有些人对最快的痴迷只是出于风格。在其他条件相同的情况下,为什么不将更高效的代码构造合并到您的默认样式中,特别是如果该构造还提供了更高的可读性呢?就可移植性而言,bash 适合的许多任务本质上是不可移植的。例如,如果自上次运行以来已经超过 X 小时,我需要运行“apt-get update”。当人们可以将可移植性从已经太长的代码约束列表中剔除时,这是一种极大的解脱。 (2认同)

Mar*_*eed 9

在标题中明确包含“in Bash”的标记为“bash”的问题中,我对所有说您应该避免的答复感到有些惊讶[[……]]因为它仅适用于 bash!

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

我看到的另一个反对意见至少适用于 bash 的假设:[[...]]有自己的特殊规则,您必须学习,而[...]就像另一个命令一样。这又是真的(桑蒂利先生带来了显示所有差异的收据),但差异是好是坏是相当主观的。我个人觉得它释放的双支架结构让我用(...)进行分组,&&||为布尔逻辑,<>进行比较,并且不带引号的参数扩展。这就像它自己的封闭小世界,在那里表达式更像是在传统的非命令外壳编程语言中的工作方式。

我还没有看到提到的一点是,这种行为[[......]]是的算术扩展结构的完全一致$((...... )),这由POSIX规定,也允许不带引号的括号和布尔和不平等运营商(其中这里执行数字而不是词法比较)。本质上,任何时候您看到双括号字符时,您都会获得相同的引号屏蔽效果。

(Bash 及其现代亲属也使用((...——))没有前导$——作为 C 风格的for循环头或执行算术运算的环境;这两种语法都不是 POSIX 的一部分。)

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


cri*_*aig 8

如果您想遵循Google 的风格指南

测试,[[[

[[ ... ]]减少错误,因为在[[和之间没有发生路径名扩展或分词]],并[[ ... ]]允许正则表达式匹配[ ... ]没有的地方。

# 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.
# For the gory details, see
# E14 at https://tiswww.case.edu/php/chet/bash/FAQ
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
Run Code Online (Sandbox Code Playgroud)

  • Google 写道“_没有路径名扩展...发生_”,但 `[[ -d ~ ]]` 返回 true (这意味着 `~` 已扩展为 `/home/user`)。我认为谷歌的写作应该更准确。 (2认同)
  • @JamesThomasMoon1979 这是波形符扩展,而不是谷歌文本中提到的路径名 expansio (2认同)

Vic*_*lea 6

无法使用的典型情况[[是在 autotools configure.ac 脚本中。括号具有特殊且不同的含义,因此您必须使用ortest来代替-- 请注意, test 和是同一个程序。[[[[

  • 鉴于 autotools 不是 POSIX shell,为什么您会期望将 `[` 定义为 POSIX shell 函数? (3认同)

Gab*_*les 5

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

是的,速度和效率。

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

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

另外,我认为更喜欢它的第二个原因[[ ]]是它的功能更丰富。它支持更多“C 风格”和现代语法。

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

速度测试结果(越低越好

这是我超过 200 万次迭代的结果:[[ ]]比快1.42 倍[ ]

在此输入图像描述

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

if [ "$word1" = "$word2" ]; then
    echo "true"
fi
Run Code Online (Sandbox Code Playgroud)

if [[ "$word1" == "$word2" ]]; then
    echo "true"
fi
Run Code Online (Sandbox Code Playgroud)

其中word1word2只是以下常量,因此echo "true" 从未运行

word1="true"
word2="false"
Run Code Online (Sandbox Code Playgroud)

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

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

word1="truetruetruetruetruetruetruetruetruetruetruetruetruetruetruetruetru"
word2="truetruetruetruetruetruetruetruetruetruetruetruetruetruetruetruetrufalse"
Run Code Online (Sandbox Code Playgroud)

...然后您将得到以下结果,其中 Bash 内置 ( [[ ]]) 仅比命令 ( ) 快1.22 倍test[ ]

在此输入图像描述

在第一种情况下,字符串仅在 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
Run Code Online (Sandbox Code Playgroud)

示例运行和输出:

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
Run Code Online (Sandbox Code Playgroud)

参考

  1. 我的plot_data()函数在这里,了解如何使用条形图上方的复杂文本制作条形图:https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/python/pandas_dataframe_iteration_vs_vectorization_vs_list_compressive_speed_tests.py
    1. 我用这段代码回答:How to iterate over rows in a DataFrame in Pandas
  2. 我的回答:如何获取当前正在执行的python文件的路径和名称?
  3. 我的答案:如何获取正在运行或来源的任何脚本的完整文件路径、完整目录和基本文件名...
  4. 我的 Bash 库:获取以浮点秒为单位的绝对时间戳:https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/bash/timestamp_lib_WIP.sh
  5. 如何制作定界文档:https://linuxize.com/post/bash-heredoc/

也可以看看

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