cheat-sheets

Bash

Bash is the default shell on most Linux servers and the language most homelab glue is written in. This sheet covers the syntax you reach for daily; ready-made patterns are in Bash Recipes, linting in ShellCheck and shfmt. Other shells: Zsh, Fish, Nushell and Shell Comparison. For text tools see Grep, SED Cheat-Sheet, AWK and jq.

Documentation: Bash Reference Manual

Script Skeleton #

bash
#!/usr/bin/env bash
set -Eeuo pipefail
IFS=$'\n\t'

main() {
  echo "hello ${1:-world}"
}

main "$@"

Strict Mode #

OPTIONDESCRIPTION
set -eExit when a command fails (not inside if, &&, || or while conditions)
set -uError on unset variables
set -o pipefailA pipeline fails if any stage fails, not just the last
set -EERR trap is inherited by functions and subshells
set -xTrace every command (debugging)
set +eTurn an option off again
shopt -s nullglobUnmatched globs expand to nothing instead of themselves
shopt -s globstar** matches recursively
command || trueAllow one command to fail under set -e

set -e has many exceptions and is not a substitute for checking the commands that matter. bash -n script.sh checks syntax without running it.

Variables #

SYNTAXDESCRIPTION
NAME=valueAssign (no spaces around =)
"$NAME" / "${NAME}"Expand (always quote)
export NAME=valuePass to child processes
readonly NAME=valueConstant
local NAME=valueFunction-scoped variable
declare -i N=5Integer
declare -r, declare -xRead-only, export
$(command)Command substitution
$((A + B))Arithmetic
$0, $1…$9, ${10}Script name and positional arguments
$#Number of arguments
"$@"All arguments, each one preserved (use this)
"$*"All arguments as one string
$?Exit code of the last command
$$PID of the shell
$!PID of the last background job
$RANDOMRandom integer 0-32767
$SECONDSSeconds since the shell started
$LINENO, $FUNCNAME, $BASH_SOURCEDebug helpers
read -r -p "Name: " NAMEPrompt for input
read -r -s -p "Password: " PWSilent input

Quoting #

SYNTAXDESCRIPTION
"$var"Expands variables and commands, no word splitting
'$var'Literal text, nothing expands
$'a\tb\n'ANSI-C quoting with escapes
\$Escape a single character
"$(cmd "$arg")"Quotes nest inside $()

Unquoted $var is split on whitespace and glob-expanded. Quote every expansion unless you specifically want splitting.

Arrays #

bash
files=(a.txt "b c.txt" c.txt)
files+=(d.txt)
echo "${files[0]}"        # first element
echo "${files[@]}"        # all elements (quote it)
echo "${#files[@]}"       # length
echo "${files[@]:1:2}"    # slice: offset 1, length 2
for f in "${files[@]}"; do echo "$f"; done

mapfile -t lines < <(grep -v '^#' list.txt)   # file/command to array

declare -A ports=([web]=80 [ssh]=22)          # associative (Bash 4+)
ports[db]=5432
echo "${ports[web]}"
echo "${!ports[@]}"       # keys
echo "${ports[@]}"        # values
[[ -v ports[ssh] ]] && echo "key exists"
unset 'ports[web]'
for k in "${!ports[@]}"; do echo "$k=${ports[$k]}"; done

Parameter Expansion #

