Conditionals & Tests
Tests & Exit Status
Bash has no boolean type. A condition is simply a command, and its exit status (0 = true, non-zero = false) decides the branch. Everything else is sugar around that idea.
The test Command
test -f /etc/passwd # true if it is a regular file
[ -f /etc/passwd ] # identical — '[' is a command, ']' closes it
[[ -f /etc/passwd ]] # Bash's extended test (preferred)
[ is an ordinary command. That is why the spaces are mandatory: [ -f x ] works, [-f x] does not.
File Tests
| Test | True when |
|---|---|
-e f | f exists (any type) |
-f f | f exists and is a regular file |
-d f | f exists and is a directory |
-r / -w / -x f | f is readable / writable / executable |
-s f | f exists and is not empty |
[[ -d "$dir" ]] && echo "directory exists"
[[ -s "$log" ]] || echo "log is empty or missing"
[[ -x "$HOME/bin/tool" ]] && "$HOME/bin/tool"
String Tests
[[ -z "$s" ]] # true if s is empty
[[ -n "$s" ]] # true if s is non-empty
[[ "$a" == "$b" ]] # equal (== is a pattern match inside [[ ]])
[[ "$a" != "$b" ]] # not equal
[[ "$a" < "$b" ]] # lexicographic less-than
[[ "$f" == *.txt ]] # glob pattern match — only inside [[ ]]
Numeric Tests
[[ "$n" -eq 0 ]] # equal
[[ "$n" -ne 0 ]] # not equal
[[ "$n" -lt 10 ]] # less than
[[ "$n" -le 10 ]] # less or equal
[[ "$n" -gt 10 ]] # greater than
[[ "$n" -ge 10 ]] # greater or equal
(( n > 10 )) # arithmetic context — often clearer for numbers
[[ ]] vs [ ]
Inside [[ ]] word splitting and globbing do not apply to the operands, so unquoted variables are safer, and the operators &&, ||, < and > work without escaping.
# POSIX: must quote, and must use -a / -o
[ -n "$user" ] && [ -f "$cfg" ]
# Bash: cleaner and safer
[[ -n $user && -f $cfg ]]
Danger in
[ ]: an empty variable makes [ $x -gt 3 ] become [ -gt 3 ], a syntax error. Inside [[ ]] it is handled correctly. Prefer [[ ]] for Bash scripts.
if / elif / else
Syntax
if [[ "$1" == "--help" ]]; then
usage; exit 0
elif [[ "$1" == "--version" ]]; then
echo "v1.0"
else
echo "unknown option: $1" >&2
exit 2
fi
The then keyword is required; put it on the same line as if (after ;) or on the next line. fi closes the block — a reminder that this is a language, not a C-like.
Combining Conditions
if [[ -f "$cfg" && -r "$cfg" ]]; then
echo "readable config"
fi
if ! command -v jq >/dev/null; then
echo "jq is required" >&2; exit 1
fi
case Statements
Matching Patterns
case is a multi-way branch that matches globs, not regular expressions. It is the cleanest way to dispatch on a word.
case "$cmd" in
start|up) echo "starting" ;;
stop|down) echo "stopping" ;;
status) echo "running" ;;
*.log) echo "that is a log file" ;;
*) echo "unknown: $cmd" >&2; exit 2 ;;
esac
Each clause ends with ;;. Alternatives are separated by |.
Fall-through
case "$n" in
1) echo "one" ;& # ;& falls through unconditionally (Bash)
2) echo "two" ;;& # ;;& retests the next patterns (Bash)
esac
Pitfalls
- Missing spaces inside
[ ]cause "command not found". if [ $x = y ]with an emptyxis a syntax error — quote it or use[[ ]].-eqis numeric;==is textual. Comparing"01"and"1"gives different answers for each.- Every
ifneeds exactly onefi;elifis one word, notelse if.