【问题标题】:How to address error "bash: !d': event not found" in Bash command substitution [duplicate]如何解决 Bash 命令替换中的错误“bash:!d':找不到事件”[重复]
【发布时间】:2014-09-20 02:35:34
【问题描述】:

我正在尝试解析 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 的方式似乎是一个问题。我将不胜感激。

【问题讨论】:

    标签: bash parsing sed command-substitution


    【解决方案1】:

    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 中。由于sourced 脚本在当前 bash 上下文中运行,因此它共享当前执行环境,包括 shell 选项。因此,在执行从交互式会话启动的sourced 脚本时,您将在$SHELLOPTS 中看到historyhistexpand,但不会发生历史扩展。为了启用它,您需要:

    set -o history
    

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

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

    如何解析历史扩展

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

    默认情况下,历史扩展字符是 !,并且 - 正如大部分文档所记录的那样 - 这将触发历史扩展,除了:

    1. 当它后跟空格或 =

    2. 如果设置了shell选项extglob,后面跟着( [注1]

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

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

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

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

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

    这里的直接问题是对第三种情况的精确解释,! 出现在单引号字符串中。通常,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 完全不同。

    【讨论】:

    • @konsolebox:谢谢。我希望能说明为什么在这种特殊情况下,用'' 或`\` 引用! 是行不通的。
    • 您的答案有其自己的细节,直到您添加了如何禁用其他答案中已经告知的历史扩展:)
    • 我不确定你的意思。 Only ‘\’ and ‘'’ may be used to escape the history expansion character. 不成立吗?顺便说一句,请不要试图用"" 使它复杂化。 "" 有自己的解析规则,与打开的命令行类似但不同。
    • 历史扩展失败不会出现在历史中的事实是一个可怕的烦恼。
    • 在 Bash 4.4-beta 中:“历史扩展现在跳过命令和进程替换中的历史扩展字符。”
    【解决方案2】:

    问题在于,在双引号内,bash 在将!d 传递给子shell 之前尝试对其进行扩展。您可以通过删除双引号来解决此问题,但我也建议您简化脚本:

    VNCServerAndDisplayNumber=$(echo "$VNCServerResponse" | awk '/desktop/ {print $NF}')
    

    这只是打印包含单词“桌面”的行上的最后一个字段。

    在较新的 bash 中,您可以使用 herestring 而不是管道echo

    VNCServerAndDisplayNumber=$(awk '/desktop/ {print $NF}' <<<"$VNCServerResponse")
    

    【讨论】:

      【解决方案3】:

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

      是的,即使输出可能包含空格或换行符或其他任何内容,在该分配行上删除这些引号也是安全的。这种类型的分配不会像命令替换或变量评估在正常的 shell 执行行上那样分开。

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

      【讨论】:

      • 不引用 $(...) 如何避免评估?一般来说,引号避免了分词和通配,但这些都不是在作业上执行的,实际上引号是无关紧要的。
      • @rici 我也不确定所涉及的机制,但从经验上看,在这种情况下,将命令替换用引号括起来会强制 shell 执行历史扩展(至少),因此导致有问题的问题将它们关闭(这是我的建议,因为正如你所说的那样它们不是必需的)不会导致这种行为。当然,这一切都应该在脚本中没有实际意义,因为无论如何都不应该在那里发生历史扩展。
      • 关于 bash 的奇妙之处在于,无论您认为对它了解多少,总有一些晦涩难懂的问题等着你。在这种情况下,至少有两个,也许更多。
      • @rici 确实。一百万年后,我永远不会猜到那里的引文会引发这样的历史扩张(或真的做任何事情)。虽然我不确定你在想什么第二个问题。
      • 我把我在一个答案中发现的所有问题都写了出来,这比应得的要长得多,但我认为完成了这项工作,我至少应该把它记录在某个地方。
      【解决方案4】:

      只有在启用历史扩展时才会发生这种情况,通常不会而且绝对不应该用于脚本。

      与其试图解决它,不如弄清楚为什么启用历史扩展以及不启用它的原因。

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

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

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

      【讨论】:

        【解决方案5】:

        使用''\ 引用它或使用set +Hshopt -u -o histexpand 禁用历史扩展。见History Expansion

        【讨论】:

          猜你喜欢
          • 2013-03-22
          • 2012-02-19
          • 2018-12-10
          • 1970-01-01
          • 2013-05-17
          • 1970-01-01
          • 1970-01-01
          • 2017-05-22
          相关资源
          最近更新 更多