87

In popular imperative languages, switch statements generally "fall through" to the next level once a case statement has been matched.

Example:

int a = 2;
switch(a)
{
   case 1:
      print "quick ";
   case 2: 
      print "brown ";
   case 3: 
      print "fox ";
      break;
   case 4:
      print "jumped ";
}

would print "brown fox".

However the same code in bash

A=2
case $A in
2)
  echo "QUICK"
  ;&
2)
  echo "BROWN"
  ;&
3)
  echo "FOX"
  ;&
4)
  echo "JUMPED"
  ;&
esac

only prints "BROWN"

How do I make the case statement in bash "fall through" to the remaining conditions like the first example?

(edit: Bash version 3.2.25, the ;& statement (from wiki) results in a syntax error)

running:

test.sh:

#!/bin/bash
A=2
case $A in
1)
  echo "QUICK"
  ;&
2)
  echo "BROWN"
  ;&
3)
  echo "FOX"
  ;&
esac

Gives:

./test.sh: line 6: syntax error near unexpected token ;' ./test.sh:
line 6:
;&'

Resorath
  • 1,602
  • 1
  • 12
  • 31
  • 2
    Related (but not duplicate) question: [Switch case with fallthrough?](http://stackoverflow.com/q/5562253) (regarding the `|` operator for separating multiple patterns in a `case` expression). – TachyonVortex Jul 02 '15 at 09:41

6 Answers6

70

Try this:

case $VAR in
normal)
    echo "This doesn't do fallthrough"
    ;;
fallthrough)
    echo -n "This does "
    ;&
somethingelse)
    echo "fall-through"
    ;;
esac
dimo414
  • 47,227
  • 18
  • 148
  • 244
René Steetskamp
  • 1,053
  • 9
  • 12
57

The ;& and ;;& operators were introduced in bash 4.0, so if you want to stick with a five year old version of bash, you'll either have to repeat code, or use ifs.

if (( a == 1)); then echo quick; fi
if (( a > 0 && a <= 2)); then echo brown; fi 
if (( a > 0 && a <= 3)); then echo fox; fi
if (( a == 4)); then echo jumped; fi

or find some other way to achieve the actual goal.

(On a side note, don't use all uppercase variable names. You risk overwriting special shell variables or environment variables.)

Community
  • 1
  • 1
geirha
  • 5,801
  • 1
  • 30
  • 35
  • Wow! Thank you, I see now my 'new' MacBookPro 2012 has a very old 2007 Bash: GNU bash, version 3.2.48(1)-release (x86_64-apple-darwin12) – AnneTheAgile Dec 07 '13 at 18:07
  • 8
    @AnneTheAgile, yes. bash 3.2 is GPLv2, bash 4.0 (and newer) is GPLv3, and Apple "doesn't like" GPLv3 (http://en.wikipedia.org/wiki/GPL_v3#Legal_Barrier_to_App_Stores). Though you can easily get a recent bash with homebrew or macports or similar, of course. – geirha Dec 09 '13 at 10:02
  • @geirha: Oh wow. googling for this question, and read this answer and your comment. As a non-macOS user, question, are things any different ~four years on? – i336_ May 29 '17 at 14:50
  • @i336_ nope, MacOs is still shipping with bash 3.2 – geirha May 29 '17 at 18:18
  • Uppercase for globals is a good style to follow, then use lowercases for `local`s. – Will Jun 09 '17 at 01:21
  • 2
    @Will not in bash and sh, since shell variables share the namespace with environment variables and special shell variables. – geirha Jun 10 '17 at 05:32
  • Well, you just need to name variables descriptively and avoid namespace conflicts. – Will Jun 19 '17 at 04:04
  • Pay no attention to the version snobs: a lot of new unix people think newer/shinier is always better (and obviously have never flown or had surgery), and especially may not understand that enterprise products stay on branches derived from releases and don't get feature updates. – user2066657 Aug 13 '18 at 20:40
  • 2
    @Will, part of "avoid namespace conflicts" is staying out of the namespace that POSIX specifies for use for variables meaningful to the operating system and shell. That namespace is defined by the standard as the set of all-caps names; see https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html, keeping in mind that environment and shell variables share a namespace. – Charles Duffy Jul 24 '19 at 20:04
  • @MestreLion "12 years old" and it's still working well. Hate to toss it just because it's stale like a michelangelo . But, I work in Enterprise, so it's different. – user2066657 Apr 21 '21 at 00:44
  • 1
    @user2066657: 12 is for Bash 4, **15** for 3.2. But to the point: the reason Apple blocks 4.0 is **not** because it's less "enterprisey" than 3.2, it's because of GPL3. It has nothing to do with stability, maturity or security. It's _not_ a technical decision, please stop implying so. – MestreLion Apr 21 '21 at 07:10
  • @user2066657: on the contrary: by adopting legacy software you transfer all the maintenance burden to yourself. You miss all security patches, all CVE reports, you have to do everything yourself. True, Apple is big enough to have a dedicated Bash team, but it's still a huge (duplicated) effort for no technical gain. And by enforcing a "no-GPL3" policy you alienate your users from all the benefits of a modern shell out of the box. – MestreLion Apr 21 '21 at 07:26
  • @MestreLion - You sound confused. Of course one uses software supported by an enterprise where one specifically does not take on the support burden. That's the entire reason for staying with established and well-tested versions: because that's what the enterprise distro vendors do to ease their version churn. It was that way when I worked for a distro and it's that way with (eg RedHat) now. You do know that's what they do and why they do it .. right? – user2066657 Apr 27 '21 at 05:13
12

Using ;& is not very portable, as it requires bash (not ash, dash, or any other minimal sh) and it requires at least bash 4.0 or newer (not available on all systems, e.g. macOS 10.14.6 still only offers bash 3.2.57).

A work around that I consider much nicer to read than a lot of if's is loop and modify the case var:

#!/bin/sh

A=2
A_BAK=$A
while [ -n "$A" ]; do
    case $A in
        1)
            echo "QUICK"
            A=2
            ;;

        2)
            echo "BROWN"
            A=3
            ;;

        3)
            echo "FOX"
            A=4
            ;;

        4)
            echo "JUMPED"
            A=""
            ;;
    esac
