Bash scripting fundamentals: variables, conditions, loops, functions
The core building blocks of a bash script: variables and quoting, if and test, for and while loops, functions, exit codes and a safe starting template with set -euo pipefail.
Drafted with AI from official documentation. Scripts were run in a test environment and repository links verified. Awaiting a human check. Did it work for you? Last edited Outdated?
A bash script is a text file of commands that the shell runs top to bottom. Anything you can type at the prompt works in a script, but once a file grows past a few lines you need variables, decisions, repetition and some discipline about errors. This article covers those building blocks and ends with a template you can copy.
A safe starting template
Start every script with a shebang line and strict mode. set -e stops on the first failing command, set -u makes an unset variable an error instead of an empty string, and set -o pipefail makes a pipeline fail when any stage fails, not only the last one.
#!/usr/bin/env bash
set -euo pipefail
usage() {
echo "Usage: $(basename "$0") <name>" >&2
exit 2
}
[[ $# -eq 1 ]] || usage
name=$1
echo "Hello, $name"
chmod +x hello.sh
./hello.sh world
./hello.sh # prints usage and exits with code 2
Variables
Assign with no spaces around =, read with $name or ${name}. Variables are strings unless you ask for arithmetic. Lowercase names are a good habit for your own variables because uppercase names are conventionally environment variables such as HOME and PATH.
name="Ada"
count=3
file="/tmp/report-$(date +%F).txt" # command substitution
echo "$name has $count items"
echo "${name}_backup" # braces when text follows
echo $(( count * 2 )) # arithmetic
# Defaults for optional values
log_dir=${LOG_DIR:-/var/log/myapp}
Always double-quote variables: "$file". Without quotes, a value containing spaces or wildcards is split into several words or expanded against the file system. The one time you leave quotes off is when you really want splitting, which in practice is almost never.
Conditions: if and test
if runs a command and checks its exit code: zero means true. The usual command to test things is [[ ... ]], which compares strings, numbers and file attributes.
if [[ -f "$file" ]]; then
echo "file exists"
elif [[ -d "$file" ]]; then
echo "it is a directory"
else
echo "missing"
fi
[[ "$name" == "Ada" ]] && echo "string match"
[[ $count -gt 2 ]] && echo "numeric comparison"
(( count > 2 )) && echo "arithmetic form"
[[ -z "$name" ]] && echo "empty string"
[[ -n "$name" ]] && echo "non-empty string"
-fregular file,-ddirectory,-eexists,-r/-w/-xreadable, writable, executable.==and!=compare strings;-eq -ne -lt -le -gt -gecompare integers.caseis cleaner than a chain ofelifwhen matching one value against patterns.
case "$1" in
start) echo "starting" ;;
stop) echo "stopping" ;;
*) echo "unknown command: $1" >&2; exit 2 ;;
esac
Loops
# Over a list of words
for env in dev test prod; do
echo "deploying to $env"
done
# Over files, safely, even with spaces in names
for f in /var/log/*.log; do
[[ -e "$f" ]] || continue
echo "$f: $(wc -l < "$f") lines"
done
# Counting
for (( i = 1; i <= 5; i++ )); do
echo "attempt $i"
done
# Read a file line by line
while IFS= read -r line; do
echo "got: $line"
done < hosts.txt
Do not loop over the output of ls or find with a plain for: file names with spaces break apart. Use a glob as above, or find ... -print0 | while IFS= read -r -d '' f for recursive searches.
Functions
Functions group commands under a name. Arguments arrive as $1, $2 and $@, exactly like a script. Use local for variables inside functions so they do not leak out, and return an exit code (0 for success) rather than a value; print values to stdout and capture them with $(...).
log() {
printf '[%s] %s\n' "$(date '+%H:%M:%S')" "$*" >&2
}
is_root() {
[[ $EUID -eq 0 ]]
}
backup_name() {
local base=$1
echo "${base}-$(date +%Y%m%d).tar.gz"
}
is_root || { log "run as root"; exit 1; }
archive=$(backup_name "site")
log "writing $archive"
Exit codes and errors
Every command sets $? to 0 on success or 1 to 255 on failure. Scripts should follow the same rule: exit 0 when the job succeeded, non-zero otherwise, so that cron, CI pipelines and other scripts can react. With set -e you rarely need to check $? by hand; when a failure is acceptable, say so explicitly with || true.
grep -q "ERROR" app.log || true # not finding anything is fine
if ! cp "$src" "$dst"; then
echo "copy failed" >&2
exit 1
fi
trap 'echo "failed at line $LINENO" >&2' ERR
Careful: rm -rf "$dir/"* with an unset or empty dir expands to rm -rf /*. set -u turns the unset case into an error, which is one of the main reasons to use it. Run ShellCheck on every script before you trust it; it catches quoting mistakes and dozens of other classic bugs.
Official documentation
Did this work for you?
No votes yet. Be the first.
If it did not work, the report form below tells the editors what went wrong.
See also
- 0049 Pipes and redirection: connecting commands A pipe sends one command's output into another command, and redirection sends output to a file. Together they let small tools build… Beginner · Explains an idea · Linux, macOS Article Basics · Text processing 15 Oct 2026
- 0050 File permissions: chmod, chown and what 755 means Every file has an owner, a group and read/write/execute permissions for three kinds of users. chmod changes the permissions and chown changes… Beginner · Explains an idea · Linux, macOS Article Basics 15 Oct 2026
- 0051 Environment variables and PATH Environment variables are named settings that every program inherits from the shell. PATH is the one that tells the shell where to… Beginner · Explains an idea · Linux, macOS Article Basics 15 Oct 2026
- 0057 Shell cheat sheet: everyday commands A quick reference for moving around, managing files, viewing and searching text, checking processes and disk space in bash and zsh, with… Beginner · Cheat sheet · Linux, macOS Article Basics · Command-line tools 15 Oct 2026
- 0058 Safe bash script template with set ‑euo pipefail and trap A starting point for bash scripts that stops on errors, rejects unset variables, cleans up temp files on exit and parses options.… Intermediate · Cheat sheet · Linux, macOS Script Shell scripting 15 Oct 2026
- 0059 Incremental snapshot backups with rsync A bash script that copies a folder into dated snapshots with rsync, hard-links unchanged files to the previous snapshot to save space,… Advanced · Step by step · Linux, macOS Script Backups & syncing · Shell scripting 15 Oct 2026
History
Every saved edit is kept. The plot on the sleeve draws one line per edit. Open any edit to see exactly what changed.
- Dominik +5,257 chars