1. Overview
Bash is a powerful and versatile scripting language that can be used to automate tasks, manipulate data, and perform complex operations. One feature that makes Bash so flexible is the ability to use conditional statements:
- if
- elif
- else
- case
In particular, we use these conditional statements to control the flow of execution based on different conditions.
In this tutorial, we’ll focus on the case statement and learn how to write nested case statements. Finally, we’ll learn how to use the ;; terminator to end each case option.
2. case Statements
case is a conditional statement that executes specific actions based on a matching pattern:
$ cat case_example.sh
#!/usr/bin/env bash
case word in
pattern1)
action1
;;
pattern2)
action2
;;
...
*)
default_action
;;
esac
Let’s interpret this code snippet as shown by cat:
- word: variable or expression we want to test against the patterns
- pattern1 and pattern2: literal strings or regular expressions that can match word
- action1 and action2: commands or statements we want to execute for each match
- ;; terminator: indicate the end of each case option
- *: pattern used as a default option when none of the other patterns matches the word
- esac: keyword used to indicate the end of the case statement
For instance, let’s suppose we have a variable called color that can have one of several values:
- red
- green
- blue
- yellow
We can use a case statement to print a different message for each color using the echo command:
$ cat case_script.sh
#!/usr/bin/env bash
color="green"
case $color in
red)
echo "The color is red."
;;
green)
echo "The color is green."
;;
blue)
echo "The color is blue."
;;
yellow)
echo "The color is yellow."
;;
*)
echo "The color is unknown."
;;
esac
Now, let’s make the above script file executable using the chmod command and run it:
$ chmod +x case_script.sh
$ ./case_script.sh
The color is green.
The output shows that green matches the second pattern in the case statement, and the corresponding message is printed.
3. Nested case Statements
Sometimes, we may need to test more than one condition or variable in our script.
For instance, let’s suppose we have two variables called animal and sound that can have different values depending on the type of animal and the sound it makes. We can use a nested case statement to print a different message for each combination of animal and sound:
$ cat nested_script.sh
#!/usr/bin/env bash
animal="dog"
sound="bark"
case $animal in
dog)
case $sound in
bark)
echo "The dog barks."
;;
howl)
echo "The dog howls."
;;
*)
echo "The dog makes an unknown sound."
;;
esac
;;
cat)
case $sound in
meow)
echo "The cat meows."
;;
purr)
echo "The cat purrs."
;;
*)
echo "The cat makes an unknown sound."
;;
esac
;;
*)
echo "The animal is unknown."
;;
esac
Now, let’s make the script file executable and run it:
$ chmod +x nested_script.sh
$ ./nested_script.sh
The dog barks.
The output is The dog barks. This is because the animal variable is set to dog and the sound variable is set to bark. The code first checks the value of animal using the outer case statement. Since animal is dog, it matches the first pattern, dog, within the outer case.
After the match, execution continues in the inner case statement, which checks the value of sound. Since sound is bark, it matches the first pattern bark within the inner case.
In summary, the code runs lines from the outside and inside case statements.
4. The ;; Terminator
As we’ve seen, we use the ;; terminator to indicate the end of each case option. It tells Bash that there are no more commands or statements to execute for that option and that it should move on to the next option or exit the case statement.
Without the ;; terminator, Bash would continue to execute the commands or statements from the next option. Further, this would continue until it finds another ;; terminator or reaches the end of the case statement. Consequently, we can have unexpected results or errors.
Let’s see an instance where we intentionally omit one ;; terminator:
$ cat case_script.sh
#!/usr/bin/env bash
color="green"
case $color in
red)
echo "The color is red."
;;
green)
echo "The color is green."
blue)
echo "The color is blue."
;;
yellow)
echo "The color is yellow."
;;
*)
echo "The color is unknown."
;;
esac
Running this script results in an error:
$ ./case_script.sh
./case_script.sh: line 10: syntax error near unexpected token `)'
./case_script.sh: line 10: ` blue)'
This indicates that Bash detected a syntax error due to the missing ;; terminator. In this case, we missed the ;; terminator after the green case option. This made Bash think that the blue option was part of the action for the previous option. Since blue) isn’t a valid command on its own, it resulted in a syntax error.
Thus, if we remove the line containing blue), it will give a different output:
$ ./case_script
The color is green.
The color is blue.
This is because Bash executes the echo command for both the green and blue options. This may not be what we intended and could cause confusion or errors in our script.
Therefore, it’s important to always use the ;; terminator after each case option code segment to avoid problems.
5. Advanced Use Cases
Moreover, case statements can be useful for handling complex condition logic in shell scripts:
- create a menu-driven program that enables users to choose from different options and sub-options
- validate user input and perform different actions based on the input format or value
- parse command-line arguments and options and execute different commands or functions based on them
- implement a finite automaton that changes its behavior based on the current state and the input
To illustrate some of these use cases, let’s consider a practical example.
6. Temperature Conversion Script
Let’s look at an example script that converts a temperature value from one unit to another using case statements.
Firstly, the script takes two arguments:
- temperature value
- unit
The unit can be either Celsius (C), Fahrenheit (F), or Kelvin (K). Then, the script converts the temperature to the other two units and prints the result. Also, the script validates the input and handles errors.
6.1. Argument Validation and Value-checking
The first step in writing the script is to check if two arguments are provided. If not, we print a usage message and exit with a non-zero exit code:
$ cat convert_temp.sh
#!/usr/bin/env bash
# Check if two arguments are given
if [ $# -ne 2 ]; then
echo "Usage: $0 temperature unit"
exit 1
fi
# Assign arguments to variables
temp=$1
unit=$2
...
Notably, we use the special variable $# to check the number of arguments.
We can then use a regex to match a valid decimal number:
$ cat convert_temp.sh
...
# Validate temperature value
if ! [[ $temp =~ ^-?[0-9]+(\.[0-9]+)?$ ]]; then
echo "Invalid temperature value: $temp"
exit 2
fi
...
The regex consists of several parts:
- ^ matches the beginning of the string
- -? matches an optional minus sign for negative numbers
- [0-9]+ matches one or more digits from 0 to 9
- (.[0-9]+)? matches an optional group of a decimal point followed by one or more digits for fractional numbers
- $ matches the end of the string
Further, we used the ! symbol to negate the expression and check if the temperature value doesn’t match the regex. If it doesn’t match, we print an error message and exit with a non-zero exit code. In this case, we’ll use 2 as an exit code for an invalid temperature value.
6.2. Temperature Conversion case Statement
The next step is to convert the temperature to other units using case statement. We want to convert the temperature based on the unit argument and print the result in Celsius, Fahrenheit, and Kelvin units:
$ cat convert_temp.sh
...
# Convert temperature to other units
case $unit in
C|c)
# Convert Celsius to Fahrenheit and Kelvin
f=$(echo "scale=2; $temp * 9 / 5 + 32" | bc)
k=$(echo "scale=2; $temp + 273.15" | bc)
echo "$temp C = $f F = $k K"
;;
F|f)
# Convert Fahrenheit to Celsius and Kelvin
c=$(echo "scale=2; ($temp - 32) * 5 / 9" | bc)
k=$(echo "scale=2; ($temp + 459.67) * 5 / 9" | bc)
echo "$temp F = $c C = $k K"
;;
K|k)
# Convert Kelvin to Celsius and Fahrenheit
c=$(echo "scale=2; $temp - 273.15" | bc)
f=$(echo "scale=2; $temp * 9 / 5 - 459.67" | bc)
echo "$temp K = $c C = $f F"
;;
*)
# Handle invalid unit
echo "Invalid unit: $unit"
exit 3
;;
esac
In the code snippet, the case statement checks the unit argument and matches it with one of three options:
- C
- F
- K
Moreover, the script accepts both upper and lower-case letters.
In addition, we use arithmetic expressions and the bc command to perform the conversion calculations for each unit option. Also, we use the scale parameter to specify the number of decimal places in the result. Notably, there is a ;; terminator after each option to end the execution of that option.
We also added a default option * to handle invalid units. If none of the other options match, we print an error message and exit with a non-zero exit code. In this instance, we use 3 as an exit code for an invalid unit.
6.3. Running the Script
Now, we make the script executable:
$ chmod +x convert_temp.sh
Now, let’s see some examples of running the script with different inputs:
$ ./convert_temp.sh 100 C
100 C = 212 F = 373.15 K
$ ./convert_temp.sh -40 f
-40 F = -40 C = 233.15 K
$ ./convert_temp.sh 273.15 k
273.15 K = 0 C = 32 F
$ ./convert_temp.sh abc C
Invalid temperature value: abc
$ ./convert_temp.sh 100 X
Invalid unit: X
$ ./convert_temp.sh
Usage: ./convert_temp.sh temperature unit
As we can see, the script works as expected and converts the temperature to other units correctly. It also handles invalid inputs gracefully and prints appropriate error messages.
7. Conclusion
In this article, we’ve learned how to use case statements and ;; terminators in Bash. We’ve seen how case statements enable control of the flow of execution based on different conditions and values. Finally, we saw a practical example of a script that converts a temperature from one unit to another using case statements.