Skip to main content

Bash Cheatsheet — 100+ Commands and Idioms for Shell Scripting on Linux and macOS

Bash cheat sheet — 100+ commands & idioms for variables, conditionals, loops, functions, pipes, traps, with real one-liners.

  • Runs locally
  • Category Developer & DevOps
  • Best for Formatting, validating, shrinking, or inspecting code-adjacent text.
Section:
198 commands
Variables (26)
name=value

Assign a variable. NO spaces around `=` — `name = value` is a command call, not an assignment.

Common pitfall: `x = 1` calls a program named `x` with args `=` and `1`. Bash will say `x: command not found` and you will spend ten minutes wondering why.

Examples
name="Lei Li"
count=42
today=$(date +%F)
export NAME=value

Mark a variable for export to child processes. Without `export`, the variable stays in the current shell only.

Examples
export PATH="$HOME/bin:$PATH"
export EDITOR=vim
"${var}"

Quoted expansion with explicit braces. The braces let you stick text right after the variable name without ambiguity.

Common pitfall: Without braces, `$prefix_v1` looks like the variable `prefix_v1`, not `$prefix` followed by `_v1`. Write `"${prefix}_v1"`.

Examples
echo "Hello, ${name}!"
file="${base}_${date}.log"
${var:-default}

Use $var if set and non-empty; otherwise substitute `default`. Does NOT modify $var.

Examples
port="${PORT:-8080}"
name="${1:-world}"   # default first arg
${var:=default}

Same as `:-` but also ASSIGNS the default back into $var. Does not work on positional args ($1, $2…).

Examples
: "${LOG_DIR:=/var/log/app}"   # ensure LOG_DIR is set
${var:?error message}

If $var is unset or empty, print the message to stderr and exit. Great as a hard guard at the top of a script.

Examples
: "${API_KEY:?need to set API_KEY in env}"
${var:+alt}

If $var is set and non-empty, substitute `alt`; otherwise substitute nothing. Useful for "only add this flag if X was set".

Examples
args="${DEBUG:+--verbose}"   # add --verbose only if DEBUG is set
${var//old/new}

Replace EVERY occurrence of `old` with `new` in $var. Single slash replaces only the first.

Examples
path="${path//\\//}"   # backslashes to forward slashes
safe="${name// /_}"
${var#prefix} / ${var##prefix}

Strip the shortest (#) or longest (##) matching `prefix` from the front of $var. Patterns are globs, not regex.

Examples
file=/path/to/file.tar.gz
echo "${file##*/}"   # file.tar.gz
echo "${file#*/}"    # path/to/file.tar.gz
${var%suffix} / ${var%%suffix}

Strip the shortest (%) or longest (%%) matching `suffix` from the end of $var. Mirror of # and ##.

Examples
file=archive.tar.gz
echo "${file%.gz}"     # archive.tar
echo "${file%%.*}"     # archive
${#var}

Length (in characters) of $var.

Examples
s="hello"
echo "${#s}"   # 5
${var:start:length}

Substring of $var: skip `start` chars, take `length` chars. Both numbers can be expressions.

Examples
s="abcdefgh"
echo "${s:2:3}"   # cde
echo "${s:(-3)}"  # fgh (last 3)
${var^^} / ${var,,}

Upper-case (^^) or lower-case (,,) all characters of $var. Bash 4+ only.

Common pitfall: macOS ships Bash 3.2 (license reasons). On macOS you need `brew install bash` to get 4+, or fall back to `tr`.

Examples
s="hello"
echo "${s^^}"   # HELLO
echo "${s,,}"   # hello (already)
$RANDOM

Bash built-in that yields a random integer 0–32767 each time it is referenced.

Examples
echo $RANDOM
echo $((RANDOM % 100))   # 0-99
tmp="/tmp/job-$RANDOM"
readonly NAME=value

Declare a constant — any later assignment to NAME will fail. Use for config you really do not want overwritten.

Examples
readonly VERSION=1.2.3
readonly LOG_DIR=/var/log/app
unset name

Delete a variable entirely. After `unset x`, both `$x` and `${x:-}` expand to empty.

Examples
unset TMPDIR
unset -f my_function   # also unsets a function
${var/old/new}

Replace only the FIRST occurrence of `old` with `new`. Double slash `//` replaces all.

Examples
p="a.b.c"
echo "${p/./_}"   # a_b.c
echo "${p//./_}"  # a_b_c
${var/#prefix/new}

Anchored replace: `/#` only matches at the START of the string, `/%` only at the END.

Examples
s="test.log"
echo "${s/#test/prod}"   # prod.log
echo "${s/%.log/.txt}"   # test.txt
${!prefix*} / ${!prefix@}

Indirect: expand to the NAMES of all variables whose name starts with `prefix`. Handy for config introspection.

Examples
APP_HOST=a; APP_PORT=b
for v in "${!APP_@}"; do echo "$v=${!v}"; done
${!ref}

Indirect expansion: if `ref=path`, then `${!ref}` expands to the value of $path. One level of pointer.

Examples
path="/usr/bin"
ref="path"
echo "${!ref}"   # /usr/bin
declare -i num

Mark a variable as integer. Assignments are then evaluated as arithmetic; `num=2+3` stores 5, not the string.

Examples
declare -i n
n="3 * 4"
echo "$n"   # 12
declare -r NAME=value

Declare a read-only variable. Same effect as `readonly` but in the unified `declare` syntax.

Examples
declare -r MAX=100
MAX=200   # bash: MAX: readonly variable
${@:start:count}

Slice positional parameters: take `count` args starting from position `start`. `${@:2}` drops the first arg.

Examples
# args: a b c d
echo "${@:2}"     # b c d
echo "${@:2:2}"   # b c
local -n ref=name

Nameref: make `ref` an alias for the variable named in `name`. Lets a function write to a caller variable. Bash 4.3+.

Examples
fill() { local -n out=$1; out=(x y z); }
fill result
echo "${result[@]}"   # x y z
BASH_VERSINFO[0]

Major version of the running bash as an integer. Gate features without parsing $BASH_VERSION strings.

Examples
if (( BASH_VERSINFO[0] < 4 )); then
  echo "need bash 4+" >&2; exit 1
fi
$LINENO / $FUNCNAME

$LINENO is the current line number; $FUNCNAME[0] is the running function name. Build precise log/error prefixes with them.

Examples
die() { echo "[$FUNCNAME:${BASH_LINENO[0]}] $*" >&2; exit 1; }
Conditionals (20)
if [[ condition ]]; then ... fi

Bash conditional. Prefer `[[ ]]` over the older `[ ]` — it handles spaces, regex, and globbing without surprises.

Examples
if [[ -f config.yaml ]]; then echo "found"; fi
if [[ "$user" == "root" ]]; then echo "boss"; fi
if [[ "$a" == "$b" ]]

String equality. `==` and `=` both work inside `[[ ]]`. Always quote the right side unless you want glob matching.

Common pitfall: Inside `[[ ]]`, an UNQUOTED right side becomes a glob: `[[ $f == *.log ]]` matches any file ending in .log. Quote it to compare literally.

Examples
if [[ "$name" == "alice" ]]; then ... fi
if [[ "$file" == *.log ]]; then ...   # glob match, no quotes
if [[ "$a" != "$b" ]]

String inequality. Symmetric to `==`.

Examples
if [[ "$env" != "prod" ]]; then echo "safe to delete"; fi
if [[ "$s" =~ regex ]]

Regex match. The regex is bash ERE; captures land in BASH_REMATCH.

Common pitfall: Do NOT quote the regex — quoting turns metacharacters into literals. `[[ $s =~ "^[0-9]+$" ]]` matches the literal string, not digits.

Examples
if [[ "$ip" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "looks like ipv4"; fi
if [[ "$s" =~ ^([a-z]+)=([0-9]+)$ ]]; then echo "${BASH_REMATCH[1]}=${BASH_REMATCH[2]}"; fi
if (( a < b ))

Arithmetic comparison. Use `(( ))` for numeric work — `<`, `>`, `==`, `!=`, `<=`, `>=` all behave like C.

Examples
if (( count > 10 )); then echo "many"; fi
if (( a + b == 100 )); then echo "match"; fi
[[ "$a" -lt "$b" ]]

POSIX-style numeric test inside `[[ ]]`. Operators: -lt -le -gt -ge -eq -ne. Use these when comparing numbers as strings.

Common pitfall: `[[ "5" > "10" ]]` is TRUE — string comparison says "5" sorts after "1". Use `-gt` or `(( 5 > 10 ))` for numeric intent.

Examples
if [[ "$age" -ge 18 ]]; then echo "adult"; fi
[[ -f file ]] / [[ -d dir ]] / [[ -e path ]]

File tests. -f: regular file. -d: directory. -e: any path exists (file, dir, socket, link target). -L: symlink.

Examples
[[ -f /etc/hosts ]] && echo "exists"
[[ -d "$HOME/.config" ]] || mkdir -p "$HOME/.config"
[[ -e /tmp/lock ]] && exit 1
[[ -z "$s" ]] / [[ -n "$s" ]]

-z: string is empty (zero length). -n: string is non-empty.

Common pitfall: ALWAYS quote: `[[ -z $s ]]` breaks if $s contains spaces. `[[ -z "$s" ]]` is safe.

Examples
[[ -z "$NAME" ]] && NAME="anon"
if [[ -n "$DEBUG" ]]; then set -x; fi
cmd && echo ok || echo fail

Short-circuit chain. && runs the next command only if the previous succeeded (exit 0); || only if it failed.

Common pitfall: `cmd && a || b` is NOT `if cmd then a else b`. If `a` itself fails, `b` runs too. Use a real `if` for branching.

Examples
ping -c1 host && echo "up" || echo "down"
[[ -d build ]] || mkdir build
if ! cmd; then ... fi

Negate a command exit status. `!` flips success/failure for use in `if` / `while`.

Examples
if ! grep -q "ok" status.log; then echo "missing ok line"; fi
while ! curl -fs http://app/health; do sleep 1; done
case "$var" in pattern) ... ;; esac

Multi-way branch on glob patterns. Cheaper and clearer than a tower of elif when matching strings.

Examples
case "$1" in
  start) start_service ;;
  stop)  stop_service ;;
  *)     echo "usage: $0 start|stop"; exit 1 ;;
esac
[[ "$a" && "$b" ]]

Logical AND inside a single `[[ ]]`. Use `&&` (not `-a`) — `[[ ]]` is bash, not POSIX test.

Examples
if [[ -f config && -r config ]]; then echo "readable"; fi
[[ file1 -nt file2 ]]

True if file1 is NEWER THAN file2 (by modification time), or file1 exists and file2 does not. `-ot` is older-than.

Examples
if [[ src.c -nt build/src.o ]]; then
  echo "need rebuild"
fi
[[ -r f ]] / [[ -w f ]] / [[ -x f ]]

Permission tests for the current user: -r readable, -w writable, -x executable (or searchable for a dir).

Examples
[[ -x "$(command -v git)" ]] && echo "git runnable"
[[ -w /etc/hosts ]] || echo "need sudo to edit hosts"
[[ -s file ]]

True if file exists AND has size greater than zero. The clean way to check "did the output actually get written".

Examples
curl -o out.json "$url"
[[ -s out.json ]] || { echo "empty download" >&2; exit 1; }
[[ -t 1 ]]

True if file descriptor 1 (stdout) is attached to a terminal. Use it to enable colors only when not piped/redirected.

Examples
if [[ -t 1 ]]; then COLOR=1; else COLOR=0; fi
[[ -v var ]]

True if `var` is SET (even if empty). Distinguishes "unset" from "set to empty string". Bash 4.2+.

Examples
x=""
[[ -v x ]] && echo "x is set (but empty)"
[[ -v y ]] || echo "y is unset"
case with | (alternation)

A single case branch can match several patterns with `|`. Cleaner than repeating branches.

Examples
case "$ans" in
  y|Y|yes) proceed ;;
  n|N|no)  abort ;;
  *)       echo "?" ;;
