A quick-reference cheatsheet for Bash scripting and the command line. Bash is both an interactive shell and a scripting language available on virtually every Unix-like system. Scripts start with #!/usr/bin/env bash and should be made executable with chmod +x script.sh.
Variables are untyped strings by default. No spaces around = when assigning. Always double-quote variable expansions ("$var") to prevent word splitting and glob expansion on values that contain spaces or special characters.
name="Alice"
echo "$name" # always quote expansions
echo "${name}_suffix" # braces disambiguate the variable name
${var:-default} # use default if var is unset or empty
${var:=default} # assign default if var is unset or empty
# without the colon: only triggers when var is unset, not empty
readonly PI=3.14
export PATH="$HOME/bin:$PATH"
export makes the variable available to child processes
$0 # script name
$1..$9 # positional args
$@ # all args (as array)
$# # arg count
$? # last exit code
$$ # current PID
count=$((count + 1))
((i++))
# $((...)) is POSIX; use it instead of let or expr
Bash provides built-in string manipulation via parameter expansion โ no need to invoke external tools like sed or awk for simple operations. Command substitution captures the output of a command into a variable.
${#var}
${var:offset:length}
# e.g. ${str:2:4} โ starts at index 2, takes 4 chars
${var/find/replace} # first match
${var//find/replace} # all matches
${var#prefix} # strip shortest prefix match
${var##longest_prefix} # strip longest prefix match
${var%suffix} # strip shortest suffix match
${var%%longest_suffix} # strip longest suffix match
# strips shortest match; ## strips longest
${var^^} # uppercase
${var,,} # lowercase
files=$(ls -1 *.txt)
today=$(date +%Y-%m-%d)
echo "Backed up on $today"
prefer $(...) over backticks โ nestable and easier to read
Bash conditions test exit codes โ 0 means true, non-zero means false. Use [[ ... ]] (Bash built-in) instead of [ ... ] (POSIX test) โ it handles empty strings and spaces safely without quoting tricks, and supports &&, ||, and regex matching.
if [[ "$1" == "start" ]]; then
echo "starting"
elif [[ "$1" == "stop" ]]; then
echo "stopping"
else
echo "unknown command"
fi
-z "$str" # empty string
-n "$str" # non-empty string
-f file # is a regular file
-d dir # is a directory
-e path # path exists
-r / -w / -x # readable / writable / executable
for file in *.txt; do
echo "Processing $file"
done
for i in {1..5}; do echo $i; done
for ((i=0; i<10; i++)); do echo $i; done
while IFS= read -r line; do
echo "$line"
done < input.txt
IFS= preserves leading/trailing whitespace; -r prevents backslash interpretation
case "$env" in
prod) echo "production" ;;
staging) echo "staging" ;;
dev|*) echo "development" ;;
esac
| separates multiple patterns per case; * is a catch-all
Functions in Bash are just named command groups. Arguments are accessed as $1, $2, etc. โ there is no parameter list. Use local to scope variables to the function; without it, all variables are global by default.
greet() {
local name="${1:-World}"
echo "Hello, $name!"
}
greet "Alice" # Hello, Alice!
greet # Hello, World!
is_even() {
(( $1 % 2 == 0 )) # exit code 0 = true
}
if is_even 4; then echo "even"; fi
# capture output instead
get_user() { echo "alice"; }
user=$(get_user)
functions return exit codes 0โ255, not values; capture stdout for strings
Bash supports indexed and associative arrays. Always quote array expansions with "${arr[@]}" to preserve elements that contain spaces. "${arr[*]}" joins all elements into one string.
fruits=("apple" "banana" "cherry")
echo "${fruits[0]}" # apple
echo "${fruits[@]}" # all elements
echo "${#fruits[@]}" # length: 3
fruits+=("date") # append
for f in "${fruits[@]}"; do echo "$f"; done
declare -A colors
colors[red]="#ff0000"
colors[green]="#00ff00"
echo "${colors[red]}"
for key in "${!colors[@]}"; do
echo "$key = ${colors[$key]}"
done
Every process has three standard file descriptors: stdin (0), stdout (1), and stderr (2). Redirection operators route these streams to files or other commands. Pipes chain commands so stdout of one becomes stdin of the next.
cmd > out.txt # overwrite
cmd >> out.txt # append
cmd 2> err.txt # stderr only
cmd > out.txt 2>&1 # stdout + stderr together
cmd > /dev/null 2>&1
send both streams to the void
ps aux | grep nginx | awk '{print $2}'
cat access.log | sort | uniq -c | sort -rn | head -20
cat <<EOF
line one
line two
EOF
# indented here-doc (Bash 4+, strips leading tabs)
cat <<-EOF
line one
line two
EOF
read -rp "Enter name: " name
read -rs -p "Password: " pass # -s = silent, no echo
-r = raw, no backslash processing; -p = inline prompt
Bash can run commands in the background, wait for them, and check their exit codes. The trap built-in lets you handle signals and run cleanup code when a script exits.
long_task &
pid=$!
wait $pid # wait for specific PID
$! holds the PID of the last background command
cmd && echo "ok" || echo "failed"
cmd; echo "exit: $?"
tmpfile=$(mktemp)
trap "rm -f $tmpfile" EXIT
# also handle Ctrl-C
trap "echo 'interrupted'; exit 1" INT TERM
EXIT fires on any exit โ normal or signal; safest place for cleanup
A few idioms that make scripts more robust and maintainable. set -euo pipefail should be at the top of almost every script โ it prevents silent failures that are hard to debug later.
#!/usr/bin/env bash
set -euo pipefail
# -e exit on error
# -u error on unset variables
# -o pipefail pipe fails if any command fails
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# use $DIR to reference files relative to the script
if ! command -v docker &> /dev/null; then
echo "docker is required but not installed" >&2
exit 1
fi
if [[ "$EUID" -ne 0 ]]; then
echo "Run as root" >&2
exit 1
fi
while getopts "vf:" opt; do
case $opt in
v) verbose=true ;;
f) file="$OPTARG" ;;
*) echo "Usage: $0 [-v] [-f file]" >&2; exit 1 ;;
esac
done
shift $((OPTIND - 1)) # remaining positional args