Key Takeaways
- Bash case statements make scripts easier to read and preserve compared to long if-then-else statements.
- The case statement in Bash matches an expression with patterns in clauses and executes statements accordingly.
- Multiple patterns can be used in a single clause, making the script more concise and efficient. Digits or numerical variables can also be used in case statements.
Bash case statements are powerful yet easy to write. When you revisit an old Linux script you’ll be glad you used a case statement instead of a long if-then-else statement.
The case Statement
Most programming languages have their version of a switch or case statement. These direct the flow of program execution according to the value of a variable. Typically, there is a branch of execution defined for each of the expected possible values of the variable and one catch-all or default branch for all other values.
The logical functionality is similar to a long sequence of if-then statements with an else statement catching everything that hasn’t been previously handled by one of the if
statements.
The Bash implementation of case tries to match an expression with one of the clauses. It does this by looking at each clause, in turn, trying to find a matching pattern. Patterns in clauses are strings, but—counterintuitively—that doesn’t mean we can’t use numerical values as the expression.
The Generic case
The generic form of the case statement is this:
case expression in pattern-1)
statement
;;
pattern-2)
statement
;;
.
.
.
pattern-N)
statement
;;
*)
statement
;;
esac
- A case statement must start with the case keyword and end with the
esac
keyword. - The expression is evaluated and compared with the patterns in each clause until a match is found.
- The statement or statements in the matching clause are executed.
- A double semicolon “
;;
” is used to end a clause. - If a pattern is matched and the statements in that clause executed, all other patterns are ignored.
- There is no limit to the number of clauses.
- An asterisk “
*
” denotes the default pattern. If an expression isn’t matched with any of the other patterns in the case statement the default clause is executed.
A Simple Example
This script tells us the opening hours for an imaginary shop. It uses the date
command with the +"%a"
format string to acquire the shortened day name. This is stored in the DayName
variable.
#!/bin/bashDayName=$(date +"%a")
echo "Opening hours for $DayName"
case $DayName in
Mon)
echo "09:00 - 17:30"
;;
Tue)
echo "09:00 - 17:30"
;;
Wed)
echo "09:00 - 12:30"
;;
Thu)
echo "09:00 - 17:30"
;;
Fri)
echo "09:00 - 16:00"
;;
Sat)
echo "09:30 - 16:00"
;;
Sun)
echo "Closed all day"
;;
*)
;;
esac
Copy that text into an editor and save it as a file called “open.sh.”
We’ll need to use the chmod command to make it executable. You’ll need to do that for all of the scripts you create as you work through this article.
chmod +x open.sh
We can now run our script.
./open.sh
The day the screenshot was taken happens to be a Friday. That means the DayName variable holds the string “Fri.” This is matched with the “Fri” pattern of the “Fri)” clause.
Note that the patterns in the clauses don’t need to be wrapped in double quotes, but it doesn’t do any harm if they are. However, you must use double quotes if the pattern contains spaces.
The default clause has been left empty. Anything that doesn’t match one of the preceding clauses is ignored.
That script works and it is easy to read, but it is long-winded and repetitive. We can shorten that type of case statement quite easily.
Using Multiple Patterns in a Clause
A really neat feature of case statements is you can use multiple patterns in each clause. If the expression matches any of those patterns the statements in that clause are executed.
Here’s a script that tells you how many days there are in a month. There can only be three answers: 30 days, 31 days, or 28 or 29 days for February. So, although there are 12 months we only need three clauses.
In this script, the user is prompted for the name of a month. To make the pattern matching case insensitive we use the shopt command with the “-s nocasematch” option. It won’t matter if the input contains uppercase, lowercase, or a mixture of the two.
#!/bin/bashshopt -s nocasematch
echo "Enter name of a month"
read month
case $month in
February)
echo "28/29 days in $month"
;;
April | June | September | November)
echo "30 days in $month"
;;
January | March | May | July | August | October | December)
echo "31 days in $month"
;;
*)
echo "Unknown month: $month"
;;
esac
February gets a clause to itself, and all the other months share two clauses according to whether they have 30 or 31 days in them. Multi-pattern clauses use the pipe symbol “|” as the separator. The default case catches badly spelled months.
We saved this into a file called “month.sh”, and made it executable.
chmod +x month.sh
We’ll run the script several times and show that it doesn’t matter if we use uppercase or lowercase.
./month.sh
Because we told the script to ignore differences in uppercase and lowercase any month name spelled correctly is handled by one of the three main clauses. Badly spelled months are caught by the default clause.
Using Digits In case Statements
We can also use digits or numerical variables as the expression. This script asks the user to enter a number in the range 1..3. To make it clear that the patterns in each clause are strings, they’ve been wrapped in double quotes. Despite this, the script still matches the user’s input to the appropriate clause.
#!/bin/bashecho "Enter 1, 2, or 3: "
read Number
case $Number in
"1")
echo "Clause 1 matched"
;;
"2")
echo "Clause 2 matched"
;;
"3")
echo "Clause 3 matched"
;;
*)
echo "Default clause matched"
;;
esac
./number.sh
Using case Statements in for Loops
A case statement tries to pattern match a single expression. If you have a lot of expressions to process, you can put the case statement inside a for loop.
This script executes the ls command to get a list of files. In the for loop, file globbing—similar but different to regular expressions —is applied to each file in turn to extract the file extension. This is stored in the Extension string variable.
The case statement uses the Extension variable as the expression it tries to match to a clause.
#!/bin/bashfor File in $(ls)
do
Extension=${File
case "$Extension" in
sh)
echo " Shell script: $File"
;;
md)
echo " Markdown file: $File"
;;
png)
echo "PNG image file: $File"
;;
*)
echo "Unknown: $File"
;;
esac
done
Save this text into a file called “filetype.sh”, make it executable, and then run it using:
./filetype.sh
Our minimalist file type identification script works.
Handling Exit Codes With case Statements
A well-behaved program will send an exit code to the shell when it terminates. The conventional scheme uses an exit code value of zero to imply a problem-free execution, and values of one or more to imply different types of error.
Many programs use only zero and one. Lumping all error conditions into a single exit code makes identifying problems more difficult, but it is common practice.
We created a small program called “go-geek” that would randomly return exit codes of zero or one. This next script calls go-geek. It acquires the exit code using the $? shell variable and uses that as the expression for the case statement.
A real-world script would do appropriate processing according to the success or failure of the command that generated the exit code.
#!/bin/bashgo-geek
case $? in
"0")
echo "Response was: Success"
echo "Do appropriate processing in here"
;;
"1")
echo "Response was: Error"
echo "Do appropriate error handling in here"
;;
*)
echo "Unrecognised response: $?"
;;
esac
Save this into a script called “return-code.sh” and make it executable. You’ll need to substitute some other command for our go-geek command. You could try to cd into a directory that doesn’t exist to get an exit code of one, and then edit your script to cd to an accessible directory to get an exit code of zero.
Running the script a few times shows the different exit codes being correctly identified by the case statement.
./return-code.sh
Legibility Helps Maintainability
Going back to old Bash scripts and working out how they do what they do, especially if they were written by someone else, is challenging. Amending the functionality of old scripts is even harder.
The case statement gives you branching logic with clear and easy syntax. That’s a win-win.