12

All of my scripts have errexit turned on; that is, I run set -o errexit. However, sometimes I want to run commands like grep, but want to continue execution of my script even if the command fails.

How do I do this? That is, how can I get the exit code of a command into a variable without killing my whole script?

I could turn errexit off, but I'd prefer not to.

Nate Rook
  • 335
  • 3
  • 10
  • 3
    Keeping `set -e` off [is really the best thing](http://mywiki.wooledge.org/BashFAQ/105). Automatic error handling is only more benefit than cost when its behavior is consistent, predictable and reliable, and `set -e` is none of those things: It varies between shells, between *releases* of the same shell, and has numerous hidden surprises: For instance, if you're writing a function, whether `errexit` takes effect on an error on any command within it depends on whether *the exit status of the function itself* is being tested. – Charles Duffy May 20 '17 at 01:54
  • 1
    @CharlesDuffy The only alternatives are the shell running off the rails after an error, ruining my servers, or adding `|| exit $?` after each command. The code maintenance overhead due to `-e` drawbacks that you describe is incomparably smaller than that. – ivan_pozdeev Jan 09 '18 at 13:44
  • @ivan_pozdeev, first, it's just `|| exit` (when not in a function context where `|| return` is desirable instead); `$?` is the default exit status. Second, `foo || exit` after each and every command is *clearly not* the right thing -- if it were, the behavior of `set -e` wouldn't have such a large (and varied, between shells) set of exceptions in its behavior, making its behavior contextually-sensitive and hard to predict. For instance, `(( ++count )) || exit` is clearly wrong since it exits when incrementing `count` from `0` to `1`, yet `set -e; (( ++count ))` does precisely that. – Charles Duffy Jan 09 '18 at 16:09
  • ...or, rather, does precisely that *in some versions of bash but not others*, which further reinforces my point: If you can't rely on the behavior on `set -e` to be consistent, you're better off putting in explicit `|| die "error message here"` calls (`die` being a conventional name for a function that prints an error before exiting), such that you *can* ensure consistency. – Charles Duffy Jan 09 '18 at 16:14

3 Answers3

13

Your errexit will only cause the script to terminate if the command that fails is "untested". Per man sh on FreeBSD:

         Exit immediately if any untested command fails in non-interactive
         mode.  The exit status of a command is considered to be explic-
         itly tested if the command is part of the list used to control an
         if, elif, while, or until; if the command is the left hand oper-
         and of an ``&&'' or ``||'' operator; or if the command is a pipe-
         line preceded by the ! keyword.

So .. if you were thinking of using a construct like this:

grep -q something /path/to/somefile
retval=$?
if [ $retval -eq 0 ]; then
  do_something  # found
else
  do_something_else  # not found
fi

you should instead use a construct like this:

if grep -q something /path/to/somefile; then
  do_something  # found
else
  do_something_else  # not found
fi

The existence of the if keyword makes the grep command tested, thus unaffected by errexit. And this way takes less typing.

Of course, if you REALLY need the exit value in a variable, there's nothing stopping you from using $?:

if grep -q something /path/to/somefile; then
  do_something  # found
else
  unnecessary=$?
  do_something $unnecessary  # not found
fi
ghoti
  • 45,319
  • 8
  • 65
  • 104
  • 3
    Great answer. Note the following "trap" expanding on the latter example: `if ! false; then e=$?; echo $e; else echo ok; fi` prints `0` as the `!` operator introduces another evaluation, while `if false; then echo ok; else e=$?; echo $e; fi`prints `1` as expected. – Alexandre Gattiker Dec 14 '19 at 19:30
3

Here's a way to achieve this: you can "turn off" set -o errexit for some lines of code, and then turn it on again when you decide:

set +e  #disables set -o errexit
grep -i something file.txt
rc="$?" #capturing the return code for last command
set -e #reenables set -o errexit

Another option would be the following:

grep -i something file.txt || rc="$?"

That would allow you to capture the return code on the variable rc, without interrupting your script. You could even extend this last option to capture and process the return code on the same line without risking to trigger an exit:

grep -i something file.txt || rc="$?" && echo rc="$?" > somefile.txt && command || :

The last bit ||: will guarantee that the line above always returns a return code = 0 (true).

Jamil Said
  • 2,033
  • 3
  • 15
  • 18
  • In fact, it's the use of the or bars (`||`) that causes this to work, not the echo of the variable to a text file. The exit status of a command is considered to be explicitly tested if it's on the left hand side of `||`. See [my answer](http://stackoverflow.com/a/44081089/1072112) for the full text from the man page. – ghoti May 20 '17 at 01:50
  • Yes, I did not state otherwise (that the use of "or" `||` is what makes it "work"). However, my comment above is correct, because if the last command fails (in my example `echo "$rc" > file.txt`), that would still trigger the exit. – Jamil Said May 20 '17 at 01:53
  • I think the only problem with your comment is that it doesn't make clear that the command appearing after the semicolon isn't actually part of the same command line containing the `||`, it's an entirely different command regardless of its "position" to the right of or below the line in question. If writing to `file.txt` generates a permission error, that's unrelated to the issue in the question. – ghoti May 20 '17 at 12:24
  • 1
    @ghoti OK, I see your point now. I changed my answer for sake of clarity, and also added some improvements. – Jamil Said May 20 '17 at 20:45
3

The most compact and least repetitive form I can think of:

<command> && rc=$? || rc=$?

A form without variable name repetition -- rc=$(<command> && echo $? || echo $?) -- has an expression rvalue but would also capture the stdout of <command>1. So, it's only safe if you "know" that <command> has no normal output.

Using the a && b || c construct is safe here because rc=$? and $(echo $?) can never fail.


1Sure, you can work around this by fiddling with file descriptors, but that'd make the construct long-winded and inconvenient as a standard

ivan_pozdeev
  • 33,874
  • 19
  • 107
  • 152