Bash Commands That Actually Matter: A Practical Bash Cheat Sheet
A working guide to the Bash you write in scripts and the terminal — variables, conditionals, loops, pipes, parameter expansion, and the quoting and set -euo pipefail gotchas.
Bash Commands That Actually Matter: A Practical Bash Cheat Sheet
Bash is the language you reach for when a task is too fiddly for a single command and too small for a real program. After a few years of writing deploy scripts, log parsers, and three-line one-liners that somehow end up in CI, I've learned that the same handful of constructs carry almost everything. This post walks through the parts I use daily — variables, conditionals, loops, pipes and redirection, and parameter expansion — plus the gotchas that cost me real time before I internalized them.
If you want the searchable version while you read, keep the Bash cheat sheet open in another tab. It has the copy-ready snippet for every idiom below.
Variables and Parameter Expansion
Assigning a variable in Bash has no spaces around the =, and that bites everyone once: x = 5 tries to run a command named x. It's x=5. When you read it back, brace it: ${x} so ${x}_suffix doesn't get parsed as a variable named x_suffix.
The real power lives in parameter expansion, which is Bash quietly doing string work without spawning sed:
name=${1:-world} # default if $1 is unset or empty
: "${API_KEY:?missing API_KEY}" # bail with a message if unset
file=report.tar.gz
echo "${file%.gz}" # strip suffix -> report.tar
echo "${file##*.}" # longest prefix strip -> gz
echo "${file/report/summary}" # replace first match
echo "${#file}" # length -> 13
${var:-default} is the one I use most: it gives a fallback without touching var, perfect for optional config. ${var:?message} is its strict sibling — it prints the message to stderr and exits if the variable is empty, which turns a silent misconfiguration into a clear failure.
Conditionals: Prefer [[ ]]
Use [[ ]] for tests and (( )) for arithmetic. The old [ ] (POSIX test) is a minefield of quoting rules; [[ ]] fixes most of them.
if [[ -f config.yml && -r config.yml ]]; then
echo "config is present and readable"
fi
if [[ $filename == *.log ]]; then # glob match
echo "it's a log"
fi
if [[ $input =~ ^[0-9]+$ ]]; then # regex; captures in BASH_REMATCH
echo "all digits"
fi
if (( count > 10 )); then # arithmetic, no -gt needed
echo "too many"
fi
File tests are worth memorizing: -f regular file, -d directory, -e exists, -s non-empty, -z empty string, -n non-empty string. One trap that catches people: inside [[ ]], > and < are string comparisons, so [[ "5" > "10" ]] is true because "5" sorts after "1". For numbers, use (( )).
Loops and Globs
The two loop forms I write constantly:
for f in *.txt; do
echo "processing $f"
mv -- "$f" "${f%.txt}.bak"
done
for ((i = 0; i < 5; i++)); do
echo "attempt $i"
done
The for f in *.txt form has a subtle edge: if there are no .txt files, the glob doesn't expand and $f becomes the literal string *.txt. Guard with shopt -s nullglob so an empty match yields zero iterations instead of one bogus one.
For reading a file line by line, there is exactly one correct pattern, and it's not the obvious one:
while IFS= read -r line; do
echo "got: $line"
done < input.txt
IFS= stops leading and trailing whitespace from being trimmed, and -r stops backslashes from being mangled. Redirect the file into the loop with < input.txt. Do not write cat input.txt | while read line — more on why in the gotchas section.
Pipes and Redirection
Redirection is just plumbing for file descriptors: 1 is stdout, 2 is stderr.
make 2> build-errors.log # stderr to a file, stdout to terminal
make > out.log 2>&1 # both streams to one file
some-command 2>&1 | grep -i warn # merge, then pipe
quiet-cmd > /dev/null 2>&1 # discard everything
Order matters in > out.log 2>&1: you redirect stdout to the file first, then point stderr at "wherever stdout goes now." Flip them and stderr still goes to the terminal. Heredocs feed multi-line input to a command, and quoting the delimiter (<<'EOF') turns off variable expansion inside:
cat <<'EOF' > setup.sh
export PATH="$HOME/bin:$PATH"
EOF
Command substitution $(cmd) captures output into a variable; process substitution <(cmd) hands a command's output to something expecting a filename, e.g. diff <(sort a) <(sort b).
A Safe Script: set -euo pipefail in Practice
Here's a small, real script that downloads a file, transforms it, and cleans up after itself. It's the skeleton I copy for almost everything:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
url=${1:?usage: fetch.sh <url>}
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
echo "downloading $url" >&2
curl -fsSL "$url" -o "$tmp"
lines=$(wc -l < "$tmp")
echo "got $lines lines" >&2
grep -i error "$tmp" || echo "no errors found" >&2
Line by line: set -e exits on the first failing command, set -u errors on any unset variable (so a typo'd $tmp aborts instead of operating on /), and set -o pipefail makes a pipeline fail if any stage fails, not just the last. The trap '...' EXIT guarantees the temp file is removed whether the script succeeds, errors out, or is interrupted. The curl -f flag makes a 404 a non-zero exit, which set -e then catches. The grep ... || echo is deliberate: under set -e, grep finding nothing would otherwise kill the script, so we give it an explicit fallback.
That header — set -euo pipefail — is the single highest-leverage line you can add to a Bash script. It converts a class of silent, half-finished runs into loud, early failures.
The Gotchas That Cost Me Time
Quoting is the big one. Always quote your expansions: "$var", "$@", "${arr[@]}". An unquoted $file containing spaces becomes multiple arguments, and an unquoted empty variable can vanish entirely. The day I stopped seeing "No such file or directory" on paths with spaces was the day I started quoting reflexively.
The subshell pipe trap is the sneakiest. This prints 0:
count=0
grep -c error log | while read -r n; do count=$n; done
echo "$count" # still 0
The right side of a pipe runs in a subshell, so any variable it sets dies when the pipe ends. Redirect instead (done < <(grep ...)) or restructure so the assignment happens in your current shell.
Two more: chain cd with && not ;, because cd /nonexistent; rm -rf * will happily delete files in the wrong directory if the cd fails. And remember set -e alone is a false friend in pipelines — without pipefail, bad-cmd | tee out.log reports success.
I keep a few sibling references nearby for the tools Bash glues together: the awk and sed cheat sheet for stream editing inside those pipes, and the Git cheat sheet for when the script is wrapping version control. Bash is the connective tissue; those are the muscles.
Quick Reference
${var:-x}default ·${var:?msg}require ·${var%.ext}strip suffix ·${var//a/b}replace all[[ -f f ]]file ·[[ $s == *.log ]]glob ·[[ $s =~ re ]]regex ·(( n > 10 ))arithmeticfor f in *.txt; do …; done·while IFS= read -r line; do …; done < file2>&1merge streams ·> /dev/null 2>&1silence ·$(cmd)capture ·<(cmd)as-fileset -euo pipefailstrict mode ·trap 'cleanup' EXITguaranteed cleanup
Bash rewards a small, well-chosen vocabulary. Learn these dozen idioms cold, add the strict-mode header by reflex, and quote everything — that covers the overwhelming majority of what scripts in the wild actually do.
Made by Toolora · Updated 2026-06-13