如何解决错误" bash:!d':未找到事件"在Bash命令替换中

时间:2014-07-28 19:49:03

标签: bash parsing sed command-substitution

我正在尝试解析VNC服务器启动事件的输出,并且在命令替换中使用sed解析时遇到了问题。具体来说,远程VNC服务器以如下方式启动:

address1="user1@lxplus.cern.ch"
VNCServerResponse="$(ssh "${address1}" 'vncserver' 2>&1)"

然后解析在此启动事件中生成的标准错误输出,以便提取服务器和显示信息。此时,变量VNCServerResponse的内容如下所示:

New 'lxplus0186.cern.ch:1 (user1)' desktop is lxplus0186.cern.ch:1

Starting applications specified in /afs/cern.ch/user/u/user1/.vnc/xstartup
Log file is /afs/cern.ch/user/u/user1/.vnc/lxplus0186.cern.ch:1.log

可以通过以下方式解析此输出,以便提取服务器并显示信息:

echo "${VNCServerResponse}" | sed '/New.*desktop.*is/!d' \
    | awk -F" desktop is " '{print $2}'

结果如下:

lxplus0186.cern.ch:1

我想要做的是在命令替换中使用此解析,如下所示:

VNCServerAndDisplayNumber="$(echo "${VNCServerResponse}" \
    | sed '/New.*desktop.*is/!d' | awk -F" desktop is " '{print $2}')"

尝试执行此操作时,出现以下错误:

bash: !d': event not found

我不知道如何解决这个问题。在命令替换中使用sed的方式似乎是一个问题。我很感激指导。

5 个答案:

答案 0 :(得分:12)

Bash历史记录扩展在bash命令行解析器中是一个非常奇怪的角落,您显然遇到了意外的历史记录扩展,这将在下面解释。但是,脚本中的任何类型的历史记录扩展都是意外的,因为通常在脚本中不启用历史记录扩展;甚至不能使用source(或.)内置版运行脚本。

如何启用(或禁用)历史记录扩展

有两个控制历史记录扩展的shell选项:

  • set -o history:要记录的历史记录是必需的。

  • set -H(或set -o histexpand):此外,还需要启用历史记录扩展。

必须设置这两个选项才能识别历史记录扩展。 (我发现手册不清楚这种互动,但它足够合乎逻辑。)

根据bash手册,这些选项未设置为非交互式shell,因此如果要在脚本中启用历史记录扩展(我无法想象您希望这样做的原因),则需要同时设置它们:

set -o history -o histexpand

使用source运行的脚本的情况更复杂(我将要说的仅适用于bash v4,并且因为它未记录的内容可能在将来发生变化)。 [注3]

历史记录(以及因此扩展)在source' d脚本中被关闭,但是通过内部标记,据我所知,该标记不可见。它当然不会出现在$SHELLOPTS中。由于source d脚本在当前bash上下文中运行,因此它共享当前执行环境,包括shell选项。因此,在执行从交互式会话启动的source d脚本时,您会在history中看到histexpand$SHELLOPTS,但不会进行历史记录扩展地点。要启用它,您需要:

set -o history

这不是无操作,因为它具有重置抑制历史记录的内部标志的副作用。设置histexpand shell选项没有这种副作用。

简而言之,我不确定您是如何设法在脚本中启用历史记录扩展的(如果事实上,行为错误的命令是在脚本中而不是在交互式shell中),但您可能不想考虑这样做,除非你有充分的理由。

如何解析历史记录扩展

历史扩展的bash实现旨在与readline一起使用,以便可以在命令输入期间执行。 (默认情况下,此函数绑定到 Meta - ^ ;通常 Meta ESC ,但您也可以自定义它。)但是,它在执行任何bash解析之前,也会在输入每一行后立即执行。

