提问人:Leonard 提问时间:3/21/2009 最后编辑:Peter MortensenLeonard 更新时间:10/14/2023 访问量:388339
在 Bash 中,双方括号 [[ ]] 是否比单方括号 [ ] 更可取?
Are double square brackets [[ ]] preferable over single square brackets [ ] in Bash?
问:
一位同事最近在代码审查中声称,该结构在诸如[[ ]]
[ ]
if [ "`id -nu`" = "$someuser" ] ; then
echo "I love you madly, $someuser"
fi
他无法提供理由。有吗?
答:
[[ ]]
具有更多功能 - 我建议您查看高级 Bash 脚本指南以获取更多信息,特别是第 7 章中的扩展测试命令部分。测试。
顺便说一句,正如指南所指出的,它是在 ksh88(1988 年版的 KornShell)中引入的。[[ ]]
评论
双括号是“复合 命令“,其中作为测试和单 括号是 shell 内置的(和 实际是相同的命令)。因此 单支架和双支架 执行不同的代码。
测试和单括号是 最便携的,因为它们以 单独的外部命令。 但是,如果您远程使用任何 现代版的BASH,双 支持括号。
评论
[[
[
test
PS1=...crazy stuff...
$PROMPT_COMMAND
apt-get update
[
和 [[
之间所做的一些速度测试。我知道你说你不在乎,但这很有趣,有时很重要。
[[
意外较少,使用起来通常更安全。但它不是可移植的 - POSIX 没有指定它的作用,只有一些 shell 支持它(除了 bash,我听说 ksh 也支持它)。例如,您可以做
[[ -e $b ]]
以测试文件是否存在。但是有了 ,你必须引用 ,因为它分裂了参数并扩展了诸如(从字面上理解它的地方)之类的东西。这也与如何成为一个外部程序并像其他程序一样正常地接收其参数有关(尽管它也可以是内置的,但它仍然没有这种特殊的处理)。[
$b
"a*"
[[
[
[[
还有一些其他不错的功能,比如正则表达式与运算符匹配,就像它们在类 C 语言中一样。这里有一个很好的页面:test、[
和 [[ 和 Bash
测试有什么区别=~
评论
[[ ]]
[ ]
#!/bin/sh
#!/bin/bash
简而言之,[[ 更好,因为它不会分叉另一个进程。没有括号或单括号比双括号慢,因为它会分叉另一个进程。
评论
type [
[[
[
type [[
[
[[
[
/bin/sh
[[ ]] 在某些版本的 SunOS 下不支持双括号,并且完全不支持以下函数内部声明:
GNU Bash,版本 2.02.0(1)-release (sparc-sun-solaris2.6)
评论
无法使用的典型情况是在 autotools configure.ac 脚本中。括号有特殊和不同的含义,所以你将不得不使用代替或 -- 注意,test 和 是同一个程序。[[
test
[
[[
[
评论
[
行为差异
Bash 4.3.11 上的一些差异:
POSIX vs Bash 扩展:
常规命令与魔法
[
只是一个名字很奇怪的常规命令。]
只是 的最后一个参数。[
Ubuntu 16.04 实际上有一个由 coreutils 提供的可执行文件,但 Bash 内置版本优先。
/usr/bin/[
Bash 解析命令的方式不会发生任何变化。
特别是,是重定向,并连接多个命令,生成子 shell,除非转义为 ,并且单词扩展照常发生。
<
&&
||
( )
\
[[ X ]]
是一个单一的结构,可以神奇地解析。、 和 被特殊对待,分词规则不同。X
<
&&
||
()
还有进一步的区别,如 和 。
=
=~
在 Bashese 中:是一个内置命令,是一个关键字:shell builtin 和 shell 关键字有什么区别?
[
[[
<
[[ a < b ]]
:词典比较[ a \< b ]
:同上。 必需,否则会像任何其他命令一样进行重定向。Bash 扩展。\
expr x"$x" \< x"$y" > /dev/null
或:POSIX 等价物,请参阅:如何在 Bash 中测试字典小于或等于的字符串?[ "$(expr x"$x" \< x"$y")" = 1 ]
&&
和||
[[ 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 将被解释为逻辑操作a
b
!
(
(
[[ (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 的重要更正和补充。
评论
[[
[
[[
[
[[
[[ $n -gt 0 ]]
[ "$n" -gt 0 ]
n=foo
[
如果您喜欢遵循 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
评论
[[ -d ~ ]]
~
/home/user
在一个标题中明确包含“in Bash”的标记为“bash”的问题中,我对所有回复都说你应该避免......因为它只在 bash 中工作![[
]]
的确,可移植性是主要的反对意见:如果你想编写一个在 Bourne 兼容的 shell 中工作的 shell 脚本,即使它们不是 bash,你应该避免......(如果你想在一个更严格的 POSIX shell 中测试你的 shell 脚本,我建议;尽管它是一个不完整的 POSIX 实现,因为它缺乏标准所需的国际化支持,但它也缺乏对 bash、ksh、zsh 等中发现的许多非 POSIX 结构的大多数(但不是全部)的支持。[[
]]
dash
我看到的另一个反对意见至少在 bash 的假设中适用:......有自己特殊的规则,你必须学习,而......就像另一个命令一样。这又是正确的(桑蒂利先生带来了显示所有差异的收据),但差异是好是坏是相当主观的。我个人发现双括号结构让我使用......用于分组、布尔逻辑、比较和未加引号的参数扩展。它就像它自己的封闭小世界,表达式的工作方式更像传统的非命令 shell 编程语言。[[
]]
[
]
(
)
&&
||
<
>
我没有看到提出的一点是,这种行为......与 POSIX 指定的算术扩展构造 ...完全一致,并且还允许不带引号的括号以及布尔和不等式运算符(此处执行数字而不是词法比较)。从本质上讲,每当您看到双括号字符时,您都会获得相同的引号屏蔽效果。[[
]]
$((
))
(Bash 和它的现代亲戚也使用 ...(不带前导)作为 C 样式的循环头或执行算术运算的环境;这两种语法都不是 POSIX 的一部分。((
))
$
for
因此,有一些很好的理由更喜欢......;还有一些原因可以避免它,这些原因可能适用于您的环境,也可能不适用于您的环境。至于你的同事,“我们的风格指南是这么说的”是一个有效的理由,但就目前而言,我也会从理解为什么风格指南推荐它所做的事情的人那里寻找背景故事。[[
]]
使用时有一个重要的注意事项,请考虑:[[ ]]
$ # 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
[[
[[
评论
[[ $n -gt 0 ]]
并且是严格等效的,不应返回错误消息。它们都返回一个退出代码(如果条件为 true,则为 0,如果条件为 false,则为 1) 我不确定您要提出的问题是什么。[ $n -gt 0 ]
$n
foo
[ $n -gt 0 ]
invalid integer ‘foo’
[[
[[ foo -lt 5 ]]
[ foo -lt 5 ]
[[ ]]
[ ]
[
[[
一位同事最近在代码审查中声称,该结构优先于
......
他无法提供理由。有吗?[[ ]]
[ ]
是的,速度和效率。
我在这里没有看到任何地方提到“速度”这个词,但由于是 Bash 内置语法,它不需要生成新进程。 另一方面,是命令,运行它会生成一个新进程。因此,应该比语法更快,因为它避免了每次遇到 a 时都会生成一个新进程。[[ ]]
[
test
[[ ]]
[ ]
test
[
是什么让我认为生成了一个新进程,而不是内置的 Bash?
好吧,当我运行它时,它告诉我它位于 .请注意,这只是命令的最后一个参数。[
which [
/usr/bin/[
]
[
此外,我认为功能更丰富。[[ ]]
我传统上喜欢 over,因为它更便携,但最近我想我可能会切换到 over,因为它更快,而且我写了很多 Bash。[ ]
[[ ]]
[[ ]]
[ ]
速度测试结果(越低越好)
以下是我在 200 万次迭代中的结果: 比 快 1.42 倍:[[ ]]
[ ]
正在测试的代码非常简单:
if [ "$word1" = "$word2" ]; then
echo "true"
fi
与。
if [[ "$word1" == "$word2" ]]; then
echo "true"
fi
其中 和 只是以下常量,所以从不运行:word1
word2
echo "true"
word1="true"
word2="false"
如果要自己运行测试,代码如下。我已将一个相当复杂的 Python 程序作为 heredoc 字符串嵌入到 Bash 脚本中以进行绘图。
如果将比较字符串常量更改为以下内容:
word1="truetruetruetruetruetruetruetruetruetruetruetruetruetruetruetruetru"
word2="truetruetruetruetruetruetruetruetruetruetruetruetruetruetruetruetrufalse"
...然后你会得到这些结果,其中 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
样品运行和输出:
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
引用
- 我在这里的功能,用于如何制作带有条形上方复杂文本的条形图:https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/python/pandas_dataframe_iteration_vs_vectorization_vs_list_comprehension_speed_tests.py
plot_data()
- 我用这段代码回答: 如何在 Pandas 中遍历 DataFrame 中的行
- 我的答案:如何获取当前正在执行的python文件的路径和名称?
- 我的回答:如何获取正在运行或获取的任何脚本的完整文件路径、完整目录和基本文件名......
- 我的 Bash 库:获取以浮点秒为单位的绝对时间戳:https://github.com/ElectricRCAircraftGuy/eRCaGuy_hello_world/blob/master/bash/timestamp_lib_WIP.sh
- 如何制作 heredoc:https://linuxize.com/post/bash-heredoc/
评论
[[
bash
ksh
[
AK-47