Computing and the Command Line

Decisions and Loops

if, elif, and else branch on a command's exit status. Tests with [[ ]] for files, strings, regular expressions, and numbers, and (( )) for arithmetic. case for matching patterns. for loops over words, files, and ranges; while read for reading a file line by line; until; break and continue. Looping over files safely, including names with spaces and folders with no matches.

  • 7 min
  • 8 steps
  • 3 questions
  • Lesson 22 of 80

In this lesson

  1. if, elif, else
  2. Tests with [[ ]]
  3. case
  4. for loops
  5. Looping over files
  6. while and until
  7. Your turn
  8. So

if, elif, else

if runs a command and branches on its exit status: 0 means “then,” anything else means “else” (module 6) 1:

if grep -q "^$USER:" /etc/passwd; then
    echo "you're a local user"
elif [[ -n "${SUDO_USER-}" ]]; then
    echo "running under sudo"
else
    echo "who are you?"
fi

then must start the block; fi (“if” backwards) ends it. Put then on the same line after a semicolon, as Google’s style guide does 2.

Quick check

What does if actually check?

Tests with [[ ]]

Most conditions are written with [[ ... ]], bash’s test command. It succeeds or fails, so it works with if, &&, and || 1 3. Spaces inside the brackets are required.

Test True when
-e path it exists
-f path it’s a regular file
-d path it’s a directory
-r, -w, -x path you can read, write, run it
-s path it exists and isn’t empty
a -nt b a is newer than b
"$s" == "text" strings equal (!= for not equal)
"$s" == *.jpg string matches a wildcard pattern (unquoted right side)
"$s" =~ ^[0-9]+$ string matches a regular expression
-z "$s", -n "$s" string is empty, not empty
"$a" -lt "$b" numbers: -eq -ne -lt -le -gt -ge

Combine with && and ||, and negate with ! 1:

if [[ -f "$file" && ! -s "$file" ]]; then
    echo "$file exists but is empty"
fi

The older single-bracket form, [ ... ] (the same as the test command), works everywhere sh does but trips over empty variables and unquoted words. In bash scripts, Google’s guide prefers [[ ]] 2.

For arithmetic, (( )) reads like math and needs no $ on variable names 1 3:

count=$(ls | wc -l)
if (( count > 100 )); then
    echo "that's a lot of files"
fi
Left, test expressions inside [[ ]]. Files: -e exists, -f regular file, -d directory, -r -w -x readable, writable, runnable, -s not empty. Strings: == and != equal and not equal, -z and -n empty and not empty, =~ matches a regex. Numbers: -eq and -ne, -lt -le -gt -ge, and (( a > b )) for math comparisons. Example [[ -f "$f" && -s "$f" ]], with spaces inside the brackets and variables quoted. Right, loops and case in a terminal: for f in *.jpg; do echo "$f"; done; for i in {1..5}; for (( i = 0; i < 5; i++ )); while read -r line; do echo "> $line"; done < notes.txt; case "$answer" in y|yes) echo ok ;; *) echo no ;; esac. break leaves the loop; continue goes to the next round.
The questions and the repetitions every script is made of. Credit: StudyCorner diagram · CC BY 4.0 · Source

case

case matches one value against several wildcard patterns, top to bottom, and runs the first that matches 1:

read -rp "Continue? [y/N] " answer
case "$answer" in
    y|Y|yes)  echo "continuing" ;;
    n|N|no|"") echo "stopping"; exit 0 ;;
    *)        echo "please answer y or n" >&2; exit 1 ;;
esac

| separates alternatives, *) catches everything else, and ;; ends each branch. It’s cleaner than a long chain of elifs, and it’s how scripts handle options and file types:

case "$file" in
    *.tar.gz|*.tgz) tar -xzf "$file" ;;
    *.zip)          unzip "$file" ;;
    *)              echo "don't know how to unpack $file" >&2 ;;
esac

for loops

for runs its body once for each word in a list 1:

for name in alex sam kids; do
    echo "making a folder for $name"
    mkdir -p "/srv/household/$name"
done

for i in {1..5}; do echo "round $i"; done          # a range (brace expansion)

for (( i = 1; i <= 10; i += 3 )); do echo "$i"; done  # C style: 1 4 7 10

Looping over files

Let the shell’s wildcards produce the list, not ls. Each match arrives as one word, spaces and all, as long as you quote "$f" inside the loop 1:

