Freebie

Control flow

Arguments and positional parameters

Shell arguments are referenced by number, prefixed with $. The following table describes the special variables:

VariableValue
$0Name of the script
$1, $2, …First, second, and subsequent arguments
$#Number of arguments passed
$*All arguments as a single string
$@All arguments as separate words
"$*"All arguments joined into one string, separated by spaces
"$@"All arguments as individually quoted strings

A practical example that processes all arguments passed to a script:

#!/bin/bash
echo "Script: $0"
echo "Arguments: $#"
echo "First arg: $1"

for arg in "$@"; do
    echo "Processing: $arg"
done

Default values

Provide fallback values for arguments that may not be set:

PATTERN=${1:-"PDF document"}    # use first arg, or default to "PDF document"
STARTDIR=${2:-.}                 # use second arg, or default to current directory

Control flow

if / elif / else

End every if block with fi. The condition can be a command (any exit status), a test in [ ], or a compound test in [[ ]]:

if [ -f "$CONFIG" ]; then
    echo "Config found: $CONFIG"
elif [ -d "$CONFIG" ]; then
    echo "Error: $CONFIG is a directory, not a file" >&2
    exit 1
else
    echo "Config not found, using defaults"
fi

Use [[ ]] for pattern matching and string comparisons, which handles special characters more safely:

if [[ $HOSTNAME == web-* ]]; then
    echo "This is a web server"
fi

Commands as conditions

Any command can serve as an if condition. The shell runs the command and tests its exit code: 0 is true, any non-zero value is false.

if touch testfile; then
    echo "Created testfile"
else
    echo "Could not create testfile" >&2
fi

Use it whenever a command’s success or failure is all you need:

if mkdir -p /var/app/logs; then
    echo "Log directory ready"
fi

if grep -q "ERROR" /var/log/app.log; then
    echo "Errors found in log"
fi

Negate a condition with !:

if ! pgrep -x nginx > /dev/null; then
    echo "nginx is not running" >&2
    exit 1
fi

For simple one-liners, && and || are shorter alternatives. && runs the second command only if the first succeeds; || runs it only if the first fails:

mkdir -p /var/app/logs && echo "Directory ready"
pgrep -x nginx > /dev/null || echo "nginx is not running" >&2

Condition tests

Numeric comparisons

The following table shows numeric test operators:

TestTrue when
n1 -eq n2n1 equals n2
n1 -ne n2n1 does not equal n2
n1 -gt n2n1 is greater than n2
n1 -ge n2n1 is greater than or equal to n2
n1 -lt n2n1 is less than n2
n1 -le n2n1 is less than or equal to n2

For arithmetic comparisons, (( )) is more natural and does not require the -eq syntax:

if (( disk_usage > 90 )); then
    echo "Critical: disk almost full"
fi

String comparisons

The following table shows string test operators:

TestTrue when
str1 = str2str1 equals str2
str1 != str2str1 does not equal str2
str1 < str2str1 sorts before str2
str1 > str2str1 sorts after str2
-n str1str1 is non-empty
-z str1str1 is empty

File tests

The following table shows file test operators:

TestTrue when
-e fileFile exists
-f fileFile exists and is a regular file
-d fileFile exists and is a directory
-r fileFile exists and is readable
-w fileFile exists and is writable
-x fileFile exists and is executable
-s fileFile exists and is not empty
-L fileFile exists and is a symbolic link
-O fileFile exists and is owned by the current user
f1 -nt f2f1 is newer than f2
f1 -ot f2f1 is older than f2

A real-world example combining file and string tests:

#!/bin/bash
LOCKFILE="/var/run/backup.lock"

if [ -f "$LOCKFILE" ]; then
    echo "Backup already running (lock file exists)" >&2
    exit 1
fi

touch "$LOCKFILE"
trap "rm -f $LOCKFILE" EXIT    # clean up lock file when the script exits

case statements

The case statement matches a value against patterns and runs the corresponding commands. It is the bash equivalent of switch in other languages. End each case block with ;; and the default case with *:

case "$ENVIRONMENT" in
    production)
        DB_HOST="db.prod.internal"
        LOG_LEVEL="error"
        ;;
    staging)
        DB_HOST="db.staging.internal"
        LOG_LEVEL="warn"
        ;;
    development|dev)
        DB_HOST="localhost"
        LOG_LEVEL="debug"
        ;;
    *)
        echo "Unknown environment: $ENVIRONMENT" >&2
        exit 1
        ;;
esac

Loops

End every loop with done.

for loops

Iterate over a list of values:

for server in web-01 web-02 web-03; do
    echo "Deploying to $server"
    ssh "$server" "./deploy.sh"
done

Iterate over all files in a directory:

for file in $(ls /var/log/*.log | sort); do
    if [ -f "$file" ]; then
        echo "$file: $(wc -l < $file) lines"
    fi
done

Use C-style syntax for numeric loops:

for (( i=0; i < 10; i++ )); do
    echo "Attempt $i"
done

while loops

Run while a condition is true:

RETRIES=0
MAX_RETRIES=5

while (( RETRIES < MAX_RETRIES )); do
    if ./health_check.sh; then
        echo "Service is healthy"
        break
    fi
    (( RETRIES++ ))
    echo "Attempt $RETRIES failed, retrying in 10 seconds"
    sleep 10
done

Read a file line by line with a while loop:

while read -r LINE; do
    echo "Processing: $LINE"
done < /etc/hosts

A while loop that renames files containing spaces by replacing spaces with underscores:

find /data -type f | while read -r file; do
    if [[ "$file" = *[[:space:]]* ]]; then
        mv "$file" "$(echo "$file" | tr ' ' '_')"
    fi
done

until loops

Run until a condition becomes true (the opposite of while):

COUNT=0
until [ "$COUNT" -gt 5 ]; do
    echo "Count is: $COUNT"
    (( COUNT++ ))
done

Loop control keywords

The following table describes loop control keywords:

KeywordEffect
breakExits the loop immediately
continueSkips to the next iteration
exitExits the entire script with an optional status code
returnReturns from a function with an optional status code