esac
shopt -s nocasematch

Make `[[ ]]` and `case` matching case-INSENSITIVE for the rest of the shell. Turn it back off with `shopt -u`.

Examples
shopt -s nocasematch
[[ "HELLO" == hello ]] && echo "match"
shopt -u nocasematch
[[ "$a" < "$b" ]]

Lexicographic (dictionary order) string comparison inside `[[ ]]`. Sorts by the current locale collation.

Common pitfall: In `[[ ]]`, `<` and `>` are string operators and need no escaping. In `[ ]` (single bracket) you must write `\<` or it becomes redirection.

Examples
[[ "apple" < "banana" ]] && echo "apple first"
Loops (19)
for x in a b c; do ... done

Loop over a literal list of words. Each iteration assigns one word to $x.

Examples
for env in dev staging prod; do
  echo "deploying $env"
done
for f in *.log; do ... done

Loop over filenames matching a glob. Handles spaces correctly — no word-splitting on whitespace inside filenames.

Common pitfall: If no files match, the glob expands to the literal pattern `*.log` and your loop body sees `f="*.log"`. Guard with `shopt -s nullglob` to skip the loop on no matches.

Examples
shopt -s nullglob
for f in *.log; do
  gzip "$f"
done
for ((i=0; i<10; i++)); do ... done

C-style for loop. Use for indexed iteration when you need the counter itself.

Examples
for ((i=1; i<=5; i++)); do
  echo "attempt $i"
done
for x in $(cmd); do ... done

Iterate over words from command output. Output is split on $IFS (default: spaces, tabs, newlines).

Common pitfall: Filenames with spaces break this. Prefer `while read -r line` with redirection for lines, or set IFS=$'\n' for newline-only splitting.

Examples
for pid in $(pgrep nginx); do
  echo "nginx pid: $pid"
done
while read -r line; do ... done < file

Read a file line by line, safely. `-r` disables backslash escapes; the < at the end feeds the loop without subshell traps.

Common pitfall: If you write `cat file | while read line`, the loop runs in a subshell and variables you set inside DIE when the loop ends. Use `done < file` instead.

Examples
while read -r line; do
  echo "got: $line"
done < urls.txt
while [[ condition ]]; do ... done

Loop while a condition stays true. Common for retry loops and queue draining.

Examples
attempts=0
while ! curl -fs http://app/health; do
  ((attempts++))
  (( attempts > 30 )) && exit 1
  sleep 1
done
until [[ condition ]]; do ... done

Loop UNTIL the condition becomes true (i.e. while it is false). Symmetric to `while`.

Examples
until ping -c1 -W1 host >/dev/null 2>&1; do sleep 1; done
break / continue

`break` exits the innermost loop right now. `continue` skips to the next iteration. `break 2` exits two levels.

Examples
for f in *; do
  [[ -d "$f" ]] || continue
  [[ "$f" == "stop" ]] && break
  echo "$f"
done
select x in a b c; do ... done

Interactive numbered menu. Bash prints the choices, the user picks a number, $x gets the value. Loops until you `break`.

Examples
select env in dev staging prod quit; do
  [[ "$env" == "quit" ]] && break
  echo "you chose $env"
done
for x in {1..10}; do ... done

Brace expansion gives a numeric range. {1..10..2} skips by 2. Expanded BEFORE variable substitution.

Common pitfall: `{1..$n}` does NOT work because brace expansion happens before $n is read. Use `seq $n` or a C-style for loop.

Examples
for i in {1..5}; do echo "$i"; done
for i in {0..100..10}; do echo "$i"; done
while IFS=, read -r a b c; do ... done < file.csv

