6

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

4

5 回答 5

13

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

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

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

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

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

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

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

set -o history -o histexpand

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

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

set -o history

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

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

如何解析历史扩展

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

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

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

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

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

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

  5. 如果前面有$or ${[注 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 完全不同。

于 2014-07-29T17:55:39.090 回答
6

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

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

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

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

VNCServerAndDisplayNumber=$(awk '/desktop/ {print $NF}' <<<"$VNCServerResponse")
于 2014-07-28T20:16:50.093 回答
2

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

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

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

于 2014-07-28T19:53:11.627 回答
2

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

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

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

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

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

于 2014-07-28T20:26:00.907 回答
0

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

于 2014-07-29T18:17:36.440 回答