The Bash shell
The ubiquitous shell on Linux systems, warts and all. Not to be confused with /bin/sh
, which is often a less-fully-featured shell.
Cheatsheet
Strings
${s%suffix} # remove suffix
${s#prefix} # remove prefix
${s/from/to} # replace first occurrence
${s//from/to} # replace all occurrences
${s:-whatever} # take value of $s or 'whatever' if unset
${x:-default} # variable or default value
Redirection
# redirect stdout to stderr
echo hello >&2
# redirect both stdout and stderr
echo hello &>log.txt
Order matters! echo hello >log.txt 2>&1
redirects both standard output and standard error to the file log.txt
. Think of the redirection operator as an assignment.
Heredocs
cat << EOF
Line 1
Line 2
EOF
cat << "EOF"
No parameter expansion is done.
EOF
cat <<- EOF
The <<- operator lets you indent your code.
EOF
s=$(cat << EOF
Line 1
Line 2
EOF
)
Control flow
for p in *.csv; do
# ...
done
if [[ ... ]]; then
fi
case "$x" in
pattern)
;;
*)
;;
esac
Conditions
[[ "$s" == "prefix"* ]] # string has prefix
[[ -z "$s" ]] # string is empty
[[ -n "$s" ]] # string is not empty
[[ -e "$f" ]] # file exists
[[ -L "$f" ]] # symlink exists (regardless of whether target exists)
[[ -d "$f" ]] # file is directory
[[ -f "$f" ]] # file is regular file
[[ -h "$f" ]] # file is a symlink
[[ -x "$f" ]] # file is executable
(( x == 100 )) # no needs for '$'
Arrays
arr=()
arr+=("$x")
${#arr[@]} # array length
${arr[@]:1} # slice starting from first element
for x in "${arr[@]}"; do .. done
Associative arrays
declare -A mymap
mymap["key"]="value"
echo "${mymap[key]}"
unset mymap["key"]
# check if a key is set (even if empty)
# NOTE that it's `mymap["key"]`, not `${mymap["key"]}`
[[ -v mymap["key"] ]] && echo exists
# iterate over keys
for key in "${!mymap[@]}"; do
# ...
done
# iterate over values
for key in "${mymap[@]}"; do
# ...
done
# pre-declare and make read-only (-r)
declare -A -r mymap=(
["key1"]="value1"
["key2"]="value2"
)
Temporary files and directories
tmp="$(mktemp)"
tmpdir="$(mktemp -d)"
# with specific extension
# https://stackoverflow.com/a/59638023/3934904
tmpdir="$(mktemp -d)"
tmp="$tmpdir/test.md"
Clean-up functions
cleanup() { ... }
trap cleanup EXIT
Get script path
SCRIPT_PATH="$(realpath "$0")"
Temporarily allow unset variables
if [[ $- = *u* ]]; then
restore_unset="1"
else
restore_unset="0"
fi
set +u
# ...
if (( restore_unset == 1 )); then
set -u
fi
tar
# create a gzipped archive of a directory with relative paths
# NOTE: '.' at the end
tar -czf OUTFILE --directory=DIR .
# exclude a directory
# NOTE: exclude path is relative to DIR, not the current working directory
tar -czf OUTFILE --exclude=EXCLUDE --directory=DIR .
# extract tarball
tar -xzf TARBALL -C DIR
Tips
set -euo pipefail
-e
: exit when a command exits with non-zero status-u
: exit on undefined variable-o pipefail
: set return value ofcmd1 | cmd2 | cmd3
to last command to exit with non-zero status (instead of exit status ofcmd3
by default)- Though this is not always what you want; see "Be careful" below.
- Use
${PIPESTATUS[@]}
to retrieve the exit status of each command.
set -x
to print each command as it is run- Wrap the script in
main() { ... }
and putmain "$@"
at the bottom.- Forces Bash to load and parse entire file before beginning execution.
- Use
/usr/bin/env bash
as the shebang for your scripts, not/bin/bash
. On macOS,/bin/bash
is an ancient version and some users have installed a newer version via Homebrew.
Be careful
- If you press Ctrl+Z in a compound command (
cmd1 && cmd2
, afor
loop, etc.), then when you runfg
only the command that was interrupted will be resumed, not the whole compound command. Try it yourself:sleep 5 && echo done
ln -sf target name
is supposed to replace the symlink atname
with one pointing totarget
, but ifname
is a directory, it instead creates a symlinkname/target
that points attarget
.-o pipefail
can cause flaky failures with commands that truncate the input (likehead
); incmd | head
, ifhead
exits beforecmd
is finished writing, the kernel will deliver aSIGPIPE
signal tocmd
, killing it with a non-zero exit status.MYVAR=hello echo "$MYVAR"
seems like it should work, but doesn't;MYVAR
is defined when the program executes, but not when the command line is parsed. You can doMYVAR=hello; echo "$MYVAR"
(note the semicolon), though this leavesMYVAR
defined in your shell afterecho
exits.
Posts
- "Robustly parsing flags in Bash scripts" (August 2025)
Bibliography
Reference:
- ⭐️ Bash guide (Greg's Wiki)
- Bash reference manual (GNU)
bash(1)
manpage- Bash cheatsheet (devhints.io)
Writing robust scripts:
- "Writing Shell Scripts" (Signs of Triviality, 2016)
- "How to write idempotent Bash scripts" (Fatih Arslan, 2019)
Minutiae:
- "Seven Surprising Bash Variables" (zwischenzugs, 2019)