done
A=$A_BAK

Here's a proof of concept: https://www.onlinegdb.com/0ngLPXXn8

Mecki
  • 125,244
  • 33
  • 244
  • 253
  • 2
    "real `sh`" is what, pre-POSIX (aka 1970s-era-syntax) Bourne? – Charles Duffy Jul 24 '19 at 20:06
  • 4
    @CharlesDuffy `/bin/sh` is a Bourne shell, the common base for BASH, DASH, ASH and also for the POSIX standard. Each of these shells support additional features, some unique to the shell but they are all SH compatible, unlike some other shells (csh and tcsh are not, zsh is only partially) – Mecki Jul 27 '19 at 15:34
  • 5
    Bourne is a shell from the 1970s. POSIX sh is a specification from 1991. `/bin/sh` on modern systems is POSIX, **not** Bourne. To pick an easy-to-test-for difference, `echo hello ^ cat` will emit `hello` on Bourne, because `^` is a pipe character there; whereas in a POSIX-compliant shell, it emits `hello ^ cat` as output, because the `^` is parsed as an argument to `echo`. – Charles Duffy Jul 27 '19 at 15:35
  • ...if you're looking for a Bourne implementation you can build and run today, [the Heirloom project maintains one](http://heirloom.sourceforge.net/sh.html) (based on the same codebase as a non-POSIX-compliant `/bin/sh` that SunOS shipped into the early 2000s). `ash` and `dash`, by contrast, are POSIX-compliant in all the places where POSIX and original Bourne differ. – Charles Duffy Jul 27 '19 at 15:37
  • 2
    (Incidentally, the `^`-as-a-pipe-character difference is the one that GNU autoconf uses to distinguish whether it's on Bourne or POSIX sh; that said, the POSIX spec generally took a lot of inspiration from early ksh, and thus codified behaviors that were originally ksh extensions). – Charles Duffy Jul 27 '19 at 15:41
  • @CharlesDuffy The Bourne shell was written 1977 and replaced the PWB shell 1979 in UNIX Version 7. The PWB shell was an extension of the Thompson shell and the Thompson shell in Version 4 already introduced `|` to replace `^` in Verson 4, so something about your facts is wrong here as the Bourne shell was written to be compatible to the Thompson shell, so something about your facts is wrong here. https://en.wikipedia.org/wiki/Thompson_shell - https://en.wikipedia.org/wiki/Bourne_shell – Mecki Jul 27 '19 at 15:48
  • In SunOS and Heirloom Bourne implementations (the recent surviving variants), *both* `|` and `^` are accepted as pipe characters, so whatever documentation is telling you that Bourne doesn't accept `^` as a pipe is simply wrong. If you want to test it yourself, log into the freenode IRC network, and `/msg evalbot b# echo hello ^ cat`, compared to `/msg evalbot # echo hello ^ cat`. – Charles Duffy Jul 27 '19 at 15:53
  • 4
    Definitely valid answer. Many embedded machines ship with ash and even sh in ubuntu is dash. For those shells this answer is valid. Have no idea why this is downvoted and why are we arguing whether non bash, sh is relevant...It is even quite elegant and idiomatic – Paulo Neves Dec 10 '19 at 16:16
  • 1
    This response doesn't answer OP's question about how to achieve fall-through. – Cameron Hudson Dec 20 '21 at 20:27
  • @CameronHudson It does fall through. I jump to 2 and it falls through to 3 and 4. I could also jump to 3 and it would just fall through to 4. And if I jump to 1, it falls through all cases. `A=""` simulates a break, so it won't fall through beyond that point. If I add two more cases below, 5 and 6, I can make 5 fall through to 6. Here's the proof, just run it https://onlinegdb.com/0ngLPXXn8 – Mecki Dec 20 '21 at 21:20
  • _"not very portable, as it requires `bash`"_ - ... yes? The question is tagged `bash` so... what is your point? It's like answering a question tagged `C++` with , _"using lambdas isn't very portable since it requires C++, not C, and it requires at least C++11"_. – Ted Lyngmo Jul 04 '23 at 17:35
  • @TedLyngmo My solution works in bash as well but it also works everywhere else and when Apple stopped using bash as default shell and switched zsh, all the developers who relied on bash-only stuff were crying and everyone who didn't have that was shrugging their shoulders. Bash is not a language, it's just an implementation of a language, so your comparison doesn't even make sense. This is not about different versions but about using C++ standard or using C++ standard + extensions that only work in a single C++ compiler and nowhere else. – Mecki Jul 05 '23 at 13:26
  • It doesn't work _"everywhere else"_, but that's beside the point. A solution that will refrain from using the unique features and idioms attached to the language/flavour at hand will usually be inferior to that which uses the features available and the idioms that goes with it. Just like chosing a C-like solution in C++ because it will work in C too. _"Bash is not a language"_ - It has enough differences from some hypothetical "pure sh" to at least be a unique flavour and the OP chose the bash tag to specify this flavour. – Ted Lyngmo Jul 05 '23 at 16:34
  • @TedLyngmo My solution is POSIX shell conform and pretty much all shells support the POSIX shell standard. They may extend it to some degree or add an own alternative syntax but they all support the POSIX standard. And every system that has a shell that does not understand POSIX shell syntax always bundles an alternative shell that does. Remove POSIX shell compatibility from any Linux/UNIX/BSD/macOS system of your choice and watch the system fail to even boot. – Mecki Jul 05 '23 at 16:42
  • I'm not advocating removing POSIX compatibility since it's a good foundation. I'm advocating using the tool at hand to the fullest. If OP had not been specific about wanting to do fallthroughs in bash, I wouldn't have had strong opinions against an answer advocating to _not_ use bash fallthroughs. – Ted Lyngmo Jul 05 '23 at 16:56
  • @TedLyngmo And I wanted to show that you don't require bash and that a solution is possible that will also work if one day your script also has to run on a system without bash available. Since no matter how good your C code compiles with MSVC, one day it may have to compile also with GCC and that won't be a problem when you followed the C standard instead of relying on Microsoft proprietary extensions. It's not that I suggested a solution that would not have worked with bash, did I? The question was how can I do that in bash, not how can I do in a way that will not work anywhere but in bash. – Mecki Jul 05 '23 at 17:03
2

Use a sequence of case statements with pattern alternation:

#!/bin/sh

a="2"
case "$a" in
    "1")
        echo "QUICK"
        ;;
