Bash The history extension is a very strange angle in the bash command line syntax, and you are clearly facing an unexpected history extension, which is explained below. However, any story extension in the script is unexpected, because usually the story extension is not included in scripts; even scripts do not run with the built-in source (or . ).
How story extension is enabled (or disabled)
There are two shell options that control story expansion:
set -o history : required to record history.
set -H (or set -o histexpand ): Additionally, the history extension must be enabled.
Both of these parameters must be set to recognize the extension of the story. (I found the manual unclear in this interaction, but it is logical enough.)
According to the bash manual, these parameters are not configured for non-interactive shells, so if you want to enable the history extension in the script (and I can not imagine the reason why you need it), you will need to install them:
set -o history -o histexpand
The situation for scripts running with source is more complicated (and what I'm going to say applies only to bash v4, and since it is not documented, it may change in the future). [Note 3]
History recording (and therefore extension) is disabled in source'd scripts, but through an internal flag, which, as far as I know, does not become visible. This, of course, does not appear in $SHELLOPTS . Since source d script works in the current bash context, it shares the current runtime, including shell parameters. Thus, when you run the source d script initiated from an interactive session, you will see both history and histexpand in $SHELLOPTS , but the history will not expand. To enable it, you need to:
set -o history
which is not no-op, since it has the side effect of discarding the internal flag, which suppresses the history record. The histexpand parameter of the membrane does not have this side effect.
In short, I'm not sure how you managed to enable the story extension in the script (if, indeed, the wrong command was in the script, and not in the interactive shell), but you might want to not do this unless you have a really good reason .
How is history expansion analyzed?
The implementation of the bash history extension is designed to work with readline , so that it can be executed while the command is being entered. (By default, this function is tied to Meta- ^ , usually Meta to ESC , but you can also configure it.) However, it also runs immediately after entering each line before any bash parsing is performed.
The default symbol is the extension of the story ! and, as a rule, documented, which will lead to an extension of the story, with the exception of:
when followed by spaces or =
if the extglob shell parameter is extglob , it follows ( [Note 1]
if it is displayed in one quotation mark
if preceded by \ [Note 2 and see below]
if preceded by $ or $ { [Note 1]
if preceded by [ [Note 1]
(Starting with bash v4.3) if this is the last character in a string with two quotes.
The immediate problem here is an accurate interpretation of the third case, ah ! - inside one quotation mark. Normally, bash launches a new citation context to substitute a command ( $(...) or an obsolete countdown entry). For example:
$ s=SUBSTITUTED $
However, the story extension scanner is not so smart. It tracks quotes, but not command spoofing. So, as far as possible, both single quotes in the above example are single quotes with double quotes, i.e. ordinary characters. Thus, the expansion of the story 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 completely normal command like sed '/foo/!d' file , where you expect single quotes to protect you from expanding the story, and you put it as a replacement for commands with two quotes:
result="$(sed '/foo/!d' file)"
you suddenly find that symbol ! is a symbol of the expansion of history. Worse, you cannot fix this, the backslash by avoiding the exclamation point, because although "\!" prevents the extension of the story, it does not remove the backslash:
$ echo "\!" \!
In this particular example - and in OP - double quotes are completely unnecessary because the right side of the variable assignment does not undergo either file name extension or word splitting. However, there are other contexts in which removing double quotes can change semantics:
# Undesired history expansion printf "The answer is '%s'\n" "$(sed '/foo/!d' file)"
In this case, the best solution is probably to include the sed argument in the variable
# Works sed_prog='/foo/!d' printf "The answer is '%s'\n" "$(sed "$sed_prog" file)"
(Quotations around $ sed_prog were not necessary in this case, but usually they would have been, and they would not have hurt.)
Notes:
Inhibiting a story extension when the next character is some form of open parenthesis only works if there is a matching closing bracket in the rest of the line. However, it should not really match the open parenthesis. For example:
# No matching close parenthesis $ echo "!(" bash: !: event not found
As stated in the bash manual, a history extension character is considered a regular character if it is immediately preceded by a backslash. This is literally true; it doesn't matter if the backslash is later treated as an escape character or not:
$ echo \! ! $ echo \\! \! $ echo \\\! \!
\ also prevents history from expanding inside double quotes, but \! is not a valid escape sequence inside a double-quoted string, so the backslash is not removed:
$ echo "\!" \! $ echo "\\!" \! $ echo "\\\!" \\!
I mean the source code for bash v4.2, as I write this, so any undocumented behavior can be completely different compared to v4.3.