75

What is the purpose of a shell command (part of a shell script) starting with an exclamation mark?

Concrete example:

In foo.sh:

#!/usr/bin/env bash
set -e
! docker stop foo
! docker rm -f foo
# ... other stuff

I know that without the space the exclamation mark is used for history replacements and ! <expression> according to the man page can be used to evaluate "True if expr is false". But in the example context that does not make sense to me.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
sthzg
  • 5,514
  • 2
  • 29
  • 50
  • 8
    If the return code (visible as `$?`) was 0 (success) it flips it to 1, if the return code was a failure (non-zero) it flips it to 0 (zero). Maybe these commands are expected to fail and you have `-e` set or `set -o errexit`? Whatever, this is a case where comments should have been added. – cdarke Nov 13 '17 at 10:01
  • 1
    @cdarke `set -e` ignores commands whose exit statuses are explicitly negated with `!`. – chepner Nov 13 '17 at 22:04
  • Related: [Elaboration of "!"](https://stackoverflow.com/questions/367069/how-can-i-negate-the-return-value-of-a-process/367167#367167). – Peter Mortensen Sep 19 '22 at 00:26

2 Answers2

80

TL;DR: This is just by-passing the set -e flag in the specific line where you are using it.


Adding add to hek2mgl's correct and useful answer.

You have:

set -e
! command

Bash Reference Manual → Pipelines describes:

Each command in a pipeline is executed in its own subshell. The exit status of a pipeline is the exit status of the last command in the pipeline (...). If the reserved word ‘!’ precedes the pipeline, the exit status is the logical negation of the exit status as described above. The shell waits for all commands in the pipeline to terminate before returning a value.

This means that ! preceding a command is negating the exit status of it:

$ echo 23
23
$ echo $?
0
                # But
$ ! echo 23
23
$ echo $?
1

Or:

$ echo 23 && echo "true" || echo "fail"
23
true
$ ! echo 23 && echo "true" || echo "fail"
23
fail

The exit status is useful in many ways. In your script, used together with set -e makes the script exit whenever a command returns a non-zero status.

Thus, when you have:

set -e
command1
command2

If command1 returns a non-zero status, the script will finish and won't proceed to command2.

However, there is also an interesting point to mention, described in 4.3.1 The Set Builtin:

-e

Exit immediately if a pipeline (see Pipelines), which may consist of a single simple command (see Simple Commands), a list (see Lists), or a compound command (see Compound Commands) returns a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return status is being inverted with !. If a compound command other than a subshell returns a non-zero status because a command failed while -e was being ignored, the shell does not exit. A trap on ERR, if set, is executed before the shell exits.


Taking all of these into consideration, when you have:

set -e
! command1
command2

What you are doing is to by-pass the set -e flag in the command1. Why?

  • if command1 runs properly, it will return a zero status. ! will negate it, but set -e won't trigger an exit by the because it comes from a return status inverted with !, as described above.
  • if command1 fails, it will return a non-zero status. ! will negate it, so the line will end up returning a zero status and the script will continue normally.
Community
  • 1
  • 1
fedorqui
  • 275,237
  • 103
  • 548
  • 598
  • 33
    so with `set -e`, it means **no error allowed** in the script, but with a leading **`!`**, it marks an exception meaning this command is fine to fail. – ryenus Nov 13 '17 at 13:49
  • 5
    @ryenus Not really. `!` simply negates the exit status of a command. It is just one of the many exceptions in the list of commands whose exit status `set -e` will ignore. – chepner Nov 13 '17 at 21:59
  • 6
    @chepner: I'm not sure why you say "Not really.". ryenus's comment is correct. – Keith Thompson Nov 13 '17 at 23:56
  • 1
    @ryenus Yes, I have. `!` negates the exit status of a command whether or not the `-e` option is in effect. It does not simply mark a command as an exception to the `-e` option. – chepner Nov 15 '17 at 02:37
  • 3
    @chpner, `!` alone indeed negates the command exit status, regardless `set -e` is set or not. But here the gist is about the **side effect of `!` in the context of `set -e`**, which doesn't fail the script even if the command succeeded. Without such side effect. the script would fail when the exit status of a succeeded command is negated by `!`. – ryenus Nov 15 '17 at 04:58
24

If you don't want the script to fail in both cases, error or success of the command, you can also use this alternative:

set -e
docker stop foo || true

The boolean or true makes the pipeline always have 0 as the return value.

hek2mgl
  • 152,036
  • 28
  • 249
  • 266
  • 1
    `man bash` says _If the reserved word ! precedes a pipeline, the exit status of that pipeline is the logical negation of the exit status_. Wouldn't this mean that if the command executes successfully, it would trigger an error since it negates a True? – fedorqui Nov 13 '17 at 12:16
  • Yeah, I'm not using the `!` – hek2mgl Nov 13 '17 at 12:22
  • @fedorqui I expected the same, but when I try it with a simple demo script it doesn't terminate when the command is successful. e.g. `set -e\n! echo "I am okay"\n\echo "done"` runs through. `set -e\n! give_me_an_error\n\echo "done"` also runs through (this is what is described in the answer). So, at least in my shell (zsh on macOS) it is lenient towards errors and does not interfere with successful `exit 0` commands. – sthzg Nov 13 '17 at 12:49
  • 12
    @sthzg OK I found it. From [Bash Reference Manual → 4.3.1 The Set Builtin](https://www.gnu.org/software/bash/manual/bashref.html#The-Set-Builtin) on `-e`: _The shell does not exit if (...) the command’s return status is being inverted with !_. – fedorqui Nov 13 '17 at 12:56
  • 1
    @fedorqui Good catch and good to know! I recommend to put that into an answer – hek2mgl Nov 13 '17 at 13:14
  • This answer is a bit confusing because it doesn't directly explain what the difference is between `! command` and `! command || true`. That is, it should state that the exclamation mark "instead means fail if the command succeeds". Without it, the reference to "both cases" is also confusing (it wasn't clear on my first reading that "error or success" was meant to be a parenthetical element and not part of a comma-separated list. – jamesdlin Nov 13 '17 at 13:22
  • Sorry, "both cases"? One case is "when the command fails" -- what's the other one? Could you edit to clarify? – R.M. Nov 13 '17 at 13:39
  • @jamesdlin, the exclamation mark doesn't mean "instead means fail if the command succeeds" as far as `set -e` is concerned. It does invert the resulting value of `$?`, of course. – ilkkachu Nov 13 '17 at 23:10
  • @ilkkachu I meant to suggest *adding* that remark in addition the existing remark of "don't fail the script when the command fails.". Either statement *by itself* is insufficient to explain the behavior. – jamesdlin Nov 13 '17 at 23:21
  • @jamesdlin, yes, and I would suggest _not adding_ such a remark, for the simple reason that it isn't true. – ilkkachu Nov 14 '17 at 08:31