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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/25003162/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-18 10:58:53  来源:igfitidea点击:

How to address error "bash: !d': event not found" in Bash command substitution

bashparsingsedcommand-substitution

提问by d3pd

I am attempting to parse the output of a VNC server startup event and have run into a problem in parsing using sed in a command substitution. Specifically, the remote VNC server is started in a manner such as the following:

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

address1="[email protected]"
VNCServerResponse="$(ssh "${address1}" 'vncserver' 2>&1)"

The standard error output produced in this startup event is then to be parsed in order to extract the server and display information. At this point the content of the variable VNCServerResponseis something such as the following:

然后将解析此启动事件中产生的标准错误输出,以提取服务器和显示信息。此时变量的内容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

This output can be parsed in the following way in order to extract the server and display information:

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

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

The result is something such as the following:

结果如下所示:

lxplus0186.cern.ch:1

What I want to do is use this parsing in a command substitution something like the following:

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

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

On attempting to do this, I am presented with the following error:

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

bash: !d': event not found

I am not sure how to address this. It appears to be a problem in the way sed is being used in the command substitution. I would appreciate guidance.

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

采纳答案by Tom Fenech

The problem is that within double quotes, bash is trying to expand !dbefore passing it to the subshell. You can get around this problem by removing the double quotes but I would also propose a simplification to your script:

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

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

This simply prints the last field on the line containing the word "desktop".

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

On a newer bash, you can use a herestring rather than piping an echo:

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

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

回答by rici

Bash history expansion is a very odd corner in the bash command line parser, and you are clearly running into an unexpected history expansion, which is explained below. However, any sort of history expansion in a script is unexpected, because normally history expansion is not enabled in scripts; not even scripts run with the source(or .) builtin.

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

How history expansion is enabled (or disabled)

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

There are two shell options which control history expansion:

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

  • set -o history: Required for the history to be recorded.

  • set -H(or set -o histexpand): Additionally required for history expansion to be enabled.

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

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