默认情况下,历史记录扩展字符为,并且 - 大部分都记录在案 - 会触发历史记录扩展,但不包括:

  1. 当后面跟着空格或 =

  2. 如果设置了shell选项extglob,则后跟 [Note 1]

  3. 如果它出现在单引号字符串

  4. 如果前面有 \ [注2并见下文]

  5. 如果前面有 $ $ { [注1]

  6. 如果前面有 [ [注1]

  7. (截至bash v4.3)如果它是双引号字符串中的最后一个字符。

  8. 这里的直接问题是第三种情况的精确解释,出现在单引号字符串中。通常,bash为命令替换($(...)或不推荐的反引号表示法)启动新的引用上下文。例如:

    $ s=SUBSTITUTED
    $ # The interior single quotes are just characters
    $ echo "'Echoing $s'"
    'Echoing SUBSTITUTED'
    $ # The interior single quotes are single quotes
    $ echo "$(echo 'Echoing $s')"
    Echoing $s
    

    然而,历史扩展扫描仪并不聪明。它跟踪引号,但不跟踪命令替换。所以就它而言,上面例子中的两个单引号都是双引号单引号,也就是普通字符。因此,历史扩张都发生在两者中:

    # A no-op to indicated history expansion
    $ HIST() { :; }
    # Single-quoted strings inhibit history expansion
    $ HIST
    $ echo '!!'
    !!
    # Double-quoted strings allow history expansion
    $ HIST
    $ echo "'!!'"
    echo "'HIST'"
    'HIST'
    # ... and it applies also to interior command substitution.
    $ HIST
    $ echo "$(echo '!!')"
    echo "$(echo 'HIST')"
    HIST
    

    因此,如果您有一个完全正常的命令,如sed '/foo/!d' file,您可以期望单引号保护您免受历史记录扩展,并将其置于双引号命令替换中:

    result="$(sed '/foo/!d' file)"
    

    你突然发现是一个历史扩展角色。更糟糕的是,你可以通过反斜杠逃避感叹号来解决这个问题,因为尽管"\!"禁止历史扩展,但它并没有消除反斜杠:

    $ echo "\!"
    \!
    

    在这个特定的例子中 - 和OP中的那个 - 双引号是完全没必要的,因为变量赋值的右侧既不进行文件扩展也不进行单词拆分。但是,还有其他上下文删除双引号会改变语义:

    # Undesired history expansion
    printf "The answer is '%s'\n" "$(sed '/foo/!d' file)"
    
    # Undesired word splitting
    printf "The answer is '%s'\n" $(sed '/foo/!d' file)
    

    在这种情况下,最好的解决方案可能是将sed参数放在变量

    # Works
    sed_prog='/foo/!d'
    printf "The answer is '%s'\n" "$(sed "$sed_prog" file)"
    

    (在这种情况下,$ sed_prog周围的引号不是必需的,但通常它们会是,并且它们不会造成伤害。)


    备注:

    1. 当以下字符是某种形式的左括号时,禁止历史扩展只有在字符串的其余部分中有相应的右括号时才有效。但是,它不必与开括号完全匹配。例如:

      # No matching close parenthesis
      $ echo "!("
      bash: !: event not found
      # The matching close parenthesis has nothing to do with the open
      $ echo "!(" ")"
      !( )
      # An actual extended glob: files whose names don't start with a
      $ echo "!(a*)"
      b
      
    2. bash手册中所示,如果前面有反斜杠,则将历史记录扩展字符视为普通字符。这确实是真的;反斜杠以后是否会被视为转义字符并不重要:

      $ echo \!
      !
      $ echo \\!
      \!
      $ echo \\\!
      \!
      

      \ 也禁止双引号内的历史扩展,但\!不是双引号字符串中的有效转义序列,因此不会删除反斜杠:

      $ echo "\!"
      \!
      $ echo "\\!"
      \!
      $ echo "\\\!"
      \\!
      
    3. 我在写这篇文章时引用了bash v4.2的源代码,因此从v4.3开始,任何未记录的行为都可能完全不同。

答案 1 :(得分:6)

问题是在双引号内,bash在将!d传递给子shell之前尝试扩展VNCServerAndDisplayNumber=$(echo "$VNCServerResponse" | awk '/desktop/ {print $NF}') 。您可以通过删除双引号来解决此问题,但我还建议您对脚本进行简化:

echo

这只是在包含单词" desktop"。

的行上打印最后一个字段

在较新的bash上,您可以使用herestring而不是管道VNCServerAndDisplayNumber=$(awk '/desktop/ {print $NF}' <<<"$VNCServerResponse")

{{1}}

答案 2 :(得分:2)

不要用双引号括起$(...)命令替换。您要求shell对引号的内容执行评估,并且正在进行历史记录替换扩展功能。删除引号,你不再告诉shell这样做,你就不会遇到这个问题。

并且,即使输出可能包含空格或换行符等,丢弃这些引号在该分配行上也是安全的。这种分配不会在命令替换或变量评估在普通shell执行行上的方式上进行拆分。

或者,在运行之前禁用shell /脚本中的历史记录扩展。 (默认情况下运行脚本时应该关闭它,无论如何。)

答案 3 :(得分:2)

只有在启用历史记录扩展时才会发生这种情况,通常情况下它并不适用于脚本。

不要试图解决它,而是找出为什么要启用历史记录扩展以及为什么这样做它不是。

  1. 如果您使用. foosource foo执行脚本,请改用./foo

  2. 如果您将此作为.bashrc中的函数或类似内容编写,请考虑将其作为单独的脚本。

  3. 如果您的脚本(或BASH_ENV)明确执行set -H,请不要这样做。

答案 4 :(得分:0)

引用''\或使用set +Hshopt -u -o histexpand停用历史记录展开。请参阅History Expansion