34.3 Conditional Expressions: if/elif/else, test, [[ ]], and (( ))
Right, let’s talk about making your bash scripts smart enough to make decisions. This is where you move from just running commands in sequence to building something that can react, adapt, and occasionally sass you back. We’re going to cover the if statement’s entourage: test, the modern [[ ]], and the math-focused (( )). Pay attention, because the differences here are where most beginners (and let’s be honest, a few pros) faceplant.
The Humble if and its Sidekick test
The basic structure of an if is deceptively simple:
if commands; then
# do things if the commands succeeded (exit code 0)
else
# do things if the commands failed (exit code non-zero)
fi
Notice I said commands, plural. The if statement doesn’t evaluate a special “condition”—it just runs the commands you put after it and checks their exit status. If the last command in the list exits with a 0 (which in the topsy-turvy world of exit codes means success), the then block runs. This is why you can do things like if grep -q "error" logfile.txt; then—it’s just running grep and seeing if it found anything.
But what if you want to check if a file exists, or compare two strings? That’s where test comes in, or its alias [. This is a command (literally /bin/[) that performs checks and returns the appropriate exit code. The syntax is… well, it’s a historical artifact. You must have spaces around the brackets because [ is a command, and the closing ] is just its required final argument.
# Check if a file exists and is readable
if test -r "$my_file"; then
echo "File is present and we can read it. Good."
fi
# The exact same thing, using the [ alias
if [ -r "$my_file" ]; then
echo "File is present and we can read it. Good."
fi
Why the quotes around $my_file? Because if $my_file is unset or contains spaces, [ -r $my_file ] will explode in a glorious spectacle of syntax errors. The quotes prevent word splitting. This is non-negotiable. Do it.
The Superior [[ ]] (if you’re using Bash)
The [ command is a POSIX-standardized old-timer. Bash gives us an upgrade: the [[ ]] compound command. It’s not an external command; it’s understood by Bash itself. This gives it superpowers and a slightly less insane syntax.
- You can use
&&and||inside it without needing to escape them or wrap everything in quotes. This is a huge win for readability. - String comparison with
==uses pattern matching by default. This is incredibly useful. - You can use the powerful
=~operator for regular expressions. - It’s safer. You don’t need to quote variables within it to prevent word splitting (though it’s still a good habit for portability and clarity).
name="Alice Bob"
# This would fail with [ because word splitting would make it [ "Alice" == "Bob" ]
if [[ $name == "Alice Bob" ]]; then
echo "See? No quotes needed around the variable inside [[ ]] to avoid splitting."
fi
# Pattern matching example
if [[ $filename == *.txt ]]; then
echo "This is a text file (probably)."
fi
# Regex example
if [[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]; then
echo "That looks like a valid-ish email address."
else
echo "Nice try. That's not an email."
fi
Unless you’re writing scripts for a system that might only have sh (like inside a minimal Docker container), use [[ ]]. It’s just better.
The Arithmetic Operator (( ))
When you’re dealing with numbers, forget -eq, -lt, and -gt. They’re ugly and confusing (-eq for numbers, == for strings? Come on.). For math, use the (( )) compound command. It’s designed for integer arithmetic.
count=5
# The old, clunky way with [ ]
if [ $count -gt 3 ]; then
echo "Greater than three."
fi
# The new, sensible way with (( ))
if (( count > 3 )); then
echo "Greater than three."
fi
Inside (( )), you can use familiar operators like >, <, ==, <=, >=, and !=. You can even do assignments and more complex math. It returns an exit code of 0 (true) if the result of the expression is non-zero, and 1 (false) if the result is zero. Yes, that’s backwards from normal logic, but it makes sense in a math context: (( 5 - 5 )) evaluates to 0 (false), while (( 5 - 4 )) evaluates to 1 (true).
elif and else: Because Life Isn’t Binary
Sometimes you have more than two options. That’s what elif (else-if) is for. You can chain as many as you need.
read -p "How enthusiastic are you about bash? (1-10): " enthusiasm
if (( enthusiasm >= 10 )); then
echo "A true shell enthusiast! Welcome."
elif (( enthusiasm >= 7 )); then
echo "That's a healthy level of enthusiasm."
elif (( enthusiasm >= 5 )); then
echo "I sense some ambivalence."
else
echo "It's okay. I'll win you over eventually."
fi
Pro tip: order your elif statements from most specific to most general. And always have a catch-all else at the end to handle the cases you didn’t explicitly account for. Your future self, debugging at 2 AM, will thank you.