Both of these options must be set for history expansion to be recognized. (I found the manual unclear on this interaction, but it's logical enough.)

必须设置这两个选项才能识别历史扩展。(我发现手册中关于这种交互的内容不清楚,但它是合乎逻辑的。)

According to the bash manual, these options are unset for non-interactive shells, so if you want to enable history expansion in a script (and I cannot imagine a reason you would want this), you would need to set both of them:

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

set -o history -o histexpand

The situation for scripts run with sourceis more complicated (and what I'm about to say only applies to bash v4, and since it's undocumented in might change in the future). [Note 3]

脚本运行的情况source更加复杂(我将要说的仅适用于 bash v4,而且由于它没有记录在未来可能会改变)。[注3]

History recording (and consequently expansion) is turned off in source'd scripts, but through an internal flag which, as far as I know, is not made visible. It certainly does not appear in $SHELLOPTS. Since a sourced script runs in the current bash context, it shares the current execution environment, including shell options. So in the execution of a sourced script initiated from an interactive session, you'll see both historyand histexpandin $SHELLOPTS, but no history expansion will take place. In order to enable it, you need to:

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

set -o history

which is not a no-op because it has the side-effect of resetting the internal flag which suppresses history recording. Setting the histexpandshell option does not have this side-effect.

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

In short, I'm not sure how you managed to enable history expansion in a script (if, indeed, the misbehaving command was in a script and not in an interactive shell), but you might want to consider not doing so, unless you have a really good reason.

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

How history expansion is parsed

如何解析历史扩展

The bash implementation of history expansion is designed to work with readline, so that it can be performed during command input. (By default this function is bound to Meta-^; generally Metais ESC, but you can customize that as well.) However, it is also performed immediately after each line is input, before any bash parsing is performed.

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

By default, the history expansion character is !, and -- as mostly documented -- that will trigger history expansion except:

默认情况下,历史扩展字符是!, 并且——正如大多数文档所描述的——将触发历史扩展,除了:

  1. when it is followed by whitespace or =

  2. if the shell option extglobis set, and it is followed by ([Note 1]

  3. if it appears in a single-quoted string

  4. if it is preceded by a \[Note 2 and see below]

  5. if it is preceded by $or ${[Note 1]

  6. if it is preceded by [[Note 1]

  7. (As of bash v4.3) if it is the last character in a double-quoted string.

  1. 当它后面跟着空格或 =

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

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

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

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

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

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

The immediate issue here is the precise interpretation of the third case, an !appearing inside of a single-quoted string. Normally, bashstarts a new quoting context for a command substitution ($(...)or the deprecated backtick notation). For example:

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

However, the history expansion scanner isn't that intelligent. It keeps track of quotes, but not of command substitution. So as far as it is concerned, both of the single quotes in the above example are double-quoted single quotes, which is to say ordinary characters. So history expansion occurs in both of them:

然而,历史扩展扫描器并不是那么智能。它跟踪引号,但不跟踪命令替换。所以就其而言,上面例子中的两个单引号都是双引号单引号,也就是普通字符。所以历史扩展发生在它们两个中:

# 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

So if you have a perfectly normal command like sed '/foo/!d' file, where you would expect the single-quotes to protect you from history-expansion, and you put it inside a double-quoted command substitution:

因此,如果您有一个完全正常的命令,例如sed '/foo/!d' file,您希望单引号可以保护您免受历史扩展的影响,并且您将它放在双引号命令替换中:

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

you suddenly find that the !is a history expansion character. Worse, you can't fix this by backslash escaping the exclamation point, because although "\!"inhibits history expansion, it doesn't remove the backslash:

你突然发现这!是一个历史扩展字符。更糟糕的是,你不能通过反斜杠转义感叹号来解决这个问题,因为虽然"\!"抑制了历史扩展,但它不会删除反斜杠:

$ echo "\!"
\!

In this particular example -- and the one in the OP -- the double quotes are completely unnecessary, because the right-hand side of a variable assignment does not undergo either filename expansion nor word splitting. However, there are other contexts in which removing the double quotes would change the semantics:

在这个特定的例子中——以及 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)

In this case, the best solution is probably to put the sed argument in a variable

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

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

(The quotes around $sed_prog were not necessary in this case but usually they would be, and they do no harm.)

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



Notes:

笔记:

  1. The inhibition of history expansion when the following character is some form of open parenthesis only works if there is a corresponding close parenthesis in the rest of the string. However, it doesn't have to really match the open parenthesis. For example:

    # 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. As indicated in the bashmanual, a history-expansion character is treated as an ordinary character if immediately preceded by a backslash. This is literally true; it doesn't matter whether the backslash will later be considered an escape character or not:

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

    \also inhibits history expansion inside double quotes, but \!is not a valid escape sequence inside the double quoted string, so the backslash is not removed:

    $ echo "\!"
    \!
    $ echo "\!"
    \!
    $ echo "\\!"
    \!
    
  3. I'm referring to the source code for bash v4.2 as I write this, so any undocumented behaviour may be completely different as of v4.3.

  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 完全不同。

回答by that other guy

This only happens when history expansion is enabled, which it normally isn't and definitely shouldn't be for scripts.

这仅在启用历史扩展时发生,它通常不会并且绝对不应该用于脚本。

Rather than trying to work around it, figure out why history expansion is enabled and what to do so it isn't.

与其试图解决这个问题,不如弄清楚为什么启用历史扩展以及不这样做的原因。

  1. If you're executing your script with . fooor source foo, use ./fooinstead.

  2. If you're writing this as a function in .bashrcor similar, consider making it a separate script.

  3. If your script (or BASH_ENV) explicitly does set -H, don't.

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

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

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

回答by Etan Reisner

Don't wrap the $(...)command substitution in double quotes. You are asking the shell to perform evaluation on the contents of the quotes and are hitting the history substitution expansion feature. Drop the quotes and you stop telling the shell to do that and you won't hit that problem.

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

And yes, dropping those quotes is safe on that assignment line even if the output may contain spaces or newlines or whatever. Assignments of that sort are not going to split on those the way command substitution or variable evaluation will on a normal shell execution line.

而且,下降的报价是对分配行安全,即使输出可能包含空格或换行或什么的。那种类型的赋值不会像命令替换或变量评估在正常的 shell 执行行上那样分裂。

Alternatively, disable history expansion in your shell/script before you run that. (It should be off when running a script by default I believe anyway.)

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

回答by konsolebox

Quote it with ''or \or disable history expansion with set +Hor shopt -u -o histexpand. See History Expansion.

用引号,''\或禁用历史扩展与set +Hshopt -u -o histexpand。请参阅历史扩展