Cheat Sheet Library

Bash Scripting Cheat Sheet

Writing Bash scripts: safe script setup, variables, parameter expansion, tests, loops, arrays, functions, redirection and error handling. Targets Bash 5.2 (most of it works from Bash 4; macOS still ships 3.2, so install a newer Bash there).

  • 146 entries
  • 14 sections
  • Updated
  • Click a command to see all its options

146 entries

On this page

Script Skeleton #

#!/usr/bin/env bash
set -euo pipefail                 # strict mode (see the caveats below)

script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

main() {
  echo "Running from $script_dir with ${#} argument(s)"
}

main "$@"
CodeWhat it does
#!/usr/bin/env bashShebang: run with the first bash on PATH (portable, unlike #!/bin/bash on macOS/BSD)
set -eExit when a command fails. Not triggered inside if/while conditions or by a && b / a || b (except the last command)
set -uUsing an unset variable is an error. Use ${var:-} when "maybe unset" is fine
set -o pipefailA pipeline fails if any command in it fails, not only the last one
Make the script executable (once)
./script.sh arg1 arg2Run it (uses the shebang)
bash script.shRun it with bash directly (no needed)
/ . lib.shRun a file in the current shell (load functions and variables)
bash -n script.shCheck the syntax without running anything

Variables & Quoting #

CodeWhat it does
name="Milan"Assign. No spaces around = (name = x runs a command called name)
Use a variable. Always double-quote it
Braces mark where the name ends
Single quotes: everything is literal, no expansion
Escape ", $ and backslash inside double quotes with a backslash
files=$(ls -1 | wc -l)Store a command's output
readonly MAX=10Constant: assigning again is an error
local count=0Variable that only exists inside the current function
export DB_HOST=localhostMake it visible to programs the script starts
DEBUG=1 ./run.shSet a variable for one command only
declare -i n=5Integer variable: n+=2 adds instead of appending
unset nameDelete a variable

Parameter Expansion #

Examples use file="photo.final.JPG" and path="/home/milan/notes.txt".

