Bash 历史扩展是 bash 命令行解析器中的一个非常奇怪的角落,并且您显然遇到了意外的历史扩展,如下所述。然而,脚本中的任何类型的历史扩展都是意外的,因为通常脚本中不启用历史扩展;甚至脚本也不能运行source
(or .
) 内置。
如何启用(或禁用)历史扩展
有两个 shell 选项可以控制历史扩展:
必须设置这两个选项才能识别历史扩展。 (我发现手册对这种互动不清楚,但它足够合乎逻辑。)
根据 bash 手册,这些选项对于非交互式 shell 是未设置的,因此如果您想在脚本中启用历史扩展(我无法想象您想要这样做的原因),您需要设置它们:
set -o history -o histexpand
脚本运行的情况source
更复杂(我要说的仅适用于 bash v4,并且由于它没有记录在将来可能会发生变化)。 [注3]
历史记录(以及相应的扩展)被关闭source
'd 脚本,但通过内部标志,据我所知,该标志不可见。它肯定不会出现在$SHELLOPTS
。自从一个source
d 脚本在当前的 bash 上下文中运行,它共享当前的执行环境,包括 shell 选项。所以在执行一个source
从交互式会话启动的 d 脚本,您将看到两者history
and histexpand
in $SHELLOPTS
,但不会发生历史扩展。为了启用它,您需要:
set -o history
这不是一个无操作,因为它具有重置抑制历史记录的内部标志的副作用。设置histexpand
shell 选项没有这种副作用。
简而言之,我不确定您如何设法在脚本中启用历史记录扩展(如果确实,行为不当的命令位于脚本中而不是交互式 shell 中),但您可能需要考虑不这样做,除非您有一个很好的理由。
历史扩展是如何解析的
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 Meta is 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.
By default, the history expansion character is !, and -- as mostly documented -- that will trigger history expansion except:
when it is followed by whitespace or =
if the shell option extglob
is set, and it is followed by ( [Note 1]
如果它出现在单引号字符串中
if it is preceded by a \ [Note 2 and see below]
if it is preceded by $ or ${ [Note 1]
if it is preceded by [ [Note 1]
(从 bash v4.3 开始)如果它是双引号字符串中的最后一个字符。
The immediate issue here is the precise interpretation of the third case, an ! appearing inside of a single-quoted string. Normally, bash
starts a new quoting context for a command substitution ($(...)
or the deprecated backtick notation). For example:
$ 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)"
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 "\!"
\!
在这个特定的例子中——以及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 周围的引号是不必要的,但通常是必要的,而且它们没有什么害处。)
Notes:
-
当后面的字符是某种形式的左括号时,只有在字符串的其余部分中存在相应的右括号时,历史扩展的抑制才起作用。但是,它不必真正匹配左括号。例如:
# 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 \\\!
\!
\ 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 "\\\!"
\\!
在撰写本文时,我指的是 bash v4.2 的源代码,因此任何未记录的行为可能与 v4.3 完全不同。