Bash quick reference

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)
  • -e exit on error, -u error on unset vars, -o pipefail fails a pipeline if any step fails, -E preserves ERR in functions/subshells.
  • Use printf over echo for 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/builtin test), 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/-x respect real UID/GID, not ACL nuances.
  • -t 0 is 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 local inside functions.
  • declare -g name=value sets 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 pipefail with pipelines. cmd1 | cmd2 fails if either fails (under pipefail).
  • 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 makes getopts “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

  • printf formatting: printf '%-10s %6d\n' "label" 42
  • read -r -p 'Prompt: ' var with -s for silent (password).
  • mapfile -t lines < file (aka readarray).
  • type cmd shows if builtin, keyword, function, or file.
  • time cmd simple timing.

Performance & safety notes

  • Always quote expansions: "$var", "$@".
  • Avoid parsing ls; iterate globs or use find.
  • Use while IFS= read -r for files; don’t use for 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]]