LiMar
LiMar

Reputation: 2962

bash one-line conditional fails when using set -e

I started using set -e in my bash scripts, and discovered that short form of conditional expression breaks the script execution.

For example the following line should check that $var is not empty:

[ -z "$var" ] && die "result is empty"

But causes silent exit from script when $var has non-zero length.

I used this form of conditional expression in many places...

What should I do to make it run correctly? Rewrite everything with "if" construction (which would be ugly)? Or abandon "set -e"?

Edit: Everybody is asking for the code. Here is full [non]working example:

#!/bin/bash
set -e

function check_me()
{
    ws="smth"
    [ -z "$ws" ] && echo " fail" && exit 1
}

echo "checking wrong thing"
check_me
echo "check finished"

I'd expect it to print both echoes before and after function call. But it silently fails in the check_me function. Output is:

checking wrong thing

Upvotes: 3

Views: 3678

Answers (4)

PJ Eby
PJ Eby

Reputation: 8946

What the bash help isn't very clear on is that only the last statement in an && or || chain is subject to causing an exit under set -e. foo && bar will exit if bar returns false, but not if foo returns false.

So your script should work... but it doesn't. Why?

It's not because of the failed -z test. It's because that failure makes the function return a non-zero status:

#!/bin/bash
set -e

function check_me()
{
    ws="smth"
    [ -z "$ws" ] && echo " fail" && exit 1
    # The line above fails, setting $? to 1
    # The function now returns, returning 1!
}

echo "checking wrong thing"
check_me   # function returns 1, causing exit here
echo "check finished"

So there are multiple ways to fix this. You could add ||true to the conditional inside the function, or to the line that calls check_me. But as others have pointed out, using ||true has its own problems.

In this specific scenario, where the desired postcondition of check_me is "either this thing is valid or the script has exited", the straightforward thing to do is to write it like that, i.e. [[ -n "$ws" ]] || die "whatever".

But using && conditions will actually work fine with set -e in general, as long as you don't use such a conditional as the last thing in a function. You need to add an explicit true or return 0 or even : as a statement following such a conditional, unless you intend the function to return false when the condition fails.

Upvotes: 1

Peter
Peter

Reputation: 1013

I'm afraid you will have to rewrite everything so no false statements occur.

The definition of set -e is clear:

-e Exit immediately if a simple command (see SHELL GRAMMAR above) exits with 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 a && or || list, or if the command's return value is being inverted via !. A trap on ERR, if set, is executed before the shell exits.

You are using the "optimization" system of Bash: because a false statement will cause an AND (&&) statement never to be true, bash knows it doesn't have to execute the second part of the line. However, this is a clever "abuse" of the system, not intended behaviour and therefore incompatible with set -e. You will have to rewrite everything so it is using proper ifs.

Upvotes: 6

l0b0
l0b0

Reputation: 58828

Use

[ -n "$var" ] || die "result is empty"

This way, the return value of the entire statement is true if $var is non-empty, so the ERR trap is not triggered.

Upvotes: 9

ams
ams

Reputation: 25579

You should write your script such that no command ever exits with non-zero status.

In your command [ -z "$var" ] can be true, in which case you call die, or false in which case -e does it's thing.

Either write it with if, as you say, or use something like this:

[ -z "$var" ] && die "result is empty" || true

I'd recommend if though.

Upvotes: 2

Related Questions