SYNTAXDESCRIPTION
${VAR:-default}Use default if unset or empty
${VAR:=default}Assign default if unset or empty
${VAR:?message}Exit with message if unset or empty
${VAR:+value}value if VAR is set, else empty
${#VAR}Length
${VAR:2:5}Substring (offset, length)
${VAR#pattern}Strip shortest prefix match
${VAR##pattern}Strip longest prefix match (${path##*/} = basename)
${VAR%pattern}Strip shortest suffix match
${VAR%%pattern}Strip longest suffix match (${file%.*} = no extension)
${VAR/old/new}Replace first match
${VAR//old/new}Replace all matches
${VAR/#old/new}, ${VAR/%old/new}Replace at start / end
${VAR^^}, ${VAR,,}Upper / lower case (Bash 4+)
${!PREFIX@}Names of variables starting with PREFIX
${!REF}Indirect expansion (value of the variable named by REF)

Conditionals #

bash
if [[ -f "$file" && -r "$file" ]]; then
  echo "readable file"
elif [[ -d "$file" ]]; then
  echo "directory"
else
  echo "missing"
fi

case "$1" in
  start|up)  echo "starting" ;;
  stop)      echo "stopping" ;;
  *)         echo "usage: $0 {start|stop}" >&2; exit 2 ;;
esac

[[ "$a" == prefix* ]]        # glob match (unquoted right side)
[[ "$a" =~ ^[0-9]+$ ]]       # regex match, groups in BASH_REMATCH
(( count > 3 )) && echo big  # arithmetic test

Prefer [[ ]] over [ ] in Bash: no word splitting, supports &&, \|\|, == patterns and =~.

Test Operators #

OPERATORTRUE WHEN
-e <PATH>Exists
-f <PATH>Regular file
-d <PATH>Directory
-L <PATH>Symlink
-r, -w, -x <PATH>Readable, writable, executable
-s <PATH>Exists and is not empty
<A> -nt <B>, <A> -ot <B>A newer / older than B
-z "$s"String is empty
-n "$s"String is not empty
"$a" == "$b", !=String equal / not equal
"$a" < "$b"String sorts before (inside [[ ]])
-eq -ne -lt -le -gt -geInteger comparisons
-v NAMEVariable is set
! <EXPR>Negate

Loops #

bash
for i in 1 2 3; do echo "$i"; done
for i in {1..10..2}; do echo "$i"; done          # 1 3 5 7 9
for ((i = 0; i < 5; i++)); do echo "$i"; done
for f in *.log; do gzip "$f"; done

while [[ $n -lt 5 ]]; do ((n++)); done
until ping -c1 -W1 host >/dev/null; do sleep 2; done

while IFS= read -r line; do echo "$line"; done < file.txt
break; continue          # leave the loop / skip to the next iteration

((n++)) returns exit code 1 when n was 0, which kills a set -e script. Use n=$((n + 1)) instead.

Functions #

bash
greet() {
  local name="${1:?name required}"
  echo "Hello, $name"
  return 0                 # return codes only; output goes to stdout
}

greeting="$(greet Bob)"    # capture stdout
greet Bob || echo "failed"

Variables are global unless declared local. Functions share the script’s $@ only if you pass it (main "$@").

trap #

bash
cleanup() { rm -rf -- "$tmpdir"; }
trap cleanup EXIT                         # runs on any exit
trap 'echo "line $LINENO failed" >&2' ERR # with set -E
trap 'echo interrupted; exit 130' INT TERM
trap - EXIT                               # remove a trap
trap -l                                   # list signals
SIGNALMEANING
EXITShell exit (pseudo-signal, always runs)
ERRA command failed
INTCtrl+C
TERMDefault kill
HUPTerminal closed
DEBUGBefore every command

SIGKILL (kill -9) cannot be trapped, so cleanup does not run.

getopts #

bash
usage() { echo "Usage: $0 [-v] [-n NAME] FILE..." >&2; exit 2; }

verbose=0 name=""
while getopts ":vn:h" opt; do
  case "$opt" in
    v) verbose=1 ;;
    n) name="$OPTARG" ;;
    h) usage ;;
    :) echo "-$OPTARG needs an argument" >&2; usage ;;
    \?) echo "unknown option -$OPTARG" >&2; usage ;;
  esac
done
shift $((OPTIND - 1))     # remaining args are the positional ones

getopts handles short options only. For --long options see the template in Bash Recipes.

Here-docs and Here-strings #

bash
cat <<EOF                 # variables expand
Hello $USER
EOF

cat <<'EOF'               # quoted delimiter: no expansion
Literal $USER
EOF

cat <<-EOF                # <<- strips leading TABs (not spaces)
	indented
	EOF

grep -c foo <<< "$text"   # here-string
ssh host bash -s <<'EOF'  # run a script remotely
uptime
EOF

Redirection #

SYNTAXDESCRIPTION
cmd > fileStdout to file (overwrite)
cmd >> fileAppend stdout
cmd 2> fileStderr to file
cmd > file 2>&1Both to file (order matters)
cmd &> fileBoth to file (Bash shorthand)
cmd 2>&1 | otherPipe stdout and stderr
cmd |& otherSame, shorter
cmd < fileRead stdin from file
cmd > /dev/null 2>&1Discard all output
cmd >&2Send stdout to stderr (error messages)
cmd | tee fileShow and save output
cmd | tee -a fileShow and append
exec > >(tee -a log.txt) 2>&1Log everything the rest of the script prints
set -o noclobberRefuse > over existing files (override with >|)

Process Substitution #

SYNTAXDESCRIPTION
diff <(sort a) <(sort b)Treat command output as a file
while read -r l; do ...; done < <(cmd)Loop over output without a subshell (variables survive)
tee >(grep ERR > errors.log) > all.logFeed output to a command as a file

cmd \| while read ... runs the loop in a subshell, so variables set inside are lost. Use < <(cmd) instead.

Exit Codes #

CODEMEANING
0Success
1General error
2Misuse of shell builtin or bad usage (conventional for usage errors)
126Found but not executable
127Command not found
128+NKilled by signal N (130 = Ctrl+C, 137 = SIGKILL, 143 = SIGTERM)
bash
if ! grep -q foo file; then echo "no match"; fi
cmd; rc=$?                       # capture the code
echo "${PIPESTATUS[@]}"          # exit code of each pipeline stage
exit 3                           # your own code

Misc #

COMMANDDESCRIPTION
source file.sh / . file.shRun a file in the current shell
"$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")"Directory of the running script
command -v <CMD> >/dev/nullTest whether a command exists (portable)
printf '%s\n' "$var"Safe printing (echo mangles -n, -e and backslashes)
printf '%05d %-10s\n' 7 nameFormatted output
$(< file)Read a whole file without cat
waitWait for background jobs
cmd &Run in the background
nohup cmd &Keep running after logout
timeout 30 cmdKill after 30 seconds
env -i bash --noprofile --norcClean shell for testing
type -a <CMD>Show every definition of a name
Search all sheets

Tip: / or Ctrl/Cmd+K to open, Esc to close.