Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to escape history expansion exclamation mark ! inside a double quoted string?

EDIT: the command substitution is not necessary for the surprising behavior, although it is the most common use case. The same question applies to just echo "'!b'"

b=a

# Enable history substitution.
# This option is on by default on interactive shells.
set -H

echo '!b'
# Output: '!b'
# OK. Escaped by single quotes.

echo $(echo '!b')
# Output: '!b'
# OK. Escaped by single quotes.

echo "$(echo '$b')"
# Output: '$b'
# OK. Escaped by single quotes.

echo "$(echo '!b')"
# Output: history expands
# BAD!! WHY??
  • In the last example, what is the best way to escape the !?
  • Why was it not escaped even if I used single quotes, while echo "$(echo '$b')" was? What is the difference between ! and $?
  • Why was does echo $(echo '!b') (no quotes) work? (pointed by @MBlanc).

I would prefer to do this without:

  • set +H as I would need set -H afterwards to maintain shell state

  • backslash escapes, because I need one for every ! and it has to be outside the quotes:

    echo "$(echo '\!a')"
    # '\!a'.
    # No good.
    
    echo "$(echo 'a '\!' b '\!' c')"
    # a ! b ! c
    # Good, but verbose.
    
  • echo $(echo '!b') (no quotes), because the command could return spaces.

Version:

bash --version | head -n1
# GNU bash, version 4.2.25(1)-release (i686-pc-linux-gnu)

People also ask

How to use exclamation mark in bash?

In shell scripts, we can use it to specify bash as an interpreter: $ cat welcome.sh #!/usr/bin/bash echo "Welcome !!!" Similarly, we can use it in a directive in a Python script to specify the python executable as the interpreter: $ cat welcome.py #!/usr/bin/python print("Welcome !!!")

How to write exclamation mark in linux?

The exclamation mark is part of history expansion in bash. To use it you need it enclosed in single quotes (eg: 'http://example.org/!132' ). You might try to directly escape it with a backslash ( \ ) before the character (eg: "http://example.org/\!132" ).

How do you escape an exclamation point?

In addition to using single quotes for exclamations, in most shells you can also use a backslash \ to escape it.


4 Answers

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
like image 110
chepner Avatar answered Oct 07 '22 07:10

chepner


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.

like image 31
jthill Avatar answered Oct 07 '22 07:10

jthill


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.

like image 36
John B Avatar answered Oct 07 '22 05:10

John B


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.

like image 2
Bruno Bronosky Avatar answered Oct 07 '22 07:10

Bruno Bronosky