for f in ~/photos/*.jpg; do
    echo "resizing $f"
    # convert "$f" -resize 50% "${f%.jpg}-small.jpg"
done

${f%.jpg} removes .jpg from the end of the value (# would remove from the start); these are bash’s string-trimming expansions 3.

If nothing matches, the pattern is left as the literal text ~/photos/*.jpg and the loop runs once on a file that doesn’t exist. Two fixes: check [[ -e "$f" ]] || continue at the top of the loop, or set shopt -s nullglob so an unmatched pattern expands to nothing 3.

Never write for f in $(ls): ls output is split at every space, so Summer 2026.jpg becomes two files 2. For whole trees, use find (module 4) and read its output in a while loop.

Quick check

Your script loops for f in *.jpg in a folder with no JPEGs. What does f become?

while and until

while repeats as long as a command succeeds; until repeats until it does 1:

until ping -c1 -W1 router > /dev/null 2>&1; do
    echo "waiting for the network..."
    sleep 5
done

The most useful while reads a file line by line 1:

while read -r line; do
    echo "> $line"
done < notes.txt

find ~/photos -name '*.jpg' -print0 | while IFS= read -r -d '' f; do
    echo "found: $f"
done

-r stops read from treating backslashes as escapes, and IFS= keeps leading spaces. The second form pairs with find -print0 to survive any file name (module 4).

break leaves a loop early; continue skips to the next round 1.

Quick check

Which loop reads a file one line at a time without mangling spaces or backslashes?

Your turn

Exercises

  1. Write filetype that takes one path and prints “directory,” “empty file,” “file,” or “doesn’t exist.”
  2. Write countdown that counts down from 10 to 1, one per second (sleep 1), then prints “liftoff.”
  3. Write lowercase that renames every *.JPG in the current folder to *.jpg. Test it in a scratch folder first.
  4. Write todo that reads ~/todo.txt line by line and prints each line numbered, skipping blank lines.
  5. Add a case to filetype: if it’s a file, also say “image” for .jpg/.png, “document” for .pdf/.odt, or “other.”
Answers
  1. Using $1, the first argument (next lesson covers arguments):

    #!/bin/bash
    set -euo pipefail
    path="${1:?usage: filetype PATH}"
    if [[ -d "$path" ]]; then
        echo "directory"
    elif [[ -f "$path" && ! -s "$path" ]]; then
        echo "empty file"
    elif [[ -f "$path" ]]; then
        echo "file"
    else
        echo "doesn't exist"
    fi
    
  2. for (( i = 10; i >= 1; i-- )); do echo "$i"; sleep 1; done; echo "liftoff".

  3. Run it in a scratch folder first:

    #!/bin/bash
    set -euo pipefail
    shopt -s nullglob
    for f in *.JPG; do
        mv -i -- "$f" "${f%.JPG}.jpg"
    done
    
  4. For example:

    #!/bin/bash
    set -euo pipefail
    n=0
    while read -r line; do
        [[ -z "$line" ]] && continue
        n=$(( n + 1 ))
        printf '%2d. %s\n' "$n" "$line"
    done < "$HOME/todo.txt"
    

    (Note n=$(( n + 1 )) rather than (( n++ )): under set -e, (( n++ )) “fails” when n is 0, because the expression’s value is 0.)

  5. Inside the file branch: case "$path" in *.jpg|*.png) echo image ;; *.pdf|*.odt) echo document ;; *) echo other ;; esac.

So

if branches on exit status, and [[ ]] asks the questions: files, strings, patterns, numbers. case picks among patterns. for loops over words, ranges, and wildcard matches (never $(ls)); while read -r walks a file line by line; break and continue steer. Quote "$f" everywhere, and plan for no matches.

Lesson complete

Nice work.

1day streak
0/1today's goal
–correct

Up next · 8 min

Arguments and Functions

Next lesson
Sources for this lesson
  1. 1
    William Shotts. The Linux Command Line, Seventh Internet Edition (25.12A). LinuxCommand.org (print edition by No Starch Press). 2026. verifiedFree CC BY-NC-ND 3.0 book, release 25.12A of July 18, 2026. Part 1, Learning the Shell: the shell and terminal emulators, prompts ($ vs. # for the superuser), command history (most distributions keep the last 1,000 commands), Shift-Ctrl-C/V for copy and paste; navigation and the directory tree; exploring the system (ls options and the long listing, file, less, the guided tour of /, symbolic links); manipulating files (wildcards and character classes, mkdir, cp, mv, rm, ln; no undelete, test wildcards with ls first); working with commands (four kinds of commands, type, which, help, --help, man and its sections, apropos, whatis, info, alias); redirection; expansion and quoting; Readline keyboard tricks, completion, history search; permissions; processes. Later parts cover the environment, vi, packages, storage, networking, find, archiving, regular expressions, text processing, and shell scripting.
  2. 2
    Shell Style Guide. Google. verifiedBash is the only shell allowed for executables, which start with #!/bin/bash; executables on PATH need no extension, libraries take .sh and aren't executable. Every file starts with a comment describing it; functions get header comments. Error messages go to STDERR. Quote variables and prefer "${var}"; use "$@" for arguments; prefer [[ ]] over [ ] and $( ) over backticks; use local in functions; constants readonly and capitalized; put the program in a main function called last with main "$@". Scripts over about 100 lines, or with non-straightforward control flow, should be rewritten in a more structured language.
  3. 3
    Chet Ramey, Brian Fox. Bash Reference Manual, Edition 5.3. GNU Project, Free Software Foundation. 2025. verifiedThe reference for Bash 5.3 (May 18, 2025). Redirections (3.6) are processed left to right and order matters: ls > dirlist 2>&1 sends both streams to dirlist, while ls 2>&1 > dirlist sends only standard output there. With set -o noclobber, > fails on an existing regular file and >| overrides it. &> word is equivalent to > word 2>&1 and &>> word to >> word 2>&1. Here documents (<<word, with <<- stripping leading tabs; quoting word disables expansion) and here strings (<<<). Expansions (3.5) happen in a fixed order: brace; tilde, parameter, arithmetic, and command substitution left to right; word splitting; filename expansion; quote removal last. Startup files (6.2): an interactive login shell reads /etc/profile then the first of ~/.bash_profile, ~/.bash_login, ~/.profile; an interactive non-login shell reads ~/.bashrc. HISTCONTROL (ignorespace, ignoredups, ignoreboth), HISTSIZE, HISTFILESIZE; set -x traces expanded commands; shell functions and variables, export.