3

One feature of Perl that I really like is its generalization of looping control keywords to any curly-brace-delimited lexical block1.

For example, one can use Perl's last directive to exit any such block, as the following toy script illustrates:

#!/usr/bin/env perl

use strict;

my $value = $ARGV[0] || 0;
my $overall_status = 'failed';
{
  $value > 0 and print "$value > 0\n" or last;
  $value > 1 and print "$value > 1\n" or last;
  $value > 2 and print "$value > 2\n" or last;
  $value > 3 and print "$value > 3\n" or last;
  $overall_status = 'ok';
}

die 'EARLY EXIT' unless $overall_status eq 'ok';

print "ok\n";

In this example, the first four statements in the curly-brace-delimited block are meant to stand in for a sequence of mandatory conditions: the failure of any of them should result in the printing of an error message (whose content is independent of which condition failed) and the termination of the script's execution.

I want to emphasize that the common response to all possible failures within the block (in this case, this response being a generic error message that does not distinguish among all the possible points of failure followed by termination) is an essential, non-negotiable aspect of this problem. In fact, I would argue that it is the aspect of the problem that makes being able to exit the code block from anywhere along it desirable.

IMPORTANT: In order to keep the example above easy to understand, the conditions are artificially simple. In practice, however, the evaluation of each condition may take up several lines of code! Please keep this in mind when you formulate your answer. The essential point of the example is that the code block can be exited from anywhere in the block.


I am looking for a good way to achieve this similar effect in a bash script.

I can't come up with anything better than this:

#!/bin/bash

overall_status=failed
value=$1
while true; do
    (( value > 0 )) && echo "$value > 0" || break;
    (( value > 1 )) && echo "$value > 1" || break;
    (( value > 2 )) && echo "$value > 2" || break;
    (( value > 3 )) && echo "$value > 3" || break;
    overall_status=ok
    break
done

if [[ "$overall_status" != ok ]]; then
    echo 'EARLY EXIT' >&2
    exit 1
fi

...which is confusing at best, because it is using a looping construct (while) where no looping is intended.

In a zsh script I can at least replace the strange-looking while loop with a single-use anonymous function2:

() {
    (( value > 0 )) && echo "$value > 0" || return 1;
    (( value > 1 )) && echo "$value > 1" || return 1;
    (( value > 2 )) && echo "$value > 2" || return 1;
    (( value > 3 )) && echo "$value > 3" || return 1;
    overall_status=ok
}

I'd still be interested in learning of other ways to implement an early exit from a code block in a zsh script.


1 It is likely that this quick, from-memory description oversimplifies somewhat...
2 In fact, with this approach, one could also dispense with the overall_status variable altogether, and test $? instead.

EDIT: I added the "IMPORTANT: ..." clarification after the Perl example.

EDIT2: Emphasized the common response to all the failures within the block is an essential aspect of the problem.

1
  • 2
    Similar to the zsh anonymous function, why don't you have a named bash function?
    – thanasisp
    Commented Sep 27, 2020 at 21:28

5 Answers 5

7

Sidestepping the question in the title, and concentrating on the particular case of an error exit, I would just put the whole error message and exit in a function and call that directly instead of breaking. That also avoids the extra block and indentation level in the main code path.

#!/bin/bash
die() {
    echo 'error: mandatory conditions not met!' >&2
    exit 1
}
value=$1
(( value > 0 )) && echo "$value > 0" || die;
(( value > 1 )) && echo "$value > 1" || die;
(( value > 2 )) && echo "$value > 2" || die;
(( value > 3 )) && echo "$value > 3" || die;

Obviously this won't work if you want to continue execution after the block with conditionals. In that case, you'd need an empty loop. A for loop with just one item might be logical here, since it naturally ends after one iteration (but you need to name the loop variable).

for __ in once; do
    echo this prints only once
    (( value > 0 )) && echo "$value > 0" || break;
    ...
done
echo continuing from here

