Bash scripting reference: things I always forget
A running collection of bash patterns I keep needing to look up. Not a tutorial — just the parts that don't stick in my head for whatever reason. Still adding to this.
Start every script with this
#!/usr/bin/env bash
set -euo pipefail
-e— exit immediately if any command fails (non-zero exit code)-u— treat unset variables as errors-o pipefail— a pipeline fails if any command in it fails, not just the last one
set -e alone isn't enough because pipelines like
grep foo file | wc -l will mask a failing grep.
Variables and quoting
# Assignment — no spaces around =
NAME="alice"
COUNT=0
# Always quote variables in strings
echo "Hello, $NAME"
cp "$SOURCE" "$DEST" # handles spaces in paths
# Braces are optional but good habit for clarity
echo "${NAME}s" # "alices" — without braces: "$NAMEs" breaks
# Default values
: "${CONFIG_FILE:=/etc/myapp/config}" # set if unset or empty
echo "${DEBUG:-false}" # use default without setting
# Fail with message if unset
: "${REQUIRED_VAR:?REQUIRED_VAR must be set}"
String manipulation
# Length
echo "${#NAME}"
# Substring: ${var:offset:length}
STR="hello world"
echo "${STR:6:5}" # "world"
# Remove prefix (shortest match)
FILE="path/to/file.tar.gz"
echo "${FILE#*/}" # "to/file.tar.gz"
# Remove prefix (longest match)
echo "${FILE##*/}" # "file.tar.gz" (same as basename)
# Remove suffix (shortest match)
echo "${FILE%.*}" # "path/to/file.tar"
# Remove suffix (longest match)
echo "${FILE%%.*}" # "path/to/file"
# Substitution
echo "${FILE/.tar.gz/.bak}" # replace first match
echo "${FILE//l/L}" # replace all matches
# Case conversion (bash 4+)
echo "${NAME^^}" # ALICE
echo "${NAME,,}" # alice
Conditionals
if [[ -f "$FILE" ]]; then
echo "file exists"
elif [[ -d "$FILE" ]]; then
echo "it's a directory"
else
echo "not found"
fi
Use [[ ]] over [ ] in bash scripts — it handles
empty variables and patterns without surprises.
Common test expressions
| Expression | True if |
|---|---|
-f "$f" | regular file exists |
-d "$f" | directory exists |
-e "$f" | anything exists at path |
-r "$f" | file is readable |
-w "$f" | file is writable |
-x "$f" | file is executable |
-s "$f" | file exists and is non-empty |
-z "$s" | string is empty |
-n "$s" | string is non-empty |
"$a" = "$b" | strings are equal |
"$a" != "$b" | strings are not equal |
"$a" =~ regex | string matches regex (bash only) |
$a -eq $b | integers are equal |
$a -lt $b | integer a is less than b |
$a -ge $b | integer a is greater than or equal to b |
Loops
# Iterate over a list
for item in one two three; do
echo "$item"
done
# Iterate over files
for f in /etc/*.conf; do
echo "Processing $f"
done
# C-style loop
for (( i=0; i<10; i++ )); do
echo "$i"
done
# While loop
while IFS= read -r line; do
echo "Line: $line"
done < /path/to/file
# Infinite loop with break
while true; do
[[ -f /tmp/stop ]] && break
sleep 5
done
Functions
log() {
local level="$1"
shift
echo "[$(date '+%H:%M:%S')] [$level] $*" >&2
}
log INFO "Starting up"
log ERROR "Something went wrong"
localscopes variables to the function — use it to avoid clobbering globals- Functions return exit codes, not values. Capture output with
$(funcname args) $*joins all args as a single string;"$@"preserves each arg separately (usually want"$@")
Useful patterns
# Check if a command exists
if ! command -v jq >/dev/null 2>&1; then
echo "jq is not installed" >&2
exit 1
fi
# Get the directory of the current script (not $PWD)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Temp file that cleans up on exit
TMPFILE=$(mktemp)
trap 'rm -f "$TMPFILE"' EXIT
# Run something only if root
if [[ $EUID -ne 0 ]]; then
echo "Must be run as root" >&2
exit 1
fi
# Parse simple flags
VERBOSE=false
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) VERBOSE=true; shift ;;
--) shift; break ;;
*) break ;;
esac
done