CodeWhat it does
${var:-default}Value of var, or default if it is unset or empty
${var:=default}Same, and also assign default to var
${var:?missing var}Exit the script with this error if var is unset or empty
${var:+yes}yes if var is set and not empty, else nothing
${#var}Length in characters
${path#*/}Remove the shortest match from the start: home/milan/notes.txt
${path##*/}Remove the longest match from the start: notes.txt (like basename)
${path%/*}Remove the shortest match from the end: /home/milan (like dirname)
${file%%.*}Remove the longest match from the end: photo
${file%.*} / ${file##*.}Name without extension photo.final / extension JPG
${var/old/new}Replace the first match
${var//old/new}Replace all matches
${var^^} / ${var,,}Uppercase / lowercase (Bash 4+)
${var^}Uppercase the first character
${var:0:5}Substring: 5 characters from position 0
${var: -3}Last 3 characters (the space before - is required)

Special Variables & Arguments #

CodeWhat it does
$0Name of the script as it was called
$1 .. $9, ${10}Positional arguments (braces from 10 on)
$#Number of arguments
"$@"All arguments, each one kept as a separate word (use this)
"$*"All arguments joined into one string (with the first char of IFS)
$?Exit code of the last command (0 = success)
$$PID of the script itself
$!PID of the last background job (cmd &)
$LINENO / $FUNCNAMECurrent line number / current function name
shift / shift 2Drop the first 1 / 2 arguments ($2 becomes $1)
set -- a b cReplace the arguments with a b c

Options with getopts (short flags like -v -o out.txt):

verbose=0 out="result.txt"
while getopts ":vo:h" opt; do      # o: means -o takes a value
  case $opt in
    v)  verbose=1 ;;
    o)  out=$OPTARG ;;
    h)  echo "usage: $0 [-v] [-o file] files..."; exit 0 ;;
    :)  echo "-$OPTARG needs a value" >&2; exit 1 ;;
    \?) echo "unknown option -$OPTARG" >&2; exit 1 ;;
  esac
done
shift $((OPTIND - 1))              # "$@" now holds the remaining arguments

Conditionals #

CodeWhat it does
if [[ cond ]]; then ...; elif ...; else ...; fiIf statement
[[ ... ]]Bash test: no word splitting, supports &&, ||, == patterns and =~
[ ... ]Old POSIX test (sh): quote every variable and use -a/-o sparingly
(( a > b ))Numeric comparison with normal math operators
cmd && echo okRun the second command only if the first succeeds
cmd || echo failedRun the second command only if the first fails
if grep -q x file; thenAny command works as a condition (exit code 0 = true)
! cmdNegate an exit code
String & number testTrue when
-z "$s"String is empty
-n "$s"String is not empty
$a == "$b"Strings are equal (!= = not equal)
$s == *.txtGlob match ([[ ]] only, pattern unquoted)
$s =~ ^[0-9]+$Regex match, groups end up in BASH_REMATCH
$a -eq $bNumbers are equal (-ne = not equal)
-lt -le -gt -geLess / less-or-equal / greater / greater-or-equal
File testTrue when
-e pathPath exists
-f pathIs a regular file
-d pathIs a directory
-r / -w / -xReadable / writable / executable
-s pathFile exists and is not empty
-L pathIs a symbolic link
a -nt bFile a is newer than b
case $1 in
  start|up)  echo "starting" ;;
  stop)      echo "stopping" ;;
  *.txt)     echo "a text file" ;;
  *)         echo "usage: $0 start|stop" >&2; exit 1 ;;
esac

Loops #

CodeWhat it does
for x in a b c; do echo "$x"; doneLoop over a list
for i in {1..5} / {0..20..5}Brace range / range with step (no variables inside braces)
for ((i = 0; i < n; i++))C-style loop (works with variables)
for f in ./*.txt; do ...; doneLoop over files (safe with spaces in names)
for f in ./*.{jpg,png}Brace expansion: several patterns
for arg in "$@"Loop over the script's arguments
while IFS= read -r line; do ...; done < fileRead a file line by line, exactly as written
while IFS= read -r line || [[ -n $line ]]; doSame, but also reads a last line without a trailing newline
while (( n < 10 )); do ...; doneLoop while a condition is true
until ping -c1 host &>/dev/null; do sleep 2; doneLoop until a command succeeds
break / continueLeave the loop / skip to the next round (break 2 = 2 levels)
shopt -s nullglobA glob with no matches becomes nothing instead of the literal *.txt

Arithmetic #

CodeWhat it does
Arithmetic expansion: 11 (integers only)
$(( 7 / 2 )) / $(( 7 % 2 ))Integer division 3 / remainder 1
$(( 2 ** 10 ))Power: 1024
(( count += 1 ))Change a variable (no $ needed inside)
(( n > 5 )) && echo bigArithmetic test: exit code 0 if the result is not 0
n=$(( n + 1 ))Increment, safe with set -e
$(( 16#ff ))Other bases: 255
Decimals need an external tool: 3.50

Arrays #

CodeWhat it does
arr=(one two "three four")Create an indexed array
arr+=(five)Append
One element (index starts at 0)
Last element
"${arr[@]}"All elements, each one a separate word
${#arr[@]}Number of elements
"${!arr[@]}"All indexes (or keys)
"${arr[@]:1:2}"Slice: 2 elements starting at index 1
unset 'arr[1]'Remove an element (indexes are not renumbered)
for x in "${arr[@]}"; do ...; doneIterate over the values
mapfile -t lines < fileRead a file into an array, one line per element
declare -A user=([name]=Milan [city]=Antwerp)Associative array (key/value, Bash 4+)
user[role]=devAdd or change a key
for k in "${!user[@]}"; do echo "$k=${user[$k]}"; doneIterate over keys and values (order not guaranteed)
[[ -v user[name] ]]Key exists

Functions #

greet() {
  local name=${1:-world}            # $1, $2... are the function's own arguments
  echo "Hello, $name"               # output = the "return value"
}
msg=$(greet Milan)                  # capture the output: "Hello, Milan"

is_even() { (( $1 % 2 == 0 )); }    # exit code = true/false
if is_even 4; then echo "even"; fi

die() { echo "error: $*" >&2; exit 1; }
[[ -f config.json ]] || die "config.json not found"
CodeWhat it does
return 1Leave the function with an exit code (0-255 only, not data)
+ $(func)Return data by printing it and capturing the output
local -n ref=$1Name reference: change the caller's variable by name (Bash 4.3+)
declare -f nameShow a function's code

Substitution & Redirection #

CodeWhat it does
today=$(date +%F)Command substitution: insert a command's output
diff <(sort a.txt) <(sort b.txt)Process substitution: use a command's output as a file
while read -r x; do ...; done < <(cmd)Loop over output without a subshell (variables survive)
cmd > out.txt / cmd >> out.txtWrite / append stdout
cmd 2> err.txtWrite stderr
cmd &> all.txtstdout and stderr to the same file
cmd > all.txt 2>&1Same, POSIX style (order matters: file first, then 2>&1)
Print to stderr (for error messages)
exec > log.txt 2>&1Redirect everything the rest of the script prints
tr a-z A-Z <<< "$text"Here-string: feed one string as stdin
cat <<EOF > config.ini              # variables and $(...) are expanded
user=$USER
date=$(date +%F)
EOF

cat <<'EOF'                         # quoted 'EOF': everything stays literal
Price: $10, run $(this) later
EOF

<<-EOF also strips leading tabs (not spaces), so the here-doc can be indented.

Errors, Exit Codes & Traps #

CodeWhat it does
exit 0 / exit 1End the script: 0 = success, 1-255 = failure
cmd || exit 1Stop if one command fails
if ! cmd; then ...; fiHandle a failure yourself
command -v jq >/dev/null || die "jq is required"Check that a program is installed
trap cleanup EXITRun cleanup whenever the script ends (success, error, exit, Ctrl+C)
trap 'echo "failed at line $LINENO" >&2' ERRRun code when a command fails (add set -E so functions inherit it)
trap '' INTIgnore Ctrl+C
tmp=$(mktemp) / tmp=$(mktemp -d)Create a unique temporary file / folder
"${PIPESTATUS[@]}"Exit codes of every command in the last pipeline
tmpdir=$(mktemp -d)
cleanup() { rm -rf "$tmpdir"; }
trap cleanup EXIT                   # temp folder is removed even on errors
curl -fsSL "$url" -o "$tmpdir/file.zip"

Input & Debugging #

CodeWhat it does
read -r -p "Name: " nameAsk for input (-r keeps backslashes as typed)
read -rs -p "Password: " pw; echoHidden input (-s = no echo)
read -r -t 10 -p "Continue? " ansGive up after 10 seconds
[[ $ans == [yY]* ]]Accept y, Y, yes, Yes...
read -ra parts <<< "$line"Split a line into an array on whitespace
IFS=, read -ra parts <<< "a,b,c"Split on a custom separator
bash -x script.shRun and print every command before it runs
set -x / set +xTurn tracing on / off for part of a script
PS4='+ ${LINENO}: 'Show line numbers in the -x trace
shellcheck script.shStatic checker that catches quoting bugs and pitfalls (install it, use it)

Common Pitfalls #

WrongRightWhy
Unquoted variables are split on spaces and globbed
for f in $(ls)for f in ./* output breaks on spaces and newlines in names
[ $a == $b ][[ $a == "$b" ]]An empty $a gives "unary operator expected"
[ "$x" > 5 ](( x > 5 )) or [[ $x -gt 5 ]]> in [ ] redirects and creates a file named 5
while read -r l; do ...; done < fThe piped loop runs in a subshell, so n is lost
local out=$(cmd)local out; out=$(cmd)local hides the failure of cmd from set -e
(( count++ )) with set -ecount=$((count + 1))(( )) that evaluates to 0 returns exit code 1, so set -e stops the script
If fails you delete files in the wrong folder

Tips #

  • Quote every expansion ("$var", "$@", "${arr[@]}") unless you know why not; that removes most Bash bugs.
  • Start scripts with set -euo pipefail, but don't rely on it alone: check important commands explicitly with || die "...".
  • Send messages to stderr (>&2) and keep stdout for real output, so the script works in pipes.
  • Use printf '%s\n' "$var" instead of echo for data that may start with - or contain backslashes.
  • Run shellcheck on every script (there are VS Code extensions for it) before you trust it.
  • When a script grows past about 100 lines or needs real data structures, consider Python instead.