Parse a CSV line by line: set IFS to comma, then read splits the line into named fields.

Examples
while IFS=, read -r name age role; do
  echo "$name ($age) is a $role"
done < team.csv
for i in $(seq 1 $n); do ... done

Loop a runtime-computed number of times. Use `seq` when the count comes from a variable (brace `{1..$n}` will not expand).

Examples
n=5
for i in $(seq 1 "$n"); do echo "$i"; done
while :; do ... done

Infinite loop. `:` is the built-in no-op that always returns true. Exit with `break` or a guarded condition inside.

Examples
while :; do
  read -r line || break
  echo "got: $line"
done
for f in dir/*/; do ... done

Loop over SUBDIRECTORIES only. The trailing slash makes the glob match directories (and symlinks to dirs).

Examples
for d in /var/log/*/; do
  echo "dir: ${d%/}"
done
while read -r a b rest; do ... done

Read splits each line into fields by $IFS; the LAST named variable soaks up everything remaining. Great for "first two columns + rest".

Examples
while read -r user uid rest; do
  echo "$user has uid $uid"
done < <(getent passwd)
find ... -print0 | while IFS= read -r -d "" f

Iterate find results SAFELY for any filename. -print0 separates with NUL; -d "" reads up to NUL. Handles spaces and newlines in names.

Common pitfall: A plain `for f in $(find ...)` breaks on spaces and newlines in filenames. The NUL-delimited pattern is the robust one.

Examples
find . -name "*.tmp" -print0 | while IFS= read -r -d "" f; do
  rm -- "$f"
done
for x in "${arr[@]}"; do ... done

Iterate an array element by element, quote-safe. Each element stays one word even with embedded spaces.

Examples
files=("a b.txt" "c.txt")
for f in "${files[@]}"; do echo "[$f]"; done
continue N

`continue N` skips to the next iteration of the N-th ENCLOSING loop. `continue 2` jumps the outer loop forward.

Examples
for i in 1 2 3; do
  for j in a b c; do
    [[ "$j" == b ]] && continue 2
    echo "$i$j"
  done
done
coproc NAME { cmd; }

Start a co-process: cmd runs in the background with its stdin/stdout wired to file descriptors in ${NAME[1]} / ${NAME[0]}.

Examples
coproc BC { bc -l; }
echo "2^10" >&"${BC[1]}"
read -r ans <&"${BC[0]}"
echo "$ans"   # 1024
Functions (14)
name() { ...; }

Define a function. The `function` keyword is optional in bash; the POSIX form `name()` is more portable.

Examples
greet() {
  echo "Hello, $1!"
}
greet "Lei"   # → Hello, Lei!
function name { ...; }

Bash-only function form. Identical to `name() { ... }` but with the keyword.

Examples
function deploy {
  echo "deploying $1 to $2"
}
$1 $2 ... $9 ${10}

Positional arguments inside a function or script. $1 is the first, $0 is the script/function name.

Common pitfall: Two-digit args need braces: `$10` reads as `$1` followed by literal `0`. Use `${10}` for the tenth arg.

Examples
copy() {
  cp "$1" "$2"
}
"$@" vs "$*"

"$@" preserves each argument as a separate word (the usual right answer). "$*" joins them with the first char of $IFS.

Common pitfall: Without quotes, both behave the same — and both break on spaces. ALWAYS use the quoted form `"$@"` when forwarding args.

Examples
run_with_logging() {
  echo "[$(date)] running: $*"
  "$@"
}
local var=value

Declare a function-local variable. Without `local`, every assignment in a function pollutes the global scope.

Examples
greet() {
  local who="$1"
  echo "Hello, $who"
}
return N

Set the function exit status. 0 = success, 1-255 = failure. NOT the same as a function "return value" — for data use stdout + $(fn).

Examples
is_valid() {
  [[ "$1" =~ ^[0-9]+$ ]] && return 0 || return 1
}
if is_valid "$x"; then echo ok; fi
result=$(my_function arg1 arg2)

Capture function output. Functions return data the same way commands do: print to stdout, caller wraps in $(...).

Examples
get_timestamp() { date +%s; }
now=$(get_timestamp)
echo "now is $now"
declare -f name

Print the definition of a function. `declare -F` lists names only. Useful for debugging which version of a function is loaded.

Examples
declare -f greet
declare -F | head   # list all defined functions
local -a arr / local -A map

Declare a function-local array (-a indexed, -A associative). Keeps array state from leaking to the global scope.

Examples
parse() {
  local -a parts
  IFS=: read -ra parts <<< "$1"
  echo "${#parts[@]} fields"
}
shift / shift N

Drop the first positional argument (or first N). After `shift`, $1 becomes the old $2 and $# drops by one.

Examples
cmd="$1"; shift
echo "running $cmd with: $*"
set -- a b c

Reset the positional parameters ($1, $2, …) to the given values. `set --` with nothing clears them all.

Examples
set -- one two three
echo "$1 $3"   # one three
set --   # now $# is 0
local var; var=$(cmd)

Declare local THEN assign, on two steps. Combining them (`local var=$(cmd)`) hides the command exit code behind local's success.

Common pitfall: `local x=$(false)` always succeeds because `local` returns 0 — the inner failure is lost. Split the lines to keep `set -e` honest.

Examples
build() {
  local out
  out=$(make 2>&1) || { echo "$out" >&2; return 1; }
}
getopts inside a function

getopts works in functions, but reset `OPTIND=1` at the top so a second call re-parses from the first argument.

Examples
greet() {
  local OPTIND opt loud=0
  while getopts "l" opt; do
    case $opt in l) loud=1 ;; esac
  done
  shift $((OPTIND-1))
  (( loud )) && echo "HELLO $1" || echo "hi $1"
}
unset -f name

Remove a function definition. `-f` is required because a variable and a function can share the same name.

Examples
greet() { echo hi; }
unset -f greet
greet   # bash: greet: command not found
Redirection (17)
cmd > file

Redirect stdout to file, OVERWRITING. Use `>>` to append instead.

Examples
ls > files.txt
date >> log.txt   # append
cmd 2> file

Redirect stderr (fd 2) to file. stdout still goes to the terminal.

Examples
make 2> build-errors.log
cmd 2>/dev/null   # discard errors
cmd > file 2>&1

Redirect BOTH stdout and stderr to the same file. Order matters: first redirect stdout, THEN dup stderr to wherever stdout now points.

Common pitfall: `cmd 2>&1 > file` does NOT work — stderr is duped to the terminal first, then stdout is moved to file. Always put `2>&1` AFTER `> file`.

Examples
build.sh > build.log 2>&1
cmd &> file   # bash shortcut for the same thing
cmd &> file

Bash shortcut: redirect both stdout AND stderr to file. Equivalent to `> file 2>&1`.

Common pitfall: `&>` is bash-specific. POSIX sh does not understand it — for portable scripts use `> file 2>&1`.

Examples
./test.sh &> test.log
cmd < file

Feed file as stdin to cmd. Same as `cat file | cmd` but without the extra cat process.

Examples
mysql -u root db < schema.sql
while read -r line; do echo "$line"; done < input.txt
cmd <<EOF ...text... EOF

Here-document: feed multi-line literal text as stdin. The marker (EOF here) just has to be unique on its own line.

Examples
cat <<EOF > config.yaml
name: app
port: 8080
EOF
cmd <<'EOF' ... EOF

Quoted heredoc — single-quoting the marker DISABLES variable and command substitution inside. The body is fully literal.

Examples
cat <<'EOF'
This $will not expand and `nor will this`.
EOF
cmd <<<"$var"

Here-string: feed a single string as stdin. Cleaner than `echo "$var" | cmd` for one-liners.

Examples
grep -c "ERROR" <<< "$log"
tr a-z A-Z <<< "hello"
exec > file