In general, I would also suggest giving a different error message for each error case, so the user knows which requirement failed. That could be done by having die the the message as an argument. In some cases, it might also be possible to do all the checks regardless of if one fails, and only exit afterwards. This would let the user know all issues at the same time.

#!/bin/bash
errexit=
error() {
    echo "error: $1" >&2
    errexit=1   # global
}
value=$1
(( value > 0 )) && echo "$value > 0" || error 'value <= 0';
(( value > 1 )) && echo "$value > 1" || error 'value <= 1';
(( value > 2 )) && echo "$value > 2" || error 'value <= 2';
(( value > 3 )) && echo "$value > 3" || error 'value <= 3';
[ "$errexit" ] && exit 1
2
  • I like the first suggestion. As to the second one, the common error message is an essential aspect of the problem specification.
    – kjo
    Commented Sep 27, 2020 at 20:20
  • 1
    @kjo, ah, okay. Well, it should be simple to do either way.
    – ilkkachu
    Commented Sep 28, 2020 at 10:39
3

I am not aware of a way to do what you're after in Bash (other than the options that you've already identified).

If I were going to try to do what you've after, I'd probably do something like:

#!/bin/bash

overall_status=failed
value=$1

if   (( value > 0 )); then echo "value > 0"
elif (( value > 1 )); then echo "value > 1"
elif (( value > 2 )); then echo "value > 2"
elif (( value > 3 )); then echo "value > 3"
else overall_status=ok
fi

if [[ "$overall_status" != ok ]]; then
    echo 'EARLY EXIT' >&2
    exit 1
fi
3

The simple way for an early exit from a commands group, is the and list using && which breaks at the first failure, which is a short substitution for if or case statements. Replacing the dummy while loop in your example with this:

(( value > 0 )) && echo "$value > 0" &&
(( value > 1 )) && echo "$value > 1" &&
(( value > 2 )) && echo "$value > 2" &&
(( value > 3 )) && echo "$value > 3" &&
overall_status=ok

Or similar to your zsh anonymous function, why don't you have a named bash function.

#!/bin/bash

arg=$1

function f() {
    value=$1
    (( value > 0 )) && echo "$value > 0" || return 1
    (( value > 1 )) && echo "$value > 1" || return 1
    (( value > 2 )) && echo "$value > 2" || return 1
    (( value > 3 )) && echo "$value > 3" || return 1
    return 0
}

if f arg; then
    exit 0
else
    echo 'EARLY EXIT' >&2
    exit 1
fi
2

Some more options:

  • subshell and use exit to break out of it:

    (
      ... || exit # failure
      ... && exit # success
    ) || die 'subshell failed'
    
  • put your code in the condition part of a loop:

    while
      ... || break
      ... && break
    do break; done
    

    note however that the exit status of the loop will always be 0.

    With the for loop, approach, you can use ! break to exit the loop with a failure status (not with pdksh or derivatives though):

    for once in total; do
      ... || ! break
      ... && continue # same as break
    done
    

    Note that with the loop approaches, you can break out of more than one enclosing loop with break 2, break 3...

  • source a here-doc:

    . /dev/fd/3 3<< 'EOF'
    ... || return
    ... && return
    EOF
    
1

You do it the same way.

In perl, a {...} block is a loop which runs just once. So you do the same in the shell -- a loop which runs just once. And use break instead of last:

for _ in "$_"; do
    foo || break
    bar || break
    baz || break
    {
        quux
        moo
    } || break     
done

$value > 0 and print "$value > 0\n" or last;

This sucks for same reasons foo && bar || quux does in the shell. What will happen if print fails? Unlike the shell, perl has the ternary ?: operator. Use it:

$value > 0 ? print "$value > 0\n" : last;

Or use an explicit if(...){...}else{...} if you find it too terse.

Also, the #!/usr/bin/env perl is asking for gratuitous trouble. Why would you have to test your script against all the possible perl versions a user may have in their path, instead of just the /usr/bin/perl which you can determine from the distro?

This site is temporarily in read-only mode and not accepting new answers.

Not the answer you're looking for? Browse other questions tagged .