2

I am trying to catch the error code of a command and assign the output of a command to a variable in an if statement. My code looks like this:

if ! ret=$(<some_command>); then
    rc=$?
    echo "<some_command> returned with exit code: $rc"
    echo "$ret"
fi

Here I want to assign the output of a command to ret and assign only the error code of <some_command> to rc. Since this script have set -e, I cannot just run the command standalone. As soon as the command fails, the script exits, but I do not want to exit the script, rather get the error code as well as the error into an variable. How to get this working?

  • 2
    Don't use `set -e` since you don't want the behavior it provides. In general `set -e` is best avoided as you usually want to do **something** when a failure occurs in a script, even if it's just print an error message, and `set -e`s behavior is convoluted and changes between bash versions. See http://mywiki.wooledge.org/BashFAQ/105 for more info. – Ed Morton Jul 19 '23 at 12:01
  • If you don't want to exit the script on failure, then yeah, don't use `-e`. If you want to retrieve the status code in order to log it before exit, you could use traps, like [in this question](https://stackoverflow.com/questions/35800082/how-to-trap-err-when-using-set-e-in-bash) – Aserre Jul 19 '23 at 12:23

4 Answers4

3

set -e is a nonissue here: its behavior is disabled inside if and in any other "checked" context; the ! is what's causing your exit status to be suppressed.

Take away the !, and move the error handling into an else clause:

if ret=$(some_command); then :; else
    rc=$?
    echo "<some_command> returned with exit code: $rc"
    echo "$ret"
fi

...or, perhaps more idiomatically (as || branching on a command's exit status also marks it as checked and suppresses set -e):

ret=$(some_command) || {
  rc=$?
  echo "<some_command> returned with exit code: $rc"
  echo "$ret"
}
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441
2

The ! command just negates the current exit code. For instance, after !false, the value of $? would be 0. In your case, rc will always be 0.

To retrieve the exit code for later processing, you have to collect it after the command has been executed. If you really insist in using set -e, which IMO is a bad idea, you can do it like this:

rc=0
ret=$(some_command) || rc=$?
if ((rc > 0))
then
  echo "<some_command> returned with exit code: $rc"
  echo "$ret"
fi
user1934428
  • 19,864
  • 7
  • 42
  • 87
1

I agree with the consensus here - generally just don't use set -e.
Consider maybe a trap instead, like this.

Second, most bash checks against a condition will "consume" the "exception". I'm sure there's better terminology, but test it for yourself; if you check for a fail immediately, things like set -e or a trap won't usually see the fail, and you can handle it explicitly.

user1934428's solution does this with a double-pipe to explicitly check the exit status and set the value on a fail.

Be careful - even testing only for success is likely to have this effect. See below.

$: cat tst
#! bash
set -e
cmd=( bogus command )
# check fail only (#1), success only (#2), both (#3)
"${cmd[@]}" || echo "yep, failed #1"
"${cmd[@]}" && echo "worked fine! #2" # doesn't print
"${cmd[@]}" && echo "worked fine!" || echo "oops! #3"
if "${cmd[@]}"  # the if is a test, "catches" the fail
then echo "won't get here"
else rc=$?
     echo "<${cmd[@]}> returned with exit code: $rc - #4"
fi
# running it without checking here, #5
"${cmd[@]}" # uncaught, set -e kills the script here
echo "This doesn't print! #5"

The cmd array is just an easy way to make sure the echo doesn't have to be separately maintained.

Running it looks like this:

$: ./tst
./tst: line 5: bogus: command not found
yep, failed #1
./tst: line 6: bogus: command not found
./tst: line 7: bogus: command not found
oops! #3
./tst: line 8: bogus: command not found
<bogus command> returned with exit code: 127 - #4
./tst: line 14: bogus: command not found

Hope that helps.

addendum

We totally did NOT address the output capture, but you seem to have that; and I completely agree that it's probably better to avoid the "not" (!).

Paul Hodges
  • 13,382
  • 1
  • 17
  • 36
0

Try this Shellcheck-clean code:

#! /bin/bash -p

if ! ret=$(some_command); then
    rc=${PIPESTATUS[0]}
    printf '<some_command> returned with exit code: %d\n' "$rc"
    printf '%s\n' "$ret"
fi
  • Using the built-in PIPESTATUS array in Bash is a common way to get the return status of a negated command.
  • See the accepted, and excellent, answer to Why is printf better than echo? for an explanation of why I replaced echo with printf. echo "$var" doesn't work in general.
pjh
  • 6,388
  • 2
  • 16
  • 17