69

I tried to create a script by echo'ing the contents into a file, instead of opening it with a editor

echo -e "#!/bin/bash \n /usr/bin/command args"  > .scripts/command

The output:

bash: !/bin/bash: event not found

I've isolated this strange behavior to the bang.

$ echo !
!  

$ echo "!"
bash: !: event not found

$ echo \#!
#!

$ echo \#!/bin/bash
bash: !/bin/bash: event not found
  • Why is bang causing this?
  • What are these "events" that bash refers to?
  • How do I get past this problem and print "#!/bin/bash" to the screen or my file?
Stefan
  • 25,300

6 Answers6

74

Try using single quotes.

echo -e '#!/bin/bash \n /usr/bin/command args'  > .scripts/command

echo '#!'

echo '#!/bin/bash'

The problem is occurring because bash is searching its history for !/bin/bash. Using single quotes escapes this behaviour.

Richm
  • 3,872
  • 5
    It also disables string interpolation :( – Hubro Jan 23 '15 at 07:53
  • That's the point! You can add other arguments to the same echo command using double quotes and get interpolation on those parts. – Richm Jan 23 '15 at 11:32
  • So, in echo, is it always better to using ' instead of "? – Zen Feb 07 '15 at 05:40
  • 3
    you can also use C style string in bash with $''. For example echo $'1!\n2!\n3!\n' prints each number followed by a bang and a newline. – spelufo Feb 19 '15 at 14:34
  • 2
    Putting bang in single quotes didn’t help me when entering a multi-line string: bash—single quotes do not escape bang (for real) – binki Mar 07 '16 at 17:03
  • You can use variables mid string with single quotes if you wrap the variable with '"$var"' ... single quote - double quote - variable - double quote - single quote. http://askubuntu.com/a/76842/334235 – phyatt Nov 07 '16 at 18:12
  • If you're gonna need double quotes afterwards (for interpolation), you can still use single-quotes just for the !: echo '#!'"/bin/bash\nMore $things" – ejoubaud Dec 08 '16 at 13:08
  • @binki I get the same behavior but I think that's a bug with multiline string handling, using '!' on a single line doesn't have that problem – kbolino Apr 09 '18 at 19:22
  • @phyatt that doesn't work, you have to terminate the single quotes to use variable interpolation – kbolino Apr 09 '18 at 19:22
  • 1
    @kbolino You are right. So the full example string would be: echo '0123'"$var"'456', note two pairs of single quotes and one pair of double quotes. – phyatt Apr 09 '18 at 19:26
21

As Richm said, bash is trying to do a history match. Another way to avoid it is to just escape the bang with a \:

$ echo \#\!/bin/bash
#!/bin/bash

Though beware that inside double quotes, the \ is not removed:

$ echo "\!"
\!
Michael Mrozek
  • 93,103
  • 40
  • 240
  • 233
  • 13
    In bash, "\!" actually expands to \!, not !, supposedly for POSIX compliance. – Mikel Apr 07 '11 at 23:09
  • @Mikel Sigh. So many differences between zsh and bash – Michael Mrozek Apr 08 '11 at 03:55
  • This works even when entering a multiline string. Remembering to 1. be outside of a string when entering the bang and 2. use this method (backslash) seems to work with the least surprise in most scenarios. – binki Mar 07 '16 at 17:09
  • 1
    Would be useful to note that bash doesn’t do history searches when executing a script, so no need to do anything special for it there. – binki Mar 07 '16 at 17:09
  • Isn't there any syntax to simply escape ! inside double quotes with no magic behavior? This is nuts... – Lassi Sep 05 '19 at 15:55
15

! starts a history substitution (an “event” is a line in the command history); for example !ls expands to the last command line containing ls, and !?foo expands to the last command line containing foo. You can also extract specific words (e.g. !!:1 refers to the first word of the previous command) and more; see the manual for details.

This feature was invented to quickly recall previous commands in the days when command line edition was primitive. With modern shells (at least bash and zsh) and copy-and-paste, history expansion is not as useful as it used to be — it's still helpful, but you can get by without it.

You can change which character triggers history substitution by setting the histchars variable; if you rarely use history substitution, you can set e.g. histchars='¡^' so that ¡ triggers history expansion instead of !. You can even turn off the feature altogether with set +o histexpand.

7

To be able to disable history expansion on a particular command line, you can use space as the 3rd character of $histchars:

histchars='!^ '

Then, if you enter your command with a leading space, history expansion will not be performed.

bash-4.3$ echo "#!/bin/bash"
bash: !/bin/bash: event not found
bash-4.3$  echo "#!/bin/bash"
#!/bin/bash

Note however that leading spaces are also used when $HISTCONTROL contains ignorespace as a way to tell bash not to record a command line in the history.

If you want both features indenpendantly, you'll need to choose another character as the 3rd character of $histchars. You want one that doesn't affect the way your command is interpreted. A few options:

  • using backslash (\): \echo foo works but that has the side effect of disabling aliases or keywords.
  • TAB: to insert it in first position, you need to press Ctrl+VTab though.
  • if you don't mind typing two keys, you can pick any character that normally doesn't appear in first position (%, @, ?, pick your own) and make an empty alias for it:

    histchars='!^%'
    alias %=
    

    Then enter that character, space and your command:

    bash-4.3$ % echo !!
    !!
    

(you won't be able not to record a command where history substitution has been disabled though. Also note that the default 3rd character of $histchars is # so that history expansion is not done in comments. If you change it, and you enter comments at the prompt, you should be aware of the fact that ! sequences may be expanded there).

2

The proposed solutions don't work in, e.g., the following example:

$ bash -c "echo 'hello World!'"
-bash: !'": event not found
$

In this case the bang can be printed using its octal ASCII code:

$ bash -c "echo -e 'hello World\0041'"
hello World!
$
Kevin
  • 40,767
Atti
  • 37
  • 1
  • 2
    In your first command, the ! is between double quotes, where as other answers indicate it retains its history expansion meaning. You could use bash -c 'echo '\''hello World!'\' or bash -c "echo 'hello World"\!"'". – Gilles 'SO- stop being evil' Dec 26 '10 at 11:18
  • Without the -i parameter, the environment may be altered (like your ~/.profile doesn't run). However, doing -i means any output from your .profile will be captured in the output of the command you actually want to run. – Brent Apr 06 '15 at 21:18
2

A simple solution not yet mentioned is to use a variable:

$ var='!'
$ echo -e "#${var}/bin/bash \n /usr/bin/command args"  > .scripts/command

Printing a bang! is not a problem when the string is unquoted or is single quoted or inside a C-String

$ echo hi\!pal 'hi!pal' $'hi!pal'
hi!pal 'hi!pal' $'hi!pal'

Nor it is a problem when histexpand is disabled ( either shopt -ou histexpand or set +o histexpand or simply set +H. It is also posible to change hisexpand characters to make it easier to print a bang!.

But the portable simplest solution is: use a var

$ var='!'
$ echo "hi${var}pal"
hi!pal