我正在尝试解析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的方式似乎是一个问题。我很感激指导。
答案 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解析之前,也会在输入每一行后立即执行。
默认情况下,历史记录扩展字符为!,并且 - 大部分都记录在案 - 会触发历史记录扩展,但不包括:
当后面跟着空格或 =
如果设置了shell选项extglob
,则后跟( [Note 1]
如果它出现在单引号字符串
如果前面有 \ [注2并见下文]
如果前面有 $ 或 $ { [注1]
如果前面有 [ [注1]
(截至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周围的引号不是必需的,但通常它们会是,并且它们不会造成伤害。)
备注:强>
当以下字符是某种形式的左括号时,禁止历史扩展只有在字符串的其余部分中有相应的右括号时才有效。但是,它不必与开括号完全匹配。例如:
# 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
如bash
手册中所示,如果前面有反斜杠,则将历史记录扩展字符视为普通字符。这确实是真的;反斜杠以后是否会被视为转义字符并不重要:
$ echo \!
!
$ echo \\!
\!
$ echo \\\!
\!
\ 也禁止双引号内的历史扩展,但\!
不是双引号字符串中的有效转义序列,因此不会删除反斜杠:
$ echo "\!"
\!
$ echo "\\!"
\!
$ echo "\\\!"
\\!
我在写这篇文章时引用了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)
只有在启用历史记录扩展时才会发生这种情况,通常情况下它并不适用于脚本。
不要试图解决它,而是找出为什么要启用历史记录扩展以及为什么这样做它不是。
如果您使用. foo
或source foo
执行脚本,请改用./foo
。
如果您将此作为.bashrc
中的函数或类似内容编写,请考虑将其作为单独的脚本。
如果您的脚本(或BASH_ENV)明确执行set -H
,请不要这样做。
答案 4 :(得分:0)
引用''
或\
或使用set +H
或shopt -u -o histexpand
停用历史记录展开。请参阅History Expansion。