Bash Conditionals and Loops

Intermediate
14 min

Bash Conditionals and Loops

Scripts become useful when they make decisions and repeat work. Bash provides if, case, for, while and until, but its test syntax has three different forms and several traps around quoting and word splitting. This lesson teaches the modern forms — [[ ]] for strings and files, (( )) for numbers — and the loop patterns that handle files with spaces and read input line by line correctly.

Exit Status Is the Condition

In bash, a condition is any command. if runs it and checks the exit status: 0 means true, anything else means false. This is why if grep -q pattern file works with no brackets at all.

bash
if grep -q "ERROR" app.log; then echo "errors found" fi if ! ping -c1 -W1 example.com > /dev/null 2>&1; then echo "offline" >&2 fi

[ ] (the test command), [[ ]] and (( )) are simply commands that return 0 or 1.

The [[ ]] Test

[[ ]] is bash's built-in test. Unlike the older [ ], it does not word-split variables, supports &&/|| inside, pattern matching with == and regex matching with =~.

bash
if [[ "$answer" == "yes" ]]; then echo "confirmed"; fi if [[ -z "$name" ]]; then echo "name is empty"; fi if [[ "$file" == *.tar.gz ]]; then echo "gzipped tarball"; fi # glob pattern if [[ "$email" =~ ^[^@]+@[^@]+\.[a-z]+$ ]]; then echo "looks valid"; fi

| Test | True when | |---|---| | -e path | exists | | -f path / -d path / -L path | regular file / directory / symlink | | -s path | exists and is not empty | | -z "$s" / -n "$s" | string is empty / not empty | | "$a" == "$b", != | string equal / not equal |

Use (( )) for numeric comparison rather than the -eq, -lt family, which are still valid inside [[ ]] but harder to read.

bash
if (( count > 100 )); then echo "busy"; fi if (( a >= 1 && a <= 10 )); then echo "in range"; fi

if / elif / else and Short Forms

bash
if [[ -f "$config" ]]; then source "$config" elif [[ -f "/etc/app/default.conf" ]]; then source /etc/app/default.conf else echo "no configuration found" >&2 exit 1 fi [[ -d "$dir" ]] || mkdir -p "$dir" # create if missing [[ -f .env ]] && source .env # load if present

The &&/|| short forms are ideal for one-line guards. Do not chain a && b || c as an if/else; if b fails, c runs too.

case: Matching Patterns

case compares a value against glob patterns and is cleaner than an elif ladder for command dispatch and option parsing.

bash
case "$1" in start) systemctl start app ;; stop|halt) systemctl stop app ;; restart) systemctl restart app ;; status) systemctl status app ;; -h|--help) echo "usage: $0 {start|stop|restart|status}" ;; *) echo "unknown command: $1" >&2; exit 2 ;; esac case "$OSTYPE" in linux*) pkg="apt" ;; darwin*) pkg="brew" ;; esac

Each branch ends with ;;. Patterns support | for alternatives and the usual glob characters.

Loops: for, while and until

for iterates over a list of words: literal values, a glob, an array, or command output.

bash
for env in dev staging prod; do echo "deploying to $env" done for f in *.jpg; do [[ -e "$f" ]] || continue # guard against no matches convert "$f" -resize 50% "thumb_$f" done for i in {1..5}; do echo "attempt $i"; done for (( i = 0; i < 10; i += 2 )); do echo "$i"; done # C-style for host in "${servers[@]}"; do ssh "$host" uptime; done

Never loop over $(ls) or unquoted $(cat file); filenames with spaces will split. Use a glob for files and while read for lines.

while and until

while repeats as long as the condition succeeds; until repeats until it succeeds.

bash
n=0 while (( n < 3 )); do echo "tick $n"; (( n++ )) done until curl -sf http://localhost:3000/health > /dev/null; do echo "waiting for the app..."; sleep 2 done while true; do date; sleep 60 done

Reading a File Line by Line

The canonical pattern uses read -r (do not interpret backslashes) with IFS= (keep leading and trailing whitespace) and redirects the file into the loop.

bash
while IFS= read -r line; do echo "line: $line" done < servers.txt while IFS=, read -r name email country; do echo "$name <$email> ($country)" done < <(tail -n +2 users.csv) # skip the header via process substitution find . -name "*.log" -print0 | while IFS= read -r -d '' f; do gzip "$f" done

If a command inside the loop also reads stdin (ssh, ffmpeg), it will consume your file. Give it < /dev/null or use ssh -n.

break and continue

break leaves the loop; continue skips to the next iteration. Both accept a number to affect enclosing loops (break 2).

bash
for f in *.csv; do [[ "$f" == "template.csv" ]] && continue grep -q "ERROR" "$f" && { echo "bad file: $f"; break; } process "$f" done

Common Mistakes

  • if [ $var == "x" ] with an empty $var, which becomes [ == "x" ] and errors. Quote it or use [[ ]].
  • Missing spaces: [[$a == $b]] is a syntax error; the brackets are commands and need spaces around them.
  • Using = for numbers or -eq for strings.
  • for line in $(cat file) splitting on every space instead of every line.
  • Piping into while read and then wondering why variables set inside are empty afterwards — the pipe runs the loop in a subshell. Use < file or < <(cmd).
Quick Quiz
Question 1 of 3

Which test is true when a variable holds an empty string?

Key Takeaways

  • Any command can be a condition; exit status 0 is true.
  • Use [[ ]] for strings, files and patterns, and (( )) for numbers; always quote variables inside tests.
  • case dispatches on glob patterns and is cleaner than long elif chains.
  • for loops over words, globs or arrays; guard globs with [[ -e "$f" ]] || continue.
  • Read lines with while IFS= read -r line; do ...; done < file, never with for line in $(cat file).

Next lesson: Bash Functions, Arguments and Exit Codes — structure scripts into reusable functions and communicate success or failure to callers.

Bash Conditionals and Loops - Linux & Command Line | CodeYourCraft | CodeYourCraft