Bash quick reference
A compact cheat-sheet for day-to-day Bash scripting. Examples assume Bash ≥ 4 unless noted.
Basics
#!/usr/bin/env bash
set -Eeuo pipefail # strict mode
IFS=$'\n\t' # safer field splitting
# chmod +x script.sh && ./script.sh
# shellcheck script.sh # static analysis (recommended)
set -x # xtrace: print commands before executing (use `set +x` to disable)
-eexit on error,-uerror on unset vars,-o pipefailfails a pipeline if any step fails,-Epreserves ERR in functions/subshells.- Use
printfoverechofor predictable output.
Variables & quoting
name="Ada" # no spaces around =
export PATH="/usr/bin:$PATH"
readonly BUILD="prod" # or: declare -r BUILD
declare -i n=42 # integer attribute
declare -a arr=(a b c) # indexed array
declare -A map=([k]=v) # associative array (Bash ≥ 4)
printf '%s\n' "$name" # always quote expansions
cmd_out="$(date)" # preferred (nestable) command substitution
cmd_out=`date` # legacy backticks; harder to nest, avoid
sum=$((2 + 3 * n)) # arithmetic
Special params: $? exit code, $$ PID, $! last bg PID, $0 script path, $- shell options, $PWD, $OLDPWD.
Script arguments (positional params)
# ./script.sh foo bar
echo "$0" # script name
echo "$1" # 'foo'
echo "$#" # count
for x in "$@"; do printf '[%s]\n' "$x"; done # iterate args safely
shift 2 # drop first two args
"$@"→ each arg as its own word;"$*"→ all args as one string.
Parameter expansion (power tools)
: "${REQ:?REQ must be set}" # require var or exit
user="${USER:-nobody}" # default
greet="${name:+hello}" # substitute if set/non-empty
len=${#name} # length
sub=${name:1:2} # slice
path="/tmp/a.tar.gz"
base=${path##*/} # rm longest prefix pattern -> a.tar.gz
stem=${base%.tar.gz} # rm shortest suffix pattern -> a
new=${name/da/DA} # first match
all=${name//a/A} # all matches
up=${name^^} # uppercase (Bash ≥ 4)
low=${name,,} # lowercase
indirect=${!VAR_NAME} # indirection
Tests & conditionals
Brackets at a glance
[…]POSIX test (external/builtintest), subject to globbing/word splitting.[[…]]Bash keyword; no word splitting or globbing on unquoted vars; supports regex; safer.((…))arithmetic evaluation (C-style).{ …; }command group in current shell (affects vars).( … )subshell group (vars don’t leak out).{a..z}brace expansion (performed before parameter expansion).
if, file, string, number, regex
if [[ -f "$file" && -r "$file" ]]; then
:
elif [[ -d "$dir" ]]; then
:
else
:
fi
Regex specifics
if [[ $s =~ ^([A-Za-z_][A-Za-z0-9_]*)$ ]]; then
ident="${BASH_REMATCH[1]}"
fi
# WRONG: quotes disable regex parsing on the right-hand side
# if [[ $s =~ "^pattern$" ]]; then …
# RIGHT:
# if [[ $s =~ ^pattern$ ]]; then …
- Do not quote the regex on the right side of =~; quoting turns it into a literal string match.
- Use
^…$to anchor; the engine is ERE, not PCRE.
Common tests
Use [[ … ]] for Bash-specific tests (safer, no pathname expansion), [ … ] (aka test) for POSIX portability, and (( … )) for arithmetic. Combine with &&, ||, and !.
File tests
Use with a pathname in [[ … ]] or [ … ].
| Test | Meaning |
|---|---|
-e file |
Exists (any type) |
-f file |
Regular file |
-d file |
Directory |
-L file / -h file |
Symbolic link |
-s file |
Size > 0 bytes |
-r file |
Readable by current user |
-w file |
Writable by current user |
-x file |
Executable/searchable by current user |
-O file |
Owned by current user |
-G file |
Owned by user’s effective group |
-N file |
Modified since last read |
-b file |
Block device |
-c file |
Character device |
-p file |
Named pipe (FIFO) |
-S file |
Socket |
file1 -nt file2 |
file1 newer than file2 (mtime) |
file1 -ot file2 |
file1 older than file2 |
file1 -ef file2 |
Same inode (hard link to same file) |
-t fd |
FD is a terminal (e.g. -t 1 for stdout) |
Examples:
if [[ -d $dir && -w $dir ]]; then echo "can write to dir"; fi
if [[ -L $p && ! -e $p ]]; then echo "broken symlink"; fi
if [[ $a -nt $b ]]; then echo "$a is newer than $b"; fi
Notes:
-r/-w/-xrespect real UID/GID, not ACL nuances.-t 0is useful to detect interactive stdin.
String tests (in [[ … ]])
| Test | Meaning |
|---|---|
-z "$s" |
Empty string |
-n "$s" |
Non-empty string |
$a == $b |
Equal; right side does glob matching unless quoted |
$a = $b |
Same as == |
$a != $b |
Not equal |
$a < $b |
Lexicographic less-than |
$a > $b |
Lexicographic greater-than |
$s =~ regex |
Regex match (ERE) |
Examples:
if [[ -z $USER ]]; then echo "no user"; fi
if [[ $mode == dev* ]]; then echo "dev mode"; fi # pattern match
if [[ $a == "$b" ]]; then echo "literal compare"; fi # quote to avoid globbing
if [[ $ver1 < $ver2 ]]; then echo "ver1 sorts before ver2"; fi
Regex specifics:
if [[ $s =~ ^([A-Za-z_][A-Za-z0-9_]*)$ ]]; then
ident="${BASH_REMATCH[1]}" # capture groups in BASH_REMATCH
fi
- Don’t quote the regex on the right side of =~ (quoting disables regex).
- Use
^…$to anchor; it’s an ERE, not PCRE.
Portability tip: in POSIX [ … ], < and > are redirection operators; write "\<" and "\>" or use test with escaped operators, but prefer [[ … ]] in Bash.
Integer tests
Use either test-style with -eq etc., or arithmetic context.
In [ or [[ |
In (( … )) |
Meaning |
|---|---|---|
a -eq b |
a == b |
Equal |
a -ne b |
a != b |
Not equal |
a -lt b |
a < b |
Less than |
a -le b |
a <= b |
Less or equal |
a -gt b |
a > b |
Greater than |
a -ge b |
a >= b |
Greater or equal |
Examples:
if (( count > 10 )); then echo "big"; fi
if [[ $n -gt 0 && $n -le 100 ]]; then echo "1..100"; fi
Boolean operators:
if [[ cond1 && cond2 || ! cond3 ]]; then …; fi
Loops
for x in a b c; do printf '%s\n' "$x"; done
for ((i=0; i<10; i++)); do echo "$i"; done # arithmetic for
while read -r line; do # safe line read
printf '%s\n' "$line"
done < file.txt
until ping -c1 host >/dev/null 2>&1; do sleep 1; done
select opt in start stop quit; do echo "$opt"; break; done # simple menus
Control: break, continue, break 2 (outer level).
Functions
say() { # function name() { … } is equivalent
local msg=${1:-"hi"} # local scope
printf '%s\n' "$msg"
}
say "hello"
- Return status via
return <code>(0–255). For data, print to stdout and capture with$(…). - Globals are default; prefer
localinside functions. declare -g name=valuesets a global from inside a function.
Case statement
case "$mode" in
start) echo "starting";;
stop) echo "stopping";;
*) echo "usage: …"; exit 2;;
esac
Arrays
arr=(alpha beta gamma)
echo "${arr[0]}" # first
echo "${#arr[@]}" # length
printf '%s\n' "${arr[@]}" # iterate
arr+=("delta") # append
unset 'arr[1]' # remove element (hole remains)
arr=("${arr[@]/beta/BETA}") # element-wise substitution
declare -A map=([host]=db1 [port]=5432)
echo "${map[host]}"
printf '%s=%s\n' "${!map[@]}" "${map[@]}" # keys then values
I/O, redirection, and pipelines
cmd >out.txt # stdout
cmd >>out.txt # append
cmd 2>err.txt # stderr
cmd >out 2>&1 # both to out
cmd &>all.txt # Bash: stdout+stderr
cmd < infile
cat <<'EOF' # here-doc (single quotes => no expansion)
literal $text
EOF
grep foo <<< "$text" # here-string
diff <(sort a) <(sort b) # process substitution
- Use
set -o pipefailwith pipelines.cmd1 | cmd2fails if either fails (underpipefail). - Short-circuit:
cmd || echo "failed";cmd && echo "ok".
File descriptors
exec 3< input.txt
read -r line <&3
exec 3>&- # close FD 3
Grouping, subshells, and scope
{ echo "in group"; var=1; } # same shell, var persists
( echo "in subshell"; var=2 ) # child shell, var discarded
Globbing & shopt
shopt -s nullglob dotglob # no literal “*.txt” if no matches; include dotfiles
shopt -s extglob # extended globs
# rm !(*.md|*.txt) # everything except .md or .txt (be careful)
getopts (flags parsing)
usage() { printf 'Usage: %s [-f file] [-v]\n' "$0"; }
verbose=0; file=''
while getopts ':f:vh' opt; do
case "$opt" in
f) file=$OPTARG ;;
v) verbose=1 ;;
h) usage; exit 0 ;;
\?) printf 'Unknown option: -%s\n' "$OPTARG"; usage; exit 2 ;;
:) printf 'Missing arg for -%s\n' "$OPTARG"; usage; exit 2 ;;
esac
done
shift $((OPTIND - 1))
:at start makesgetopts“silent”; handle errors yourself.- After parsing, remaining args start at
$1.
Traps & cleanup
tmpdir="$(mktemp -d)"
cleanup() { rm -rf "$tmpdir"; }
trap cleanup EXIT INT TERM
trap 'echo "ERR at $BASH_SOURCE:$LINENO"; exit 1' ERR
Useful builtins
printfformatting:printf '%-10s %6d\n' "label" 42read -r -p 'Prompt: ' varwith-sfor silent (password).mapfile -t lines < file(akareadarray).type cmdshows if builtin, keyword, function, or file.time cmdsimple timing.
Performance & safety notes
- Always quote expansions:
"$var","$@". - Avoid parsing
ls; iterate globs or usefind. - Use
while IFS= read -rfor files; don’t usefor line in $(cat file). - Prefer
[[ … ]]to[for conditionals. - Validate inputs early; use
set -Eeuo pipefail. - Keep functions small; test with
shellcheck.
Handy snippets
Join array with delimiter
join_by() { local IFS=$1; shift; printf '%s\n' "$*"; }
join_by ',' "${arr[@]}"
Retry with backoff
retry() {
local tries=$1; shift
local delay=1
until "$@"; do
((tries--)) || return 1
sleep "$delay"; delay=$((delay*2))
done
}
retry 5 curl -fsS https://example.com
Read key=val file into map
declare -A cfg
while IFS='=' read -r k v; do
[[ $k && ${k:0:1} != '#' ]] && cfg[$k]=$v
done < config.env
Timer
start=$SECONDS
# … work …
printf 'Elapsed: %ss\n' "$((SECONDS - start))"
Links
- [[2025-W43]]