4

The bash manual entry for double quotes (https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html) states:

If enabled, history expansion will be performed unless an ‘!’ appearing in double quotes is escaped using a backslash. The backslash preceding the ‘!’ is not removed.

Why is there a special case made for !? If I escape a $ (for example) the backslash doesn't appear in the output (e.g. compare echo "\$" -> $ and echo "\!" -> \!).

What do if I just want a literal ! inside double quotes?

A limitation I have for this problem is that I'm unable to easily manipulate the form of the bash invocation. By this I mean I'm stuck executing the command using a single double-quoted string, i.e. my-command "a string I generate in an external program that may contain !"

I can control how the string is generated, but I can't split it up on the command line (e.g. something like "part1"\!"part2" wouldn't work for me.)

Kusalananda
  • 333,661
Adam
  • 257
  • 1
    echo -e "\x22\x21\x22" – George Vasiliou Jan 31 '20 at 01:15
  • @GeorgeVasiliou can you elaborate a bit more on your solution? Might there be encoding issues? – Adam Jan 31 '20 at 01:37
  • Would that work outside of echo? I don't see any mention of that kind of strategy in the bash manual page – Adam Jan 31 '20 at 01:39
  • 2
    How exactly does the string that might contain an exclamation mark get into double-quotes? If it's from a variable or command substitution (i.e. cmd "$var" or cmd "$(othercmd)"), then there's no problem at all. If you're constructing a command line to be passed to bash for execution, just replace each exclamation mark with "'!'" (that is, a double-quote, a single-quote, and exclamation mark, another single-quote, and finally a double-quote). – Gordon Davisson Jan 31 '20 at 09:32

2 Answers2

4

Printing an string with a ! is not a problem when unquoted, quoted with single quotes, inside a C-string, or inside an script.

$ echo hello\!world 'hello!world' $'hello!world'
hello!world hello!world hello!world

$ echo 'echo "hello!world"' >file $ bash file hello!world

The script example is an special case in which histexpand isn't set (by default). So, disabling histexpand (set +o histexpand, same as shopt -ou histexpand or simply set +H) will have the same effect and avoid the history expansion.

However you are asking for a double quoted string in a (default) interactive shell without breaking the string in parts.Fine, what about this:

$ a='!'
$ echo "hello${a}world"
hello!world

About the historical Why?.

Why is there a special case made for !?

Because most of the ascii characters are not special (inside "):

$ printf '%s\n' "\a\b\c\d\e\f...\z \!\@\#\%     \$a \`date\` \\"

\a\b\c\d\e\f...\z !@#% $a date

Relevant text from man bash: Enclosing characters in double quotes preserves the literal value of all characters within the quotes, with the exception of $, `, \, and, when history expansion is enabled, !.

That is the way a backslash inside double quotes works in ancient shells, and the POSIX spec reflects that:

The <backslash> shall retain its special meaning as an escape character (see Escape Character (Backslash)) only when followed by one of the following characters when considered special:

$ ` " \ <newline>

0

Apparently, Bash not removing the backslash used to escape a ! appearing within double quotes (to inhibit history expansion) has been reported as a bug several times on the bug-bash mailing list.

The historical reason for this behavior that is mentioned in the answer you already have is indeed the explanation given by Bash's developer and maintainer. Quoting a bug-bash message from Chet Ramey:

As documented, the `!' may be escaped only with single quotes or a backslash. Double quotes don't work because `!' is not one of the characters that are treated specially within double quotes, according to the POSIX.2 spec and traditional sh behavior.

From the same source, about how to handle ! within double quotes:

You can either turn off history expansion with `set +o histexpand' or use backslash outside double quotes. Note that history expansion is not normally enabled when the shell is not interactive, so removing the backslash within a script should not be a problem.

(A few variations of the same statements can be found on that mailing list, e.g. 1 and 2, which also mention the relationship with the C shell).

If you can't use any of those solutions: various workarounds are shown in the answers to How to echo a bang!, such as setting the first character of histchars to an unlikely used character or using the third character of the same variable to start commands you don't want history expansion applied to.

fra-san
  • 10,205
  • 2
  • 22
  • 43
  • From one of the posts you're quoting: "Double quotes don't matter -- csh doesn't let them inhibit history expansion, so bash doesn't." As if the history expansion in bash were even remotely compatible with that from csh. FWIW and FYI bangs (!s) also expand within SINGLE quotes in csh, and they also look to the current command, which means that they can be used to pass arguments to macros. alias echo2 'echo \!:* \!:*', echo! a b => a b a b. –  Mar 22 '20 at 19:02