Redirect the SCRIPT's own stdout from this line on. After this, every command in the script writes to `file` instead of the terminal.

Examples
#!/bin/bash
exec > script.log 2>&1
echo "this goes to script.log"
# tee pattern:
exec > >(tee script.log) 2>&1
cmd 2>/dev/null

Throw stderr away. Useful when a command spews known noise — e.g. `find` complaining about permission-denied dirs.

Common pitfall: Hiding errors hides real bugs. Use sparingly and prefer filtering specific patterns: `cmd 2> >(grep -v "expected noise" >&2)`.

Examples
find / -name foo 2>/dev/null
which gcc &>/dev/null && echo "have gcc"
cmd >> file

Append stdout to file instead of overwriting. The file is created if it does not exist.

Examples
echo "$(date): started" >> run.log
cat extra.txt >> combined.txt
exec 3< file

Open a file on a custom file descriptor (3 here). Then `read -u 3` reads from it; `exec 3<&-` closes it.

Examples
exec 3< data.txt
read -r first <&3
read -r second <&3
exec 3<&-
cmd 2>&1 | tee log

Merge stderr into stdout BEFORE the pipe, so tee captures both streams to the log and the screen.

Common pitfall: `cmd | tee log 2>&1` is wrong — only stdout reaches tee; stderr still goes straight to the terminal. Put `2>&1` before the `|`.

Examples
make 2>&1 | tee build.log
cmd <<-EOF

Heredoc with `<<-` strips LEADING TABS from each line (tabs only, not spaces). Lets you indent the heredoc body inside a function.

Examples
gen() {
	cat <<-EOF
		line one
		line two
	EOF
}
> file (truncate)

A bare redirect with no command truncates (empties) a file, or creates it empty if missing. The `: > file` form is the explicit idiom.

Examples
> app.log        # empty the log
: > /tmp/marker  # create/empty a marker file
cmd 3>&1 1>&2 2>&3

Swap stdout and stderr using a temporary fd 3. After this, stdout goes where stderr went and vice versa.

Examples
# send only stdout (not stderr) down a pipe to grep:
noisy_cmd 3>&1 1>&2 2>&3 | grep -v warning
read var < file

Read just the FIRST line of a file into a variable. Cheaper than `$(head -1 file)` — no subshell, no extra process.

Examples
read -r version < VERSION
echo "building $version"
Pipes & subshells (17)
cmd1 | cmd2

Pipe: stdout of cmd1 becomes stdin of cmd2. Each command runs in its own subprocess in parallel.

Examples
ps aux | grep nginx
cat access.log | awk '{print $7}' | sort | uniq -c | sort -rn
cmd1 |& cmd2

Pipe both stdout AND stderr into cmd2. Bash 4+ shortcut for `cmd1 2>&1 | cmd2`.

Examples
make |& tee build.log
cmd &

Run cmd in the background. Returns immediately with the PID in $!. `wait` blocks until all background jobs finish.

Examples
long_task &
pid=$!
# do other work
wait $pid
$(cmd)

Command substitution: replace with cmd's stdout. Preferred over backticks because $(...) NESTS cleanly.

Common pitfall: Trailing newlines are STRIPPED. If you care about preserving them, append a sentinel: `out=$(cmd; echo END)`.

Examples
today=$(date +%F)
count=$(grep -c ERROR app.log)
files=$(find . -name "*.py" | head -5)
`cmd`

Old-style command substitution. Works but does NOT nest cleanly and quoting gets ugly. Prefer `$(cmd)`.

Examples
date=`date +%F`   # works but legacy
echo "Today: $(date +%F)"   # better
<(cmd)

Process substitution: makes cmd's output look like a temp filename. Lets you pass "streams" where a tool expects file arguments.

Common pitfall: Process substitution is bash, not POSIX sh. The `<(...)` syntax errors out under dash / busybox sh.

Examples
diff <(sort a.txt) <(sort b.txt)
comm -12 <(sort users-old) <(sort users-new)
set -o pipefail

Make a pipeline fail if ANY command in the pipe fails. Without this, only the LAST command's exit code is reported.

Examples
set -euo pipefail
curl -fsS bad-url | jq .   # without pipefail, jq exit 0 hides curl failure
cmd | tee file

Pipe to file AND keep printing to stdout. Add `-a` to append instead of overwrite.

Examples
make 2>&1 | tee build.log
echo "192.168.1.1 host" | sudo tee -a /etc/hosts
( cmd1; cmd2 )

Run a group in a SUBSHELL. Variables you set inside do NOT leak out — useful for scoping `cd`, `set -x`, env tweaks.

Examples
( cd /tmp && tar xzf "$tarball" )   # the parent shell stays where it was
{ cmd1; cmd2; }

Run a group in the CURRENT shell (no subshell). Variables persist after the group. Needs spaces and trailing semicolon.

Examples
{ echo "header"; cat file; echo "footer"; } > out.txt
wait / wait -n

Block until background jobs finish. `wait` waits for all; `wait -n` returns as soon as ANY one finishes (bash 4.3+).

Examples
job1 & job2 & job3 &
wait   # block until all three done
echo "all finished"
cmd1 & cmd2 & wait

Run two commands in parallel and wait for both. A two-line "fan out then join" without external tools.

Examples
download a.url & download b.url &
wait
echo "both downloaded"
PIPESTATUS array

After a pipeline, ${PIPESTATUS[@]} holds the exit code of EACH command in order. Inspect a specific stage even without pipefail.

Examples
curl -fsS "$url" | gzip > out.gz
echo "curl=${PIPESTATUS[0]} gzip=${PIPESTATUS[1]}"
cmd | xargs -P4 -n1 worker

Fan input out to up to 4 parallel `worker` processes, one argument each. Simple bounded parallelism from a list.

Examples
cat urls.txt | xargs -P4 -n1 curl -sO
cmd | xargs -0

Read NUL-separated input into xargs. Pair with `find -print0` to handle filenames with spaces or newlines safely.

Examples
find . -name "*.bak" -print0 | xargs -0 rm -v
diff <(cmd1) <(cmd2)

Compare the OUTPUT of two commands directly, no temp files. Process substitution feeds each side as a pseudo-file.

Examples
diff <(curl -s host1/v) <(curl -s host2/v)
diff <(echo "$a") <(echo "$b")
cmd | read -r var (caveat)

Reading from a pipe runs the right side in a SUBSHELL, so the variable is empty afterward. Use a here-string or process substitution instead.

Common pitfall: `echo "$x" | read y` leaves $y unset in the parent. Write `read y <<< "$x"` or `read y < <(cmd)`.

Examples
read -r first <<< "$line"
read -r n < <(wc -l < file)
Strings (12)
echo -n "$var" | wc -c

Byte length of $var (use `${#var}` for character length in single-byte locales).

Common pitfall: `${#var}` counts characters not bytes — a Chinese character may be 1 in count but 3 bytes in UTF-8. `wc -c` counts bytes; `wc -m` counts characters in the current locale.

Examples
s="hello"
echo "${#s} chars"
echo -n "$s" | wc -c
IFS=":" read -ra parts <<< "$path"

Split a string by a delimiter into an array. `-a parts` reads into the indexed array `parts`.

Examples
IFS=":" read -ra dirs <<< "$PATH"
for d in "${dirs[@]}"; do echo "$d"; done
printf -v var "format" args

Assign formatted output directly to a variable — no subshell, no command substitution overhead.

Examples
printf -v fname "report-%s-%02d.csv" "$dept" "$month"
echo "$fname"   # report-sales-05.csv
echo "${s// /_}"

Replace all spaces with underscores. Single slash replaces only the first match.

Examples
name="hello world"
safe="${name// /_}"
echo "$safe"   # hello_world
[[ "$s" == prefix* ]]

Test if a string starts with a prefix using glob in `[[ ]]`. Suffix is `*prefix`.

