如何在Bash命令替换中解决错误"bash:!d':event not found"

d3p*_*3pd 5 bash parsing sed command-substitution

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

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

然后解析在此启动事件中生成的标准错误输出,以便提取服务器和显示信息.此时变量的内容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
Run Code Online (Sandbox Code Playgroud)

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

echo "${VNCServerResponse}" | sed '/New.*desktop.*is/!d' \
    | awk -F" desktop is " '{print $2}'
Run Code Online (Sandbox Code Playgroud)

结果如下:

lxplus0186.cern.ch:1
Run Code Online (Sandbox Code Playgroud)

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

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

在尝试这样做时,我收到以下错误:

bash: !d': event not found
Run Code Online (Sandbox Code Playgroud)

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

ric*_*ici 12

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

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

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

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

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

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

根据bash手册,这些选项对于非交互式shell没有设置,因此如果你想在脚本中启用历史记录扩展(我无法想象你想要这个的原因),你需要设置它们:

set -o history -o histexpand
Run Code Online (Sandbox Code Playgroud)

脚本运行的情况source更复杂(我将要说的仅适用于bash v4,因为它未记录在未来可能会改变).[注3]

历史记录(以及因此扩展)在source'd脚本中被关闭,但是通过内部标志,据我所知,该标志不可见.它当然不会出现$SHELLOPTS.由于sourced脚本在当前bash上下文中运行,因此它共享当前执行环境,包括shell选项.因此,在执行source从交互式会话发起d脚本,你会看到两个historyhistexpand$SHELLOPTS,但没有历史扩展将发生.要启用它,您需要:

set -o history
Run Code Online (Sandbox Code Playgroud)

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

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

如何解析历史扩展

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

默认情况下,历史记录扩展字符是!- 并且 - 大部分都记录在案 - 会触发历史记录扩展,除了:

  1. 当它后面是空格或 =

  2. 如果extglob设置了shell选项,则后跟([注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
Run Code Online (Sandbox Code Playgroud)

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

# 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
Run Code Online (Sandbox Code Playgroud)

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

result="$(sed '/foo/!d' file)"
Run Code Online (Sandbox Code Playgroud)

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

$ echo "\!"
\!
Run Code Online (Sandbox Code Playgroud)

在这个特定的例子中 - 和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)
Run Code Online (Sandbox Code Playgroud)

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

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

(在这种情况下,$ 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
    
    Run Code Online (Sandbox Code Playgroud)
  2. bash手册中所示,如果紧接在前面加上反斜杠,则将历史扩展字符视为普通字符.这确实是真的; 反斜杠以后是否会被视为转义字符并不重要:

    $ echo \!
    !
    $ echo \\!
    \!
    $ echo \\\!
    \!
    
    Run Code Online (Sandbox Code Playgroud)

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

    $ echo "\!"
    \!
    $ echo "\\!"
    \!
    $ echo "\\\!"
    \\!
    
    Run Code Online (Sandbox Code Playgroud)
  3. 在我写这篇文章时,我指的是bash v4.2的源代码,因此从v4.3开始,任何未记录的行为都可能完全不同.

  • 历史扩展失败并没有在历史中结束的事实是一个可怕的烦恼。 (2认同)

Tom*_*ech 6

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

VNCServerAndDisplayNumber=$(echo "$VNCServerResponse" | awk '/desktop/ {print $NF}')
Run Code Online (Sandbox Code Playgroud)

这只是在包含单词"desktop"的行上打印最后一个字段.

在较新的bash上,你可以使用herestring而不是管道echo:

VNCServerAndDisplayNumber=$(awk '/desktop/ {print $NF}' <<<"$VNCServerResponse")
Run Code Online (Sandbox Code Playgroud)