在 shell 脚本中使用 $() 而不是反引号有啥好处?

Posted

技术标签:

【中文标题】在 shell 脚本中使用 $() 而不是反引号有啥好处?【英文标题】:What is the benefit of using $() instead of backticks in shell scripts?在 shell 脚本中使用 $() 而不是反引号有什么好处? 【发布时间】:2012-03-16 00:37:56 【问题描述】:

有两种方法可以捕获bash中的命令行输出:

    Legacy Bourne shell 反引号``

    var=`command`
    

    $() 语法(据我所知,它是 Bash 特有的,或者至少不受非 POSIX 旧 shell (如原始 Bourne)支持)

    var=$(command)
    

与反引号相比,使用第二种语法有什么好处吗?还是两者完全 100% 等效?

【问题讨论】:

$() 是 POSIX 并受到所有现代 Bourne shell 的支持,例如ksh、bash、ash、dash、zsh、busybox,应有尽有。 (不太现代的是 Solaris /bin/sh,但在 Solaris 上,您会确保使用现代的 /usr/xpg4/bin/sh)。 另外,关于在别名中使用 $() 和反引号的说明。如果您的.bashrc 中有alias foo=$(command),那么在.bashrc 解释期间运行别名命令本身时,将执行command。使用alias foo=`command`,每次运行别名时都会执行command。但是,如果您使用$() 表单(例如alias foo=\$(command))转义$,它也会在每次运行别名时执行,而不是在.bashrc 解释期间执行。无论如何,据我通过测试可以看出;我在 bash 文档中找不到任何解释这种行为的内容。 @dirtside 这是哪个 shell,我已经测试了 bash 和 POSIX shell,当我来源时,反引号确实会被执行。简单示例:alias curDate=`date` 例如,在获取并运行 curDate 后,我收到消息它找不到命令 Mon(Sourced on Monday)。 @dirtside 这不是真的。即使使用别名 foo=`command` command 也只会执行一次。我检查了它: function aaa() printf date;回声 aaa >> ~/test.txt; 别名 test1=aaa。无论别名 (test1) 执行了多少次,函数 aaa 只执行一次(每次登录后)。我使用了 .bashrc(在 Debian 10 上)。 这能回答你的问题吗? What is the difference between $(command) and `command` in shell programming? 【参考方案1】:

最主要的是能够嵌套它们,命令中的命令,而不会失去理智,试图弄清楚某种形式的转义是否适用于反引号。

一个例子,虽然有点做作:

deps=$(find /dir -name $(ls -1tr 201112[0-9][0-9]*.txt | tail -1l) -print)

这将为您提供/dir 目录树中与 2011 年 12 月以来最早日期文本文件同名的所有文件的列表(a)

另一个例子是获取父目录的名称(不是完整路径):

pax> cd /home/pax/xyzzy/plugh
pax> parent=$(basename $(dirname $PWD))
pax> echo $parent
xyzzy

(a) 既然 specific 命令实际上可能不起作用,我还没有测试功能。所以,如果你投票给我,你就忽略了它的意图 :-) 这只是说明你如何嵌套,而不是作为一个无错误的生产就绪的 sn-p。

【讨论】:

我希望 SO 上的所有代码都是生产就绪的 sn-ps,设计符合 NASA 航天飞机代码可靠性标准。任何不足都会获得标记和删除投票。 @DVK 如果你不是在开玩笑,我不同意代码贡献应该被标记为未能自动重新许可远离 SO 默认值(CC-BY-SA 或 MIT 许可证),以便允许此类保证或目的的适用性。相反,我会自担风险重用 SO 上的代码,并根据有用性、技术优点等对贡献进行投票。 @chrstphrchvz,如果您查看我的个人资料,您会看到这个小 sn-p:“我在 Stack Overflow 上发布的所有代码都包含在“随心所欲地做任何事情”中许可证,其全文是:Do whatever the heck you want with it." :-) 无论如何,我很确定这是来自 DVK 的幽默。 但是如果它是“cd /home/pax/xyzzy/plover”呢?你会发现自己在曲折的小通道的迷宫中,完全不同吗? @wchargin 你知道这个事件是在 C++ 语言中添加使用定义的文字的主要动机吗? en.cppreference.com/w/cpp/language/user_literal【参考方案2】:

假设您要查找与安装gcc 的位置对应的lib 目录。你有一个选择:

libdir=$(dirname $(dirname $(which gcc)))/lib

libdir=`dirname \`dirname \\\`which gcc\\\`\``/lib

第一个比第二个更容易 - 使用第一个。

【讨论】:

很高兴看到这些命令替换周围的一些引号! 至少对于 bash,@TomFenech 的评论不适用于作业。 x=$(f); x=`f` 的行为与 x="$(f)"; x="`f`" 相同。相反,数组赋值x=($(f)); x=(`f`) do 在调用命令时按预期在$IFS 字符处执行拆分。这很方便(x=1 2 3 4 没有意义)但不一致。 @kdb 你说得对,x=$(f) 没有引号。我应该更具体;我提议使用libdir=$(dirname "$(dirname "$(which gcc)")")/lib(引用inner 命令替换)。如果不加引号,您仍然会受到通常的分词和全局扩展的影响。【参考方案3】:

反引号 (`...`) 是传统语法,只有最古老的不兼容 POSIX 的 bourne-shell 才需要,$(...) 是 POSIX 且更受欢迎,原因如下:

反引号内的反斜杠 (\) 以不明显的方式处理:

$ echo "`echo \\a`" "$(echo \\a)"
a \a
$ echo "`echo \\\\a`" "$(echo \\\\a)"
\a \\a
# Note that this is true for *single quotes* too!
$ foo=`echo '\\'`; bar=$(echo '\\'); echo "foo is $foo, bar is $bar" 
foo is \, bar is \\

$() 内的嵌套引用要方便得多:

echo "x is $(sed ... <<<"$y")"

代替:

echo "x is `sed ... <<<\"$y\"`"

或写类似:

IPs_inna_string=`awk "/\`cat /etc/myname\`/"'print $1' /etc/hosts`

因为$() 使用全新的上下文进行引用

因为 Bourne 和 Korn shell 需要这些反斜杠,所以它是不可移植的,而 Bash 和 dash 不需要。

嵌套命令替换的语法更简单:

x=$(grep "$(dirname "$path")" file)

比:

x=`grep "\`dirname \"$path\"\`" file`

因为$() 强制执行一个全新的引用上下文,因此每个命令替换都受到保护并且可以单独处理,而无需特别关注引用和转义。使用反引号时,两级及以上会越来越难看。

还有几个例子:

echo `echo `ls``      # INCORRECT
echo `echo \`ls\``    # CORRECT
echo $(echo $(ls))    # CORRECT

它解决了使用反引号时行为不一致的问题:

echo '\$x' 输出 \$x echo `echo '\$x'` 输出 $x echo $(echo '\$x') 输出 \$x

反引号语法对嵌入命令的内容有历史限制,无法处理一些包含反引号的有效脚本,而较新的$() 表单可以处理任何类型的有效嵌入脚本。

例如,这些其他有效的嵌入脚本在左侧列中不起作用,但在右侧起作用IEEE

