cs.thefarshad
medium

Control Flow & Exit Codes

Branch and loop with if, test, case, for, while, and functions — all driven by the exit status that every command returns.

Shell logic is built on one idea: every command returns an exit status0 for success, non-zero for failure. That number, available as $?, is what if, &&, and || test. Once you internalize this, control flow falls into place.

Step through the scripts below to watch which branch or iteration runs and how $? changes after each line.

input
1n=$1
2if (( n < 0 )); then
3 echo "negative"
4elif (( n == 0 )); then
5 echo "zero"
6else
7 echo "positive"
8fi
9echo "done ($?)"
assign n=-3; assignment succeeds → $?=0
$? =0success
vars:n=-3
output
1/5

Tests and if

if runs a command and branches on its exit status. The “command” is usually a test:

  • [[ ... ]] is bash’s modern test: string and file checks, no surprises with empty variables.
  • (( ... )) is arithmetic: it’s true when the expression is non-zero.
  • [ ... ] (a.k.a. test) is the older, portable form.
if [[ -f config.yml ]]; then
  echo "found config"
elif [[ -d config.d ]]; then
  echo "found config dir"
else
  echo "no config"
fi

if (( count > 10 )); then echo "many"; fi

Common [[ ]] tests: -f file, -d dir, -z str (empty), -n str (non-empty), ==, !=, and =~ for regex.

&& and ||

These chain commands by exit status — no if needed:

mkdir build && cd build        # cd only if mkdir succeeded
grep -q TODO file || echo "clean"   # echo only if grep found nothing
make && echo OK || echo FAILED

&& runs the right side only on success ($? == 0); || only on failure.

Loops

for f in *.log; do
  echo "processing $f"
done

i=1
while (( i <= 3 )); do
  echo "tick $i"
  (( i++ ))
done

until ping -c1 host &>/dev/null; do
  sleep 1                       # retry until the host answers
done

for iterates a list, while repeats while a test succeeds, and until repeats until it does.

case and functions

case matches a value against glob-style patterns — cleaner than a long if/elif chain:

case "$1" in
  *.txt|*.md) echo "text file" ;;
  *.png)      echo "image" ;;
  *)          echo "unknown" ;;   # default
esac

Functions group commands and return an exit code with return:

is_even() { (( $1 % 2 == 0 )); }   # the last command's status IS the return
is_even 4 && echo "yes"            # prints yes

A function’s exit status is that of its last command (or an explicit return N), so it slots straight into if, &&, and ||.

Takeaways

  • Every command yields an exit status; 0 is success, non-zero is failure.
  • if/while/until branch on a command’s status; [[ ]] and (( )) are tests.
  • && runs on success, || on failure — concise alternatives to if.
  • case matches patterns; functions return their last command’s status.

References