13

I am looking for a way to have fallthrough happen based on an if condition within a case condition in bash. For example:

input="foo"
VAR="1"

case $input in
foo)
    if [ $VAR = "1" ]; then

        # perform fallthrough

    else

        # do not perform fallthrough

    fi
;;
*)
    echo "fallthrough worked!"
;;
esac

In the above code, if the variable VAR is 1, I would like to have the case condition perform fallthrough.

Smashgen
  • 383
  • 1
  • 5
  • 8
  • 1
    Small question: Are you trying to jump from `if [ $VAR -eq 1 ]; then` part of the code to whatever is in `*)` ? Because that's entirely different from what fallthrough is called, thus making your question phrasing just slightly misleading. – Sergiy Kolodyazhnyy May 28 '18 at 20:24

8 Answers8

9

You can't. The way to have a case fall through is to replace the ;; separator with ;& (or ;;&). And it's a syntax error to put that inside an if.

You could write the whole logic out as a regular conditional:

if [ "$input" != "foo" ] || [ "$VAR" = 1 ]; then
    one branch ...
else   # $input = "foo" && $VAR != 1
    another branch...
fi
ilkkachu
  • 133,243
  • 15
  • 236
  • 397
8

The following script turns your test "inside out" in the sense that we test $var first and then perform the fallthrough (using ;& in a case) depending on $input.

We do this because the question of whether or not to "perform the fallthrough" is really only dependent on $input if $var is 1. If it's any other value, the question of whether to do the fallthrough does not even have to be asked.

#/bin/bash

input='foo'
var='1'

case $var in
    1)
        case $input in
            foo)
                echo 'perform fallthrough'
                ;&
            *)
                echo 'fallthough worked'
        esac
        ;;
    *)
        echo 'what fallthrough?'
esac

Or, without case:

if [ "$var" -eq 1 ]; then
    if [ "$input" = 'foo' ]; then
        echo 'perform fallthrough'
    fi
    echo 'fallthough worked'
else
    echo 'what fallthrough?'
fi
Barmar
  • 9,648
  • 1
  • 19
  • 28
Kusalananda
  • 320,670
  • 36
  • 633
  • 936
  • I think you nailed what OP actually wanted, which seems to be jumping from their original if statement to whatever in `*)`. Already have my +1. – Sergiy Kolodyazhnyy May 28 '18 at 20:26
7

I'd suggest restructuring your logic: put the "fallthrough" code into a function instead:

fallthrough() { echo 'fallthrough worked!'; }

for input in foo bar; do
    for var in 1 2; do
        echo "$input $var"
        case $input in
            foo)
                if (( var == 1 )); then
                    echo "falling through"
                    fallthrough
                else
                    echo "not falling through"
                fi
            ;;
            *) fallthrough;;
        esac
    done
done

outputs

foo 1
falling through
fallthrough worked!
foo 2
not falling through
bar 1
fallthrough worked!
bar 2
fallthrough worked!
glenn jackman
  • 84,176
  • 15
  • 116
  • 168
5

Not something that I would do, but you could achieve something approaching with:

shopt -s extglob # for !(*)
default='*'
case $input in
  (foo)
    if [ "$VAR" = 1 ]; then
      echo going for fallthrough
    else
      echo disabling fallthrough
      default='!(*)'
    fi ;;&

  ($default)
    echo fallthrough
esac
Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501
2

Test both variables at once (bash 4.0-alpha+):

#!/bin/bash
while (($#>1)); do
    input=$1    VAR=$2
    echo "input=${input} VAR=${VAR}"; shift 2

    if [ "$VAR" = 1 ]; then new=1; else new=0; fi

    case $input$new in
    foo0)   echo "do not perform fallthrough"   ;;
    foo*)   echo "perform fallthrough"          ;&
    *)      echo "fallthrough worked!"          ;;
    esac

    echo
done

On testing:

$ ./script foo 0   foo 1   bar baz
input=foo VAR=0
do not perform fallthrough

input=foo VAR=1
perform fallthrough
fallthrough worked!

input=bar VAR=baz
fallthrough worked!

Clean and simple.

Understand that the tested value ($new) must have only two possible values, that is why the if clause is there, to transform VAR to a Boolean value. If VAR may be made to be a Boolean, then test for 0 (not 1) in the case and remove the if.

1

You can make the fallthrough default but place a condition that the code only executes only if the condition is met

#!/bin/bash

input='foo'
var='1'

case $input in
foo)
        echo "Do fall through"
;& #always fall through
*)
        if [ $var = "1" ] #execute only if condition matches
        then
        echo "fallthrough took place"
        fi
esac

But as ilkkachu suggested you can also use conditions rather than switch.

yashC
  • 185
  • 8
1

If you don't mind someone complaining about they don't understand your code, you could simply switch the order of the two conditionals:

input="foo"
VAR="1"

if 
    case $input in
    foo)
        [ $VAR = "1" ]
    ;;
    esac
then
    echo "fallthrough worked!"
fi

Or:

input="foo"
VAR="1"

case $input in
foo)
    [ $VAR = "1" ]
;;
esac &&
    echo "fallthrough worked!"

Simple and clear (at least to me). case doesn't support fallthrough itself. But you can replace *) with && after esac to make it respect to the return values of other branches.

user23013
  • 1,022
  • 1
  • 8
  • 18
-3

New ;& and ;;& operators were introduced in Bash 4.0 and although they both might be useful in similar situations I think they are of no use in your case. This is what man bash says about these operators:

If the ;; operator is used, no subsequent matches are attempted after the first pattern match. Using ;& in place of ;; causes execution to continue with the list associated with the next set of patterns. Using ;;& in place of ;; causes the shell to test the next pattern list in the statement, if any, and execute any associated list on a successful match.

In other words, ;& is a fall through and as we know it from C and ;;& makes bash check remaining cases instead of returning from case block entirely. You can find a nice example of ;;& in action here: https://stackoverflow.com/a/24544780/3691891.

That being said, neither ;& nor ;;& could be used in your script because both of them would go to *) that would be always run.

The following script works and does what you want without re-arranging the logic but consider it only as an example and never rely on it, it's too fragile. I've taken the idea from here:

#!/usr/bin/env bash

function jumpto
{
    label=$1
    cmd=$(sed -n "/$label:/{:a;n;p;ba};" "$0" | grep -v ':$')
    cmd=$(echo "$cmd" | sed 's,;;,,')
    cmd=$(echo "$cmd" | sed 's,esac,,')
    eval "$cmd"
}

input="foo"
VAR="1"

case $input in
foo)
    if [ $VAR = "1" ]; then

        printf "perform fallthrough\n"
        jumpto ft
    else
        printf "do not perform fallthrough\n"

    fi
;;
*)
    ft:
    echo "fallthrough worked!"
;;
esac
Arkadiusz Drabczyk
  • 25,049
  • 5
  • 53
  • 68
  • 1
    This may work in a toy example but it's near-incomprehensible and wouldn't work in a larger script. The way you simulate goto is extremely fragile, and to say that it really simulates goto is an exaggeration. The code returns from the `jumpto` function and executes whatever comes after it. The fact that this is equivalent to continuing executing after the block between `ft:` and `;;` is a coincidence because `jumpto` is the last command in the same complex command as the jumped-to block. – Gilles 'SO- stop being evil' May 28 '18 at 20:59
  • 1
    OK, I clearly emphasized that in my answer. – Arkadiusz Drabczyk May 28 '18 at 21:01