echo `                         echo $(
cat <<\eof                     cat <<\eof
a here-doc with `              a here-doc with )
eof                            eof
`                              )


echo `                         echo $(
echo abc # a comment with `    echo abc # a comment with )
`                              )


echo `                         echo $(
echo '`'                       echo ')'
`                              )

因此,$-prefixed command substitution 的语法应该是首选方法,因为它在视觉上清晰且语法清晰(提高了人类和机器的可读性),它是可嵌套和直观的,它的内部解析是独立的,并且它也更加一致(与从双引号内解析的所有其他扩展),其中反引号是唯一的例外,` 字符在与" 相邻时很容易伪装,使其更难以阅读,尤其是小或不寻常的字体。

来源:Why is $(...) preferred over `...` (backticks)?@BashFAQ

另见:

POSIX standard section "2.6.3 Command Substitution" POSIX rationale for including the $() syntax Command Substitution bash-hackers: command substitution

【讨论】:

重音样式替换中的嵌套引用实际上是未定义的,您可以在之外使用双引号或者或在内使用,但不能使用 b> 两者,便携。贝壳对它们的解释不同;有些需要反斜杠来转义它们,有些要求它们不能被反斜杠转义。【参考方案4】:

来自 man bash:

$(命令) 要么 `命令` Bash 通过执行命令并替换 com- 来执行扩展 用命令的标准输出命令替换,任何 尾随换行被删除。嵌入的换行符不会被删除,但它们 可以在分词期间删除。命令替换 $(cat file) 可以替换为等效但更快的 $(

【讨论】:

【参考方案5】:

除了其他答案,

$(...)

视觉效果优于

`...`

反引号看起来太像撇号了;这取决于您使用的字体。

(而且,正如我刚刚注意到的,反引号更难在内联代码示例中输入。)

【讨论】:

你一定有一个奇怪的键盘(或者我有?)。对我来说,输入反引号要容易得多 - 它们是左上角的键,不需要 SHIFT 或 ALT。 @DVK:我说的是它们的外观,而不是易于打字。 (我的键盘可能和你的一样。)不过,既然你提到了它,我认为我对 $ () 的肌肉记忆比对反引号更好; YMMV。 从来没有在 bash 中编程(从旧的 ksh 跳过到 Perl)所以绝对没有记忆那个特定的语法:) @DVK,我以为Keith指的是这里的非块代码(块代码意味着在行首使用四个空格)使用反引号来表示它,因此很难放置其中的反引号,是嵌套困难的另一个例证 :-) FWIW,您可能会发现代码和 /code 标记(执行非块代码的另一种方式)可以更容易地包含反引号。 @Pax - 明白了。呸!出于某种原因,我确实在精神上卡在了块代码上。【参考方案6】:

$() 允许嵌套。

out=$(echo today is $(date))

我认为反引号不允许这样做。

【讨论】:

你可以嵌套反引号;更难:out=`echo today is \`date\`` .【参考方案7】:

POSIX 标准定义了$(command) 形式的命令替换。今天使用的大多数 shell 都符合 POSIX 标准,并且支持这种首选形式,而不是过时的反引号表示法。 Shell 语言文档的 command substitution 部分 (2.6.3) 对此进行了描述:

命令替换允许将命令的输出替换为命令名称本身。当命令包含以下内容时,将发生命令替换:

$(<i>command</i>)

或(反引号版本):

`<i>command</i>`

shell 应该通过执行 command 来扩展命令替换 在子外壳环境中(请参阅Shell Execution Environment)和 替换命令替换(command 的文本加上 将 "$()" 或反引号) 与 命令,删除一个或多个 &lt;newline&gt; 字符的序列 替换结束。嵌入&lt;newline&gt; 字符前 不得删除输出;但是,它们可能被视为 字段分隔符并在字段拆分期间消除,具体取决于 IFS 的值和有效的引用。如果输出包含 任何空字节,行为未指定。

在命令替换的反引号样式中,&lt;backslash&gt; 应 保留其字面意思,除非后面跟着: '$' 、 '`' 或 &lt;backslash&gt;。应满足对匹配反引号的搜索 通过第一个未引用的非转义反引号;在此搜索过程中,如果 在 shell 注释中遇到非转义的反引号,a here-document,$(command) 的嵌入式命令替换 形式或带引号的字符串,会出现未定义的结果。单引号或 在“`...`”内开始但不结束的双引号字符串 序列产生未定义的结果。

使用 $(command) 形式,所有跟在 open 后面的字符 匹配右括号的括号构成 命令command 可以使用任何有效的 shell 脚本,除了 仅由产生未指定的重定向组成的脚本 结果。

命令替换的结果不作进一步处理 波浪号扩展、参数扩展、命令替换或 算术扩展。如果内部发生命令替换 双引号、字段拆分和路径名扩展不得 对替换结果执行。

命令替换可以嵌套。在 反引号版本,应用程序应在内部反引号之前 带有&lt;backslash&gt; 字符;例如:

\`<i>command</i>\`

shell 命令语言的语法对于以“$((”开头的扩展有歧义, 它可以引入以子shell开头的算术扩展或命令替换。 算术展开优先;即shell首先要确定 是否可以将展开式解析为算术展开式 并且只能将扩展解析为命令替换 如果它确定它不能将扩展解析为算术扩展。 执行此确定时,shell 不需要评估嵌套扩展。 如果它在尚未确定的情况下遇到输入结束 它不能将扩展解析为算术扩展, shell 应将扩展视为不完整的算术扩展并报告语法错误。 符合要求的应用程序应确保将“$(”和“(”分成两个标记 (即,用空格分隔它们)在以子 shell 开头的命令替换中。 例如,包含单个子 shell 的命令替换可以写成:

$( (<i>command</i>) )

【讨论】:

【参考方案8】:

我想出了一个完全有效的例子,$(...) 优于 `...`

我在运行Cygwin 的Windows 上使用remote desktop 并想遍历命令的结果。遗憾的是,由于远程桌面或 Cygwin 本身的原因,无法输入反引号字符。

在这种奇怪的设置中输入美元符号和括号会更容易。

【讨论】:

【参考方案9】:

在 2021 年,值得一提的是一个奇怪的事实,作为对其他答案的补充。

Microsoft DevOps YAML 管道的“脚本”可能包括Bash tasks。但是,$() 符号用于引用在 YAML 上下文中定义的变量,因此在这种情况下,应该使用反引号来捕获命令的输出。

这主要是在将脚本代码复制到 YAML 脚本时出现的问题,因为 DevOps 预处理器对不存在的变量非常宽容,因此不会出现任何错误消息。

【讨论】:

以上是关于在 shell 脚本中使用 $() 而不是反引号有啥好处?的主要内容,如果未能解决你的问题,请参考以下文章

shell脚本中的反引号使用 `

shell脚本中单引号和双引号的区别

shell 脚本中双引号 单引号 反引号 的区别

在go语言中,如何在反引号中调用变量的值而不是变量名

Shell脚本编写5-----Shell 基本运算符

shell脚本中 的 单引号和反引号经常混淆,请高手帮我区别它们的功能,谢谢