Bash’s if, else, and case statements aren’t just for basic branching; they’re the bedrock of dynamic command execution, letting your scripts adapt to an astonishing variety of runtime conditions.
Let’s see if in action. Imagine you’re checking if a specific file exists before trying to read it.
FILE_TO_CHECK="/path/to/my/important_data.txt"
if [ -f "$FILE_TO_CHECK" ]; then
echo "File '$FILE_TO_CHECK' exists. Proceeding to read."
cat "$FILE_TO_CHECK"
else
echo "Error: File '$FILE_TO_CHECK' not found. Exiting."
exit 1
fi
Here, [ -f "$FILE_TO_CHECK" ] is the core. It’s a test command that evaluates to true (exit code 0) if "$FILE_TO_CHECK" is a regular file, and false (exit code 1) otherwise. The if statement then executes the commands in the then block if the test is true, or the else block if it’s false. Notice the semicolon after the test condition; it’s required when then is on the same line.
case offers a more structured way to handle multiple distinct possibilities, especially when dealing with string matching. Consider a script that processes different types of input based on an argument:
INPUT_TYPE="$1"
case "$INPUT_TYPE" in
"json")
echo "Processing as JSON..."
# Commands to handle JSON
;;
"xml"|"yaml")
echo "Processing as XML or YAML..."
# Commands to handle XML/YAML
;;
*)
echo "Unknown input type: '$INPUT_TYPE'. Please specify 'json', 'xml', or 'yaml'."
exit 1
;;
esac
In this case statement, "$INPUT_TYPE" is matched against each pattern: "json", "xml"|"yaml", and the catch-all *. The pipe | allows multiple patterns to lead to the same block of commands. Each block ends with ;;. The * pattern acts like a default or else for case, catching anything that doesn’t match previous patterns.
The mental model for these constructs is simple: they are gateways. Each statement evaluates a condition (a test command, a variable’s value) and, based on the outcome, directs the script’s execution down one of several predetermined paths. You’re not just running commands; you’re making decisions about which commands to run, and under what circumstances. This makes your scripts responsive to their environment, user input, or the state of other processes.
The real power comes from combining these. You can nest if statements within case branches, or use complex tests within if conditions. For instance, if [[ "$VAR" =~ ^[0-9]+$ ]] checks if $VAR consists solely of digits using Bash’s extended regular expression matching within double brackets [[ ]], which is more robust than single brackets [ ].
Here’s a critical detail most people overlook: the exit status of the last command executed within a conditional block determines the overall exit status of that block unless an explicit exit command is used. In the if example, if cat succeeds, the if block’s exit status is 0. If cat fails, the if block’s exit status is whatever cat returned. This is crucial for building reliable pipelines and error handling.
The next step in controlling script flow is understanding loops, like for and while, which allow you to execute blocks of code repeatedly based on conditions or lists of items.