如何逃避历史扩展感叹号!在一个双引号字符串里面?

时间:2014-03-02 08:56:22

标签: bash syntax quotes

编辑:命令替换对于令人惊讶的行为不是必需的,尽管它是最常见的用例。同样的问题仅适用于echo "'!b'"

b=a

# Enable history substitution.
# This option is on by default on interactive shells.
set -H

echo '!b'
# Output: '!b'
# OK. Escaped by single quotes.

echo $(echo '!b')
# Output: '!b'
# OK. Escaped by single quotes.

echo "$(echo '$b')"
# Output: '$b'
# OK. Escaped by single quotes.

echo "$(echo '!b')"
# Output: history expands
# BAD!! WHY??
  • 在最后一个示例中,退出!的最佳方式是什么?
  • 为什么即使我使用单引号也没有转义,而echo "$(echo '$b')"是? !$之间有什么区别?
  • 为什么echo $(echo '!b')(没有引号)有效? (由@MBlanc指出)。

我更愿意这样做:

  • set +H因为我需要set -H来保持shell状态

  • 反斜杠转义,因为每!我需要一个,它必须在引号之外:

    echo "$(echo '\!a')"
    # '\!a'.
    # No good.
    
    echo "$(echo 'a '\!' b '\!' c')"
    # a ! b ! c
    # Good, but verbose.
    
  • echo $(echo '!b')(没有引号),因为该命令可以返回空格。

版本:

bash --version | head -n1
# GNU bash, version 4.2.25(1)-release (i686-pc-linux-gnu)

4 个答案:

答案 0 :(得分:20)

在上一个例子中,

echo "$(echo '!b')"

感叹号不是单引号。因为历史扩展在解析过程的早期发生,所以单引号只是双引号字符串的一部分;解析器还没有识别命令替换,以建立一个新的上下文,其中单引号将引用运算符。

要修复,您必须暂时关闭历史记录扩展:

set +H
echo "$(echo '!b')"
set -H

答案 1 :(得分:4)

This was repeatedly reported as a bug, most recently against bash 4.3 in 2014, for behavior going back to bash 3

有人讨论这是一个错误或预期但可能是不良行为;似乎已经达成共识,但是你想要表征这种行为,不应该让它继续下去。

它已在bash 4.4中修复,echo "$(echo '!b')"不会展开,echo "'!b'"会这样做,我认为这是正确的行为,因为单引号是第一个示例中的shell语法标记,而不是第二个示例中的shell语法标记。

答案 2 :(得分:2)

如果启用了历史记录扩展,则只有在!字符放入单引号,转义或后跟空格字符,回车或=时才能回显man bash字符。

来自 Only backslash (\) and single quotes can quote the history expansion character. Several characters inhibit history expansion if found immediately fol- lowing the history expansion character, even if it is unquoted: space, tab, newline, carriage return, and =.

{{1}}

我相信这里的关键词是“只有”。问题中提供的示例仅将最外面的引号视为双引号。

答案 3 :(得分:0)

有时您需要在大型命令管道中添加少量内容

在很多情况下,OP的“很好,但很冗长”的示例实际上很棒。

请原谅人为的例子。我需要这种解决方案的全部原因是,我有很多分散注意力的嵌套代码。但是,归结为:我必须在双引号bash命令扩展内的!d中进行sed

这有效

$ ifconfig | sed '/inet/!d'
inet 127.0.0.1 netmask 0xff000000
…

这不是

$ echo "$(ifconfig | sed '/inet/!d')"
-bash: !d': event not found

这是最简单的折衷方案

$ echo "$(ifconfig | sed '/inet/'\!'d')"
inet 127.0.0.1 netmask 0xff000000
…

使用折衷方案使我可以在现有代码中插入几个字符,并生成任何人都可以理解的Pull请求……即使生成的代码更难以理解。如果我进行了完整的重构,则代码审阅者将需要更多的时间来验证它。当然,这个bash没有单元测试。