Examples
if [[ "$file" == /etc/* ]]; then echo "system config"; fi
if [[ "$url" == https://* ]]; then ...; fi
tr "a-z" "A-Z" <<< "$s"

Upper-case a string portably (works on bash 3.2 macOS). tr does character translation.

Examples
upper=$(tr "a-z" "A-Z" <<< "$name")
echo "$upper"
printf "%s\n" "$var"

Safer than echo for arbitrary strings — `printf` never interprets leading `-` as an option and is portable across shells.

Common pitfall: `echo "$x"` mangles values like `-n` or `-e` (treats them as flags) and varies by shell. `printf "%s\n"` is predictable.

Examples
printf "%s\n" "-n looks like a flag"
printf "%s=%s\n" key value
printf "%q" "$s"

Print a string in a SHELL-QUOTED form that can be safely re-used as input. Great for logging exact commands or generating scripts.

Examples
printf "run: %q %q\n" "$cmd" "$arg with space"
trimmed="${s#"${s%%[![:space:]]*}"}"

Strip leading whitespace using pure parameter expansion (no subshell). Mirror with a trailing-space pattern to fully trim.

Examples
s="   hi   "
lead="${s#"${s%%[![:space:]]*}"}"
echo "[$lead]"   # [hi   ]
IFS= read -rd "" var < file

Slurp an ENTIRE file (including trailing newlines) into one variable. `-d ""` reads until NUL, which a text file never contains.

Examples
IFS= read -rd "" content < notes.md
echo "${#content} bytes read"
[[ "$s" =~ ^[0-9]+$ ]]

Check a string is all digits (a non-negative integer) with a regex. Add a leading `-?` to allow a minus sign.

Examples
is_int() { [[ "$1" =~ ^-?[0-9]+$ ]]; }
is_int "42" && echo ok
is_int "4a" || echo "not an int"
mapfile -t arr < <(grep ...)

Split command output into an array by LINES, safely keeping spaces within each line. Better than `arr=($(cmd))` which word-splits.

Examples
mapfile -t hits < <(grep -n TODO src.py)
echo "${#hits[@]} TODOs"
Flow control (16)
set -e

Exit immediately if any command returns non-zero. The "fail fast" flag — combine with -u and -o pipefail for safe scripts.

Common pitfall: set -e is FULL of corner cases. `cmd1 && cmd2` does NOT trigger -e if cmd1 fails. `cmd || true` is the idiom for "I expect this to fail sometimes".

Examples
set -euo pipefail
IFS=$'\n\t'   # the "unofficial bash strict mode" header
set -u

Treat unset variables as errors. Catches typos like `$ptah` vs `$path` immediately instead of silently expanding to "".

Common pitfall: With -u, `${var}` errors out if unset. Always use `${var:-}` for "I know it might be unset" and `${var:?msg}` to assert it must be set.

Examples
set -u
echo "$undefined"   # bash: undefined: unbound variable
set -x

Print every command (with expanded variables) before running it. The fastest way to debug a script.

Examples
set -x
name="lei"
echo "hello $name"
# stderr: + name=lei
#         + echo 'hello lei'
set -o pipefail

A pipeline's exit code is the rightmost FAILING command, not just the last command. Pairs with -e.

Examples
set -o pipefail
curl bad-url | jq .   # now fails properly when curl fails
trap "cleanup" EXIT

Run `cleanup` when the script exits, no matter how (normal, error, signal). The standard pattern for tmpfile cleanup.

Examples
tmp=$(mktemp)
trap "rm -f \"$tmp\"" EXIT
# tmp gets removed even if the script crashes
trap "..." INT TERM

Intercept Ctrl-C (INT) and `kill` (TERM). Print a message, do cleanup, then exit gracefully.

Examples
trap "echo '^C — bye'; exit 130" INT
trap "stop_server; exit" TERM
exit N

Terminate the script with exit code N. 0 = success. By convention 1 = generic error, 2 = misuse, 127 = command not found.

Examples
exit 0
exit 1   # generic failure
[[ $# -eq 0 ]] && { echo "need an arg"; exit 2; }
$?

Exit status of the LAST command. Read it ONCE — every later command (including `[ ]`) updates it.

Common pitfall: `cmd; if [[ $? -eq 0 ]]; then echo "ok"; fi` is over-engineered. Just write `if cmd; then echo "ok"; fi`.

Examples
ping -c1 host >/dev/null
rc=$?
echo "ping returned $rc"
time cmd

Print how long cmd took (real, user, sys). Built-in to bash; works on pipelines too.

Examples
time make build
time (find /usr | wc -l)
trap "cleanup" ERR

Run a handler whenever any command fails (returns non-zero) under `set -e`. Useful for logging the failing line before exit.

Examples
set -e
trap "echo \"failed at line $LINENO\" >&2" ERR
false   # triggers the trap
trap - SIGNAL

Reset a trap back to the shell's default behavior for that signal. `trap - EXIT` cancels a previously installed exit handler.

Examples
trap cleanup EXIT
# ... later, when no longer needed:
trap - EXIT
set +e / set +x

A leading `+` TURNS OFF a shell option (`-` turns it on). Wrap a fragile section: `set +e; risky; set -e`.

Examples
set +e
might_fail   # do not abort on failure here
rc=$?
set -e
exec cmd

REPLACE the current shell process with cmd — no new process, no return. The script ends and cmd inherits its PID.

Examples
#!/usr/bin/env bash
# set up env, then hand off:
export APP_ENV=prod
exec ./server "$@"
exit code 124 / 130 / 137

Common signal-derived exit codes: 124 = `timeout` killed it, 130 = Ctrl-C (128+SIGINT 2), 137 = SIGKILL (128+9, e.g. OOM kill).

Examples
timeout 5 long_task
case $? in
  0) echo ok ;;
  124) echo "timed out" ;;
esac
timeout 10s cmd

Run cmd but kill it after the given duration, exiting 124 on timeout. Add `-k 5s` to send SIGKILL if it ignores the first signal.

Examples
timeout 30 curl -s "$url" || echo "slow endpoint"
timeout -k 5s 60s ./batch.sh
set -E (errtrace)

Make the ERR trap inherit into functions, command substitutions, and subshells. Without it, a `trap ... ERR` does NOT fire inside functions.

Examples
set -Eeuo pipefail
trap "echo err >&2" ERR
f() { false; }
f   # now the trap fires
Script patterns (20)
#!/usr/bin/env bash

Shebang line. `env bash` finds bash on $PATH instead of hard-coding /bin/bash (which on macOS is the ancient 3.2 build).

Examples
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
while getopts ":hf:v" opt; do ... done

POSIX-portable option parsing. `:` after a letter means it takes an argument (in $OPTARG); leading `:` enables silent error handling.

Examples
while getopts ":hf:v" opt; do
  case $opt in
    h) echo "usage: ..."; exit 0 ;;
    f) file="$OPTARG" ;;
    v) verbose=1 ;;
    \?) echo "bad flag: -$OPTARG" >&2; exit 2 ;;
  esac
done
shift $((OPTIND - 1))   # consume parsed flags
while [[ $# -gt 0 ]]; do case "$1" in ... esac; done

Hand-rolled arg parsing for long options (--flag value, --flag=value). More flexible than getopts but you write more code.

Examples
while [[ $# -gt 0 ]]; do
  case "$1" in
    --port)  port="$2"; shift 2 ;;
    --port=*) port="${1#*=}"; shift ;;
    --) shift; break ;;
    *) args+=("$1"); shift ;;
  esac
done
tmp=$(mktemp); trap "rm -f \"$tmp\"" EXIT

Create a guaranteed-unique temp file and arrange to delete it on exit. Use `mktemp -d` for a temp directory.

Common pitfall: BSD mktemp (macOS) needs a template: `mktemp /tmp/myapp.XXXXXX`. GNU mktemp works without one. `mktemp -t prefix` is the portable form.

Examples
tmp=$(mktemp)
trap "rm -f \"$tmp\"" EXIT
curl -o "$tmp" https://example.com/data.json
echo -e "\033[31mred\033[0m"

Print colored text via ANSI escape codes. \033[31m = red, \033[0m = reset. Only when output is a TTY (test with `[[ -t 1 ]]`).

Examples
RED=$'\033[31m'
RESET=$'\033[0m'
[[ -t 1 ]] || RED='' RESET=''   # disable colors when piped
echo "${RED}ERROR${RESET}: bad config"
progress() { printf "\r[%-50s] %d%%" "$bar" "$pct"; }

A poor-man's progress bar. \r returns the cursor to the line start so each update overwrites the previous one.

Examples
for i in {1..100}; do
  pct=$i
  bar=$(printf "%.0s#" $(seq 1 $((pct/2))))
  printf "\r[%-50s] %3d%%" "$bar" "$pct"
  sleep 0.05
done
echo
echo "$0"

Script's own invocation name. Useful for usage strings: `echo "usage: $0 [options]"`.

Examples
if [[ $# -lt 2 ]]; then
  echo "usage: $0 <src> <dst>" >&2
  exit 2
fi
BASH_SOURCE / readlink -f

Resolve the script's own directory, even when called through symlinks. The standard "find my siblings" idiom.

Examples
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/lib.sh"
command -v cmd >/dev/null

Check whether a command exists without running it. Returns 0 if found. More portable than `which`.

Examples
if ! command -v jq >/dev/null; then
  echo "need jq, please install" >&2
  exit 1
fi
read -p "Continue? [y/N] " ans

Prompt the user and read one line into $ans. -p sets the prompt; -s hides input (for passwords).

Examples
read -p "Continue? [y/N] " ans
[[ "${ans,,}" == "y" || "${ans,,}" == "yes" ]] || exit 0
log() { echo "[$(date +%H:%M:%S)] $*" >&2; }

Cheap timestamped log helper that writes to stderr (so it does not pollute pipeline output).

Examples
log() { echo "[$(date +%H:%M:%S)] $*" >&2; }
log "starting deploy"
log "done"
: "${VAR:?must be set}"

Assert a required variable at the top of a script. `:` is a no-op, so this line only exists to trigger the `:?` guard.

Examples
: "${DEPLOY_ENV:?set DEPLOY_ENV to dev|prod}"
: "${TOKEN:?missing TOKEN}"
cd "$(dirname "$0")"

Change to the script's own directory so relative paths inside it resolve regardless of where it was invoked from.

Common pitfall: Breaks when the script is reached via a symlink — `$0` is the link path. Use `${BASH_SOURCE[0]}` + `readlink -f` for symlink-proof resolution.

Examples
cd "$(dirname "$0")" || exit 1
source ./config.sh
usage() { cat <<EOF ... EOF; }

A usage/help function built from a heredoc. Print it on `-h` or on bad arguments, then `exit`.

Examples
usage() {
  cat <<EOF
usage: $0 [-v] <src> <dst>
  -v   verbose
EOF
}
[[ "$1" == "-h" ]] && { usage; exit 0; }
shopt -s globstar

Enable `**` to match files across directories recursively. After it, `**/*.js` finds every .js at any depth. Bash 4+.

Examples
shopt -s globstar
for f in src/**/*.test.ts; do echo "$f"; done
shopt -s extglob

Enable extended globs: `?(x)` `*(x)` `+(x)` `@(a|b)` `!(x)`. Lets globs do alternation and negation like mini-regex.

Examples
shopt -s extglob
rm -- !(*.keep)        # delete everything except *.keep
echo @(jpg|png|gif)    # any of these
trap cleanup EXIT INT TERM

One trap covering normal exit, Ctrl-C, and kill. The most common cleanup setup for scripts that create temp resources.

Examples
workdir=$(mktemp -d)
cleanup() { rm -rf "$workdir"; }
trap cleanup EXIT INT TERM
IFS=$'\n\t'

Part of "bash strict mode": set IFS to newline+tab only, so unquoted expansions do not split on spaces. Pairs with `set -euo pipefail`.

Examples
set -euo pipefail
IFS=$'\n\t'
flock -n lockfile cmd

Run cmd only if it can grab an exclusive lock — prevents two copies of a cron job from overlapping. `-n` fails fast instead of waiting.

Examples
flock -n /tmp/job.lock ./nightly.sh || echo "already running"
read -rsp "Password: " pw; echo

Prompt for a secret without echoing keystrokes: -s silences input, -p shows the prompt. Add a trailing `echo` for the newline.

Examples
read -rsp "Password: " pw
echo
curl -u "user:$pw" "$url"
Arrays (16)
declare -a arr

Declare an indexed (numeric-key) array. Bash arrays are 0-indexed.

Examples
declare -a fruits=(apple banana cherry)
echo "${fruits[0]}"   # apple
declare -A map

Declare an associative array (string keys). Bash 4+ only. macOS Bash 3.2 does NOT support these.

Common pitfall: You MUST `declare -A map` before using it. Otherwise `map["key"]="v"` silently treats `"key"` as 0 and you get an indexed array with one element.

Examples
declare -A color
color[apple]=red
color[grape]=purple
echo "${color[apple]}"   # red
arr=(a b c)

Initialize an indexed array literally. Space-separated, parens around.

Examples
envs=(dev staging prod)
echo "${envs[1]}"   # staging
arr+=(new1 new2)

Append elements to the end of an indexed array.

Examples
files=()
for f in *.log; do files+=("$f"); done
echo "${#files[@]} log files"
"${arr[@]}"

Expand to ALL elements as separate quoted words. The usual right way to iterate.

Common pitfall: `"${arr[*]}"` joins everything into ONE string with $IFS. Almost always you want `"${arr[@]}"` instead.

Examples
for f in "${files[@]}"; do
  echo "$f"
done
"${#arr[@]}"

Number of elements in the array.

Examples
echo "${#files[@]} files matched"
if (( ${#errors[@]} > 0 )); then echo "had errors"; fi
"${!arr[@]}"

Expand to all KEYS of the array (indices for indexed arrays, string keys for associative).

Examples
for i in "${!arr[@]}"; do
  echo "$i -> ${arr[$i]}"
done
for k in "${!color[@]}"; do
  echo "$k is ${color[$k]}"
done
unset "arr[2]"

Remove a single element. Indices are NOT renumbered — `${#arr[@]}` shrinks but `${arr[3]}` is still there.

Examples
arr=(a b c d)
unset "arr[1]"
echo "${arr[@]}"   # a c d
echo "${!arr[@]}"   # 0 2 3
mapfile -t arr < file

Read every line of a file into an array, one line per element. `-t` strips the trailing newline from each line.

Examples
mapfile -t lines < urls.txt
echo "read ${#lines[@]} urls"
"${arr[@]:s:n}"

Array slice: take `n` elements starting at index `s`. `"${arr[@]:1}"` drops the first element.

Examples
a=(a b c d e)
echo "${a[@]:1:2}"   # b c
echo "${a[@]: -2}"   # d e (note the space)
arr=("${arr[@]}" extra)

Rebuild an array adding elements — also the way to RE-INDEX a sparse array (close the gaps left by `unset`).

Examples
a=(x y z)
unset "a[1]"
a=("${a[@]}")   # now indices are 0,1 again
echo "${!a[@]}"   # 0 1
"${arr[*]}" with custom IFS

Join array elements into one string. Set IFS to the separator first; `[*]` glues them using IFS's first character.

Examples
parts=(2026 05 30)
IFS=-; echo "${parts[*]}"; unset IFS   # 2026-05-30
declare -p arr

Print an array (or any variable) in a re-usable `declare` form. The fastest way to debug exactly what an array holds.

Examples
a=(one "two words" three)
declare -p a   # declare -a a=([0]="one" [1]="two words" [2]="three")
map[key]+=value

Append to an associative-array value in place. Common for accumulating counts or concatenating per-key lists.

Examples
declare -A seen
for w in a b a c a; do seen[$w]=$(( ${seen[$w]:-0} + 1 )); done
echo "${seen[a]}"   # 3
[[ -v map[key] ]]

Test whether a specific associative-array KEY exists (even if its value is empty). Bash 4.3+.

Examples
declare -A cfg=([host]=a [port]="")
[[ -v cfg[port] ]] && echo "port key exists"
[[ -v cfg[tls] ]] || echo "no tls key"
readarray -t -d "" arr < <(find ... -print0)

Read NUL-delimited items into an array, one per element. `readarray` is a synonym for `mapfile`; `-d ""` splits on NUL.

Examples
readarray -t -d "" files < <(find . -name "*.log" -print0)
echo "${#files[@]} logs"
Common pitfalls (21)
Always quote your variables

Unquoted variable expansion goes through word-splitting (split on $IFS) AND glob expansion. A filename with spaces or a glob char becomes multiple arguments.

Common pitfall: `rm $file` with file="report 2024.csv" becomes `rm report 2024.csv` — two args. `rm "$file"` is the safe form. Default to quoting; un-quote only when you specifically want splitting.

Examples
cp "$src" "$dst"   # safe
for f in "$@"; do echo "$f"; done   # safe
cd may fail — guard with &&

`cd /missing/dir; rm -rf *` deletes the CURRENT directory if cd fails. Always chain: `cd dir && rm ...` or check `cd || exit`.

Common pitfall: set -e does NOT help with this — `cd` failing followed by `;` rm runs anyway. Use && or `cd dir || { echo "no such dir" >&2; exit 1; }`.

Examples
cd "$build_dir" && rm -rf *   # safe
pushd "$dir" || exit; do_work; popd
Pipe into while loop runs in a subshell

`cat file | while read line; do count=$((count+1)); done` — $count is 0 after the loop because while ran in a subshell. Use `done < file` or process substitution.

Examples
# wrong:
# cat file | while read l; do n=$((n+1)); done
# right:
while read -r l; do n=$((n+1)); done < file
echo "$n"
set -e does not catch every failure

set -e ignores failures inside `if`, `while`, `until`, `&&`, `||`, and pipelines (without pipefail). It only kills on bare commands at the top level.

Common pitfall: Add `set -o pipefail` and never rely on -e as your only safety net. Explicit checks (`|| { echo err; exit 1; }`) are the real defense.

Examples
set -euo pipefail
critical_cmd || { echo "critical failed" >&2; exit 1; }
globbing surprises (no match)

A glob with no matches expands to the LITERAL pattern, not an empty list. `for f in *.log` with no logs iterates once with f="*.log".

Common pitfall: Fix with `shopt -s nullglob` (no-match → empty list) or `shopt -s failglob` (no-match → error). Pick once per script and stick with it.

Examples
shopt -s nullglob
for f in *.log; do echo "$f"; done
wc -l misses the last line without a newline

`wc -l` counts NEWLINE characters, not lines. A file ending without \n loses its last line in the count.

Common pitfall: Use `awk "END{print NR}"` for a true line count, or `grep -c ""` which counts records including the unterminated one.

Examples
printf "a\nb" | wc -l   # 1 (!)
printf "a\nb" | awk "END{print NR}"   # 2
Arithmetic vs string in [[ ]]

`[[ "5" > "10" ]]` is TRUE because it does ASCII comparison. For numbers use `(( 5 > 10 ))` or `[[ 5 -gt 10 ]]`.

Examples
(( age >= 18 ))   # numeric
[[ "$name" > "alice" ]]   # alphabetic
$_ vs $0 in sourced scripts

In a sourced script, `$0` is the SHELL's name (bash, zsh), not the script's. Use `${BASH_SOURCE[0]}` to get the actual file path.

Examples
# inside lib.sh:
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  echo "run directly"
else
  echo "sourced from $0"
fi
macOS bash is stuck at 3.2

Apple ships bash 3.2 (May 2007) because newer versions are GPLv3-licensed. Associative arrays, ${var^^}, mapfile, |& all need bash 4+.

Common pitfall: `brew install bash` then put `/opt/homebrew/bin/bash` in your shebang. Or change to zsh, which is default on modern macOS.

Examples
bash --version   # check yours
#!/opt/homebrew/bin/bash   # macOS modern bash
Subshell variable scope

Parentheses `(...)` run in a subshell — variable assignments and `cd` inside do NOT affect the parent shell.

Examples
x=outer
(x=inner; echo "in: $x")   # in: inner
echo "out: $x"             # out: outer
getopts cannot do long options

Built-in `getopts` only handles single-char flags like `-f file`. For `--file file` you need `getopt` (note: no s — different program) or hand-rolled parsing.

Common pitfall: GNU getopt and BSD getopt are incompatible. macOS getopt is the original BSD one — no long-option support. For cross-platform long flags, hand-roll the loop.

Examples
# portable long-option parsing:
while [[ $# -gt 0 ]]; do
  case "$1" in
    --file) file="$2"; shift 2 ;;
    --) shift; break ;;
    *) shift ;;
  esac
done
echo is not portable

`echo -e` and `echo -n` behave differently across shells and builds. For escapes or no-newline output, use `printf` instead.

Common pitfall: On some systems `echo "-n"` literally prints `-n`. `printf "%s"` / `printf "%b"` are the predictable replacements.

Examples
printf "no newline"
printf "%b\n" "tab\there"
a=$(cmd) hides the exit code

Assigning a command substitution always makes the assignment succeed, so `set -e` will NOT abort even if `cmd` failed.

Common pitfall: Split it: `out=$(cmd)` on its own line still hides it; check explicitly with `out=$(cmd) || exit 1` or test `$?` right after.

Examples
out=$(grep ERR log) || { echo "grep failed"; exit 1; }
unquoted $() in arrays splits wrong

`arr=($(cmd))` splits output on $IFS AND globs it — filenames with spaces or a `*` explode. Use `mapfile -t arr < <(cmd)`.

Common pitfall: A filename like `my file.txt` becomes two elements; a value `*` expands to the directory listing. `mapfile -t` splits on lines only.

Examples
mapfile -t names < <(ls)   # safe, line-by-line
trailing newline stripped by $()

Command substitution removes ALL trailing newlines. Comparing `$(cmd)` to text with a final newline will surprise you.

Common pitfall: To preserve them, append a sentinel and strip it: `out=$(cmd; printf x); out=${out%x}`.

Examples
out=$(printf "a\n\n")
echo "${#out}"   # 1 — the two newlines are gone
rm -rf "$dir/" with empty dir

If `$dir` is unset/empty, `rm -rf "$dir/"` becomes `rm -rf "/"`. ALWAYS validate the variable before a destructive rm.

Common pitfall: Guard with `[[ -n "$dir" && -d "$dir" ]] || exit 1` and prefer `set -u` so an unset var errors instead of expanding to nothing.

Examples
[[ -n "${dir:-}" ]] || { echo "dir unset" >&2; exit 1; }
rm -rf -- "$dir"
read drops the last line without newline

`while read -r line` skips a final line that has no trailing newline, because read returns non-zero on EOF mid-line.

Common pitfall: Catch it with `while read -r line || [[ -n "$line" ]]; do ...; done` so the partial last line is still processed.

Examples
while read -r l || [[ -n "$l" ]]; do echo "$l"; done < no-trailing-newline.txt
tilde ~ does not expand in quotes

Inside quotes, `~` stays literal: `path="~/x"` is the four characters `~/x`, not your home. Use `"$HOME/x"` instead.

Common pitfall: Tilde expansion only happens UNQUOTED and at the start of a word. `cd "~"` fails; `cd ~` or `cd "$HOME"` works.

Examples
log="$HOME/app.log"   # right
log=~/app.log         # also right (unquoted)
[ ] vs [[ ]] with unset vars

In old `[ ]`, an empty unquoted variable becomes a syntax error: `[ $x = y ]` with empty x is `[ = y ]`. `[[ ]]` handles it; or always quote.

Common pitfall: The classic defensive trick for `[ ]` is `[ "x$a" = "x$b" ]`. With `[[ ]]` you do not need it.

Examples
[[ "$a" == "$b" ]]   # safe even if both empty
cron has a minimal PATH

Scripts run by cron get a bare PATH (often just /usr/bin:/bin), so tools in /usr/local/bin go missing. Use absolute paths or set PATH at the top.

Common pitfall: A script that works in your terminal can fail under cron with "command not found". Add `export PATH=/usr/local/bin:$PATH` or call binaries by full path.

Examples
#!/usr/bin/env bash
export PATH="/usr/local/bin:/usr/bin:/bin"
sourcing a script runs it now

`source f` (or `. f`) executes f IN THE CURRENT shell — its `exit` kills your shell, its `set -e` affects you, its variables persist.

Common pitfall: A library meant to be sourced should never call bare `exit`; use `return` so the caller survives a failed load.

Examples
# in lib.sh, guard "run vs sourced":
[[ "${BASH_SOURCE[0]}" != "$0" ]] && return 0

What this tool does

Searchable Bash cheat sheet with 100+ commands and idioms, organized into the eleven sections you actually reach for at 2am. Variables: "${var}", defaults ${var:-x}, assign-if-unset ${var:=x}, error ${var:?msg}, strip ${var#…} ${var%…}, replace ${var//old/new}, length ${#var}, substring ${var:0:5}, case ${var^^} ${var,,}. Conditionals: [[ ]], string and glob equality, =~ regex with BASH_REMATCH, arithmetic (( )), -lt -gt -eq, file tests -f -d -e -L -s, -z -n, && ||, case. Loops: for over lists and globs, C-style for ((i=0;i<n;i++)), the safe while read -r line < file pattern, until, select, break / continue, {1..10}. Functions: name() and function forms, $1 ${10}, "$@" vs "$*", local, return, capture with x=$(fn). Redirection: > >> 2> 2>&1 &> < <<EOF <<'EOF' <<<, exec for the script itself, /dev/null. Pipes: | |& & with $!, $(cmd), <(cmd), set -o pipefail, tee, ( ) subshell vs { } group. Strings: byte vs character length, IFS-split into arrays, printf -v, glob prefix tests. Flow: set -e -u -x, pipefail, trap EXIT for cleanup, trap INT TERM for signals, exit N, $?, time. Script patterns: shebang, getopts, hand-rolled long options, mktemp + trap, ANSI colors with TTY detection, progress bar, BASH_SOURCE, command -v, read -p, stderr logging. Arrays: declare -a / -A, +=(), "${arr[@]}", "${#arr[@]}", "${!arr[@]}", unset, mapfile. Pitfalls: always quote, cd may fail (use && not ;), pipe-into-while runs in a subshell so counters die, set -e misses failures in if / && / pipelines without pipefail, unmatched glob becomes the literal pattern (nullglob), wc -l counts newlines so the last unterminated line is missed, [[ "5" > "10" ]] is true (string compare), $0 vs ${BASH_SOURCE[0]} in sourced scripts, macOS ships bash 3.2 (brew install bash for 4+), getopts can't do long options. Every entry has bilingual text, a copy-ready example, and a pitfall callout where it matters. Search filters across command, description, pitfall, and example; one-click copy.

Tool details

Input
Text
The page exposes text boxes, numeric controls, file pickers, or structured inputs depending on the tool.
Output
Live result + Copy
The result area focuses on usable output, with copy, download, or preview actions when supported.
Privacy
Browser-side processing
The main tool logic does not call an external API, so inputs normally stay in the current tab.
Save / share
Shareable URL state
Key settings are encoded in the URL so another person can reopen the same setup.
Performance budget
Initial JS <= 28 KB
No WASM budget is declared, keeping the tool quick to open on mobile.
Best fit
Developer & DevOps · Developer
Category and role tags drive related tools, internal links, and quick fit checks.

How to use

  1. 1. Input

    Paste or drop your content into the tool panel.

  2. 2. Process

    Click the button. All processing is local in your browser.

  3. 3. Copy / Download

    Copy the result or download to disk in one click.

How Bash Cheatsheet fits into your work

Use it in the small gaps between coding, reviewing, debugging, and shipping.

Developer jobs

  • Formatting, validating, shrinking, or inspecting code-adjacent text.
  • Preparing snippets for documentation, tickets, commits, or handoff.
  • Checking a small payload quickly without switching tools.

Developer checks

  • Run irreversible transforms like minify or obfuscate on a copy.
  • Keep secrets out of pasted snippets unless the tool explicitly stays local.
  • Use your normal tests or linter before shipping transformed code.

Good next steps

These links move the current task into a more complete workflow.

  1. 1 Nginx Cheatsheet Nginx cheat sheet — common configs, location/server blocks, SSL, reverse proxy, gzip, real examples & gotchas. Open
  2. 2 awk + sed Cheatsheet awk + sed cheat sheet — 80+ one-liners for text processing, with real examples and common pitfalls. Open
  3. 3 Vim Cheatsheet Vim cheat sheet — 100+ commands covering modes, motions, edits, search, registers, splits, with mnemonics. Open

Real-world use cases

  • Debugging a CI deploy script that fails silently

    A 200-line deploy.sh passes in CI but skips half its steps. You search the sheet for "pipefail" and "set -e", paste `set -euo pipefail` into the header, and find the `curl | tar` pipe that was swallowing a 404. Five minutes, no Stack Overflow tab, and the next run fails loud at the right line.

  • Writing a one-off log parser at 2am during an incident

    Production is down and you need to count 5xx lines per minute from a 4GB log. You grab the `while read -r line < file` pattern (not `cat | while`, which loses the counter), the IFS-split idiom, and `printf -v` to format the timestamp. The loop runs in one shell, the count survives, and you have a number to put in the incident channel in under three minutes.

  • Porting a script from your Mac to a Linux container

    Your script uses `declare -A` and `${path,,}` and dies on the colleague's Alpine box with a cryptic error. You hit the macOS-vs-Linux section, learn the Mac was secretly running bash 3.2, add the `BASH_VERSINFO` version assert, and swap the associative array for a case statement. One pass, both machines green, no "works on my machine" ping-pong.

  • Teaching a junior the safe argument-parsing pattern

    A teammate hand-rolls `$1 == "-f"` checks and trips over `--file=foo`. You open the getopts entry and the long-flag loop side by side, copy the `case "$1" in --file=*)` skeleton into their PR, and explain why `shift 2` matters. The example is copy-ready, so the review comment is a paste, not a lecture.

Common pitfalls

  • Using `cat file | while read` to accumulate a counter — the loop runs in a subshell and the count is 0 after it. Redirect instead with `while read -r line; do …; done < file`.

  • Trusting `set -e` alone in a pipeline. `bad-cmd | tee out` still reports success; add `set -o pipefail` so the pipeline fails on the leftmost failure.

  • Comparing numbers with `>` inside `[[ ]]`, e.g. `[[ "5" > "10" ]]` is true because it is a string compare. Use `(( 5 > 10 ))` for arithmetic.

Privacy

Everything runs in your browser. The command list is a static in-memory array, and the search box, category chips, and copy button never make a network request. Nothing you type into search is logged or sent anywhere, and no input is written to the URL, so the sheet works offline, behind a corporate proxy, or on an air-gapped host without leaking a byte.

FAQ

Tool combos

Folks in your role tend to reach for these alongside this tool.

Made by Toolora · 100% client-side · Updated 2026-07-02