esac
case "$a" in
    "1" | "2")
        echo "BROWN"
        ;;
esac
case "$a" in
    "1" | "2" | "3")
        echo "FOX"
        ;;
esac
case "$a" in
    "4")
        echo "JUMPED"
        ;;
esac

Benefits:

  • Follows the format of the original question
  • Works with any POSIX-compliant sh, including old versions of Bash
  • Unordered string values of a such as "apple", "pear", "banana", "tree" will work
  • Control flow is straightforward without backward branches

Disadvantages:

  • More characters are needed since there are four case statements instead of just one

Note that lowercase a is better than uppercase A for non-environment variables and that $a as well as the case patterns should be quoted for the general case where a might have spaces in it. Quoting can be removed for simple cases.

cjfp
  • 153
  • 1
  • 9
  • I really like this approach, because of its readability, even with the burden of extra characters. I think people who don't have a lot of bash experience will be less likely to be confused, and more likely to intuitively understand how it works and how to make changes. Note that cases that do not need fallthrough can be combined in the same case statement. You only need a separate case statement where a 'break' would be omitted in a 'switch' statement.. – Bob Kerns Apr 06 '23 at 22:33
0

While @cjfp's answer with multiple case statements would be my preferred approach to actually doing fallthrough, I usually avoid the issue entirely.

The benefits of this are not obvious for trivial examples, but when each case becomes complex, fallthrough becomes a nightmare to follow.

The solution is to simplify the case statement with functions.

Approach #1

#!/bin/bash

quick() {
  echo "QUICK"
}
brown() {
  echo "BROWN"
}
fox() {
  echo "FOX"
}
jumped() {
  echo "JUMPED"
}

qbf() {
  case "$1" in
    1)
      quick
    ;;
    2)
      quick; brown
    ;;
    3)
      quick; brown; fox
    ;;
    4)
      quick; brown; fox; jumped
    ;;
  esac
}
qbf "$1"

Of course, for large numbers of cases, this can get out of hand. But it need not grow linearly.

Let's add "over the lazy dog". I'll omit the echo-functions; you get the idea.

Approach #2

# Continuing from above
lazydog() {
  case "$1" in
  5)
    qbf "$1"; over
  ;;
  6)
    qbf "$1"; over; the
  ;;
  7)
    qgf "$1"; over; the; lazy
  ;;
  8)
   qbf "$1"; over; the; lazy; dog
  ;;
  esac
}

But this gets out of hand, too. But really long case statements are already problematic. We'd like to avoid adding any redundancy at all.

So here's how we can do that! (Reverting to the original example for brevity).

Approach #3

#!/bin/sh

foxy() {
  case "$1" in
    1)
      echo "QUICK"
      ;;
    2)
      foxy 1; echo "BROWN"
    ;;
    3)
      foxy 2; echo "FOX
    ;;
    4)
      foxy 3; echo "JUMPED"
    ;;
  esac
}

foxy "$1"

What goes on here is that fallthrough is replaced with invoking the prior case recursively before the current one.

I normally use approach #1 for simple cases. (I probably use @cjfp's approach at times without even thinking about it in very simple cases).

If I get to the level I'd want approach #2, I'd just skip ahead to the more general approach #3. #2 is introduced as a stepping-stone to recursion.

But really, if I get to #3, I'm considering writing it in something other than bash. But sometimes bash is a constraint, thanks to its incredible ubiquity.

But I try to avoid fallthrough even in languages that support it.

It's a lot more clear to think of each case separately, with the option to include the prior case. It's a lot more powerful, as well. You can have all later cases include case 1 without including cases 2...n-1.

Between breaking out the cases as functions that can be invoked independently, and recursive invocation, each case can be composed freely from a palette of independent functions and/or complete sub-cases.

switch only looks attractive for very simple situations, with a very linear structure.

Between the options I present here, along with @cjfp's approach with multiple case statements, or a state-machine approach such as in @Meki's answer, the lack of a case statement is not much of a burden.

The risks of accidental fallthrough outweigh the benefits; thus language designers tend to shy away from including switch, despite its familiarity due to certain widely-used languages. It's not been a part of any language I've had a part in designing. I can't take credit; it was widely recognized even before my time, and I'm retired now.

I hope that my answer, along with the others here, give you a different way of thinking about the problem. I'd avoid the ';&' feature. Even aside from compatibility problems you are giving up the semantic safety offered by vanilla 'case'.

Bob Kerns
  • 1,767
  • 1
  • 16
  • 14
-1

bash switch with fallthrough, implemented with function and flag

#! /bin/sh

switch_fallthrough() {
  [ $# = 0 ] && { echo no value >&2; return; }
  local f= # fall through
  [ "$1" = 1 ] && { echo quick; f=1; }
  [ $f ] || [ "$1" = 2 ] && { echo brown; f=1; }
  [ $f ] || [ "$1" = 3 ] && { echo fox; return; }
  [ $f ] || [ "$1" = 4 ] && echo jumped
  return 1 # error = no case did match
}

switch_fallthrough "2"
# brown
# fox
milahu
  • 2,447
  • 1
  • 18
  • 25