How to escape history expansion exclamation mark ! inside a double quoted string?
If History Expansion is enabled, you can only echo the !
character if it is put in single quotes, escaped or if followed by a whitespace character, carriage return, or =
.
From man bash
:
Only backslash (\) and single quotes can quote the history
expansion character.
Several characters inhibit history expansion if found immediately fol-
lowing the history expansion character, even if it is unquoted: space,
tab, newline, carriage return, and =.
I believe the key word here is “Only”. The examples provided in the question only consider the outer most quotes being double quotes.
In your last example,
echo "$(echo '!b')"
the exclamation point is not single-quoted. Because history expansion occurs so early in the parsing process, the single quotes are just part of the double-quoted string; the parser hasn't recognized the command substitution yet to establish a new context where the single quotes would be quoting operators.
To fix, you'll have to temporarily turn off history expansion:
set +H
echo "$(echo '!b')"
set -H
Sometimes you need to make a small addition to a big command pipe
The OP's "Good, but verbose" example is actually pretty awesome for many cases.
Please forgive the contrived example. The whole reason I need such a solution is that I have a lot of distracting, nested code. But, it boils down to: I must do a !d
in sed
within a double quoted bash command expansion.
This works
$ ifconfig | sed '/inet/!d'
inet 127.0.0.1 netmask 0xff000000
…
This does not
$ echo "$(ifconfig | sed '/inet/!d')"
-bash: !d': event not found
This is a simplest compromise
$ echo "$(ifconfig | sed '/inet/'\!'d')"
inet 127.0.0.1 netmask 0xff000000
…
Using the compromise allows me to insert a few characters into the existing code and produce a Pull Request that anyone can understand… even though resulting code is more difficult to understand. If I did a complete refactor, the code reviewers would have a much more challenging time verifying it. And of course this bash has no unit tests.
This was repeatedly reported as a bug, most recently against bash 4.3 in 2014, for behavior going back to bash 3.
There was some discussion whether this constituted a bug or expected but perhaps undesirable behavior; it seems the consensus has been that, however you want to characterize the behavior, it shouldn't be allowed to continue.
It's fixed in bash 4.4, echo "$(echo '!b')"
doesn't expand, echo "'!b'"
does, which I regard as proper behavior because the single quotes are shell syntax markers in the first example and not in the second.