cheat sheet
Bash
Comprehensive Bash scripting reference covering variables, parameter expansion, control flow, functions, arrays, string manipulation, arithmetic, traps, process substitution, and the Bash 5.3 in-shell command substitution forms.
Bash — Shell Scripting Reference
What it is
Bash (Bourne Again SHell) is the default interactive shell on most Linux distributions and macOS, and the de-facto standard scripting language for Unix system automation. It is maintained by the GNU Project and released under the GPL. Reach for Bash when you need to orchestrate other programs, automate file operations, or write portable glue scripts; for complex data manipulation, arithmetic-heavy logic, or anything that benefits from data structures, prefer Python or another full language. Redirection and pipes are covered separately in the Bash Redirection & Pipes cheat sheet.
The current stable release is Bash 5.3 (July 2025), which introduces fork-free in-shell command substitution (${ cmd; } and ${| cmd; }), the GLOBSORT variable, read -E with Readline completion, and C23 conformance. Examples below note the minimum required version where a feature post-dates Bash 4. macOS still ships Bash 3.2 by default; install Bash 5 via Homebrew (brew install bash) to use anything from Bash 4 onward.
Configuration
Bash reads a different set of startup files depending on whether it is invoked as a login shell, an interactive non-login shell, or non-interactively (a script). The wrong file can run twice, or not at all, when a user logs in through a desktop manager versus a terminal — knowing the order is the first step in debugging "my alias / PATH / prompt isn't loading" problems.
| File | Read by |
|---|---|
/etc/profile | Login shells (system-wide) |
/etc/profile.d/*.sh | Sourced from /etc/profile |
/etc/bash.bashrc | Interactive shells (Debian/Ubuntu only; not POSIX-standard) |
~/.bash_profile | Login shells — preferred over .profile if present |
~/.bash_login | Login shells — fallback if .bash_profile is missing |
~/.profile | Login shells — fallback if neither of the above exists |
~/.bashrc | Interactive non-login shells |
~/.bash_logout | Read when a login shell exits |
~/.inputrc | Readline key-bindings and completion options |
~/.bash_history | Command history (path controlled by $HISTFILE) |
$BASH_ENV | Non-interactive shells source the file this points to |
$ENV | POSIX-mode non-interactive shells source this file |
Output: (none — file reference table)
# Common pattern: source ~/.bashrc from ~/.bash_profile so both
# login and interactive shells get the same environment.
# Put in ~/.bash_profile:
[[ -f ~/.bashrc ]] && source ~/.bashrc
# Inspect which startup files actually fired (Bash 5.0+)
bash -lic 'echo "$BASH_SOURCE"; shopt'
# Show current shell's effective options
shopt | grep on$
set -o
Output:
autocd off
cdable_vars off
checkwinsize on
…
# History-related variables — usually set in ~/.bashrc
HISTSIZE=10000 # in-memory history entries
HISTFILESIZE=20000 # entries kept in ~/.bash_history
HISTCONTROL=ignoreboth # ignore dups + commands starting with space
HISTTIMEFORMAT='%F %T ' # timestamp every history entry
shopt -s histappend # append on exit instead of overwriting
Output: (none — configuration assignments)
# Useful ~/.inputrc settings (Readline, used by Bash interactively)
set completion-ignore-case on # case-insensitive tab-completion
set show-all-if-ambiguous on # list matches on first Tab
set colored-stats on # color completion list by file type
set bell-style none # silence the terminal bell
"\e[A": history-search-backward # Up-arrow: search history by prefix
"\e[B": history-search-forward
Output: (none — Readline configuration)
Variables and assignment
A variable assignment in Bash must have no spaces around the =. Variable names are case-sensitive. Unquoted variable references undergo word-splitting and glob expansion; always double-quote "$var" unless you explicitly want splitting.
name="Alice Dev"
count=42
is_ready=true # Bash has no native boolean; this is just a string
echo "$name"
echo "${count}"
echo "Ready: $is_ready"
Output:
Alice Dev
42
Ready: true
# readonly — cannot be reassigned
readonly CONFIG_DIR="/etc/myapp"
# Declare with type attributes
declare -i num=10 # integer — arithmetic on assignment
declare -r LIMIT=100 # readonly
declare -l lower="ABC" # auto-lowercase
declare -u upper="abc" # auto-uppercase
echo "$lower $upper"
Output:
abc ABC
# Unset a variable
unset name
# Check if a variable is set
if [[ -v name ]]; then echo "set"; else echo "unset"; fi
Output:
unset
Parameter expansion
Parameter expansion transforms a variable's value inline, without invoking a subshell. The ${…} syntax provides defaults, substring extraction, search-and-replace, case conversion, and more.
url="https://example.com/path/file.tar.gz"
echo "${url#*/}" # remove shortest prefix match */>
echo "${url##*/}" # remove longest prefix match — basename
echo "${url%.*}" # remove shortest suffix match .* — strip last ext
echo "${url%%.*}" # remove longest suffix match — strip all dots onward
echo "${url/path/dest}" # replace first occurrence
echo "${url//e/E}" # replace all occurrences
echo "${url:8:11}" # substring: offset 8, length 11
echo "${#url}" # string length
Output:
/example.com/path/file.tar.gz
file.tar.gz
https://example.com/path/file.tar
https://example
https://example.com/dest/file.tar.gz
https://ExamplE.com/path/filE.tar.gz
example.com
35
# Defaults and fallbacks
unset PORT
echo "${PORT:-8080}" # use 8080 if PORT is unset or empty
echo "${PORT:=8080}" # assign 8080 AND use it
echo "${PORT:?must be set}" # error and exit if unset/empty
echo "${PORT:+override}" # use "override" only if PORT is set
Output:
8080
8080
8080
override
# Case conversion (Bash 4+)
word="hello world"
echo "${word^}" # Capitalise first char
echo "${word^^}" # All uppercase
echo "${word,}" # Lowercase first char
echo "${word,,}" # All lowercase
Output:
Hello world
HELLO WORLD
hello world
hello world
Special variables
Special variables are set by the shell and give access to positional parameters, process state, and the last command's status. They cannot be assigned directly (except $0 via exec).
$0 # Name / path of the current script
$1 … $9 # Positional parameters (script/function arguments)
${10} # Positional parameters with index ≥ 10
$# # Number of positional parameters
$@ # All positional parameters as separate words (quote-safe)
$* # All positional parameters as a single word (joined by $IFS)
$? # Exit status of the last foreground command
$$ # PID of the current shell
$! # PID of the most recently backgrounded job
$_ # Last argument of the previous command
$IFS # Internal field separator (default: space, tab, newline)
$LINENO # Current line number in the script
$RANDOM # Pseudo-random integer 0–32767 (reseeded each access)
$SECONDS # Seconds since the shell started
$BASH_VERSION # Bash version string
$BASHPID # PID of the current Bash process (differs from $$ in subshells)
$BASH_SOURCE # Array of source files in the call stack
$FUNCNAME # Array of function names in the call stack
$EPOCHSECONDS # Unix timestamp in seconds (Bash 5.0+)
$EPOCHREALTIME # Unix timestamp with microsecond precision (Bash 5.0+, improved 5.3)
$GLOBSORT # Sort order for pathname expansion (Bash 5.3+: name/size/mtime/numeric/none)
Output: (none — variable reference table)
#!/usr/bin/env bash
# Example: show positional parameters
echo "Script: $0"
echo "Args : $#"
echo "All : $@"
echo "First : $1"
Output:
Script: ./myscript.sh
Args : 3
All : foo bar baz
First : foo
Control flow
if / elif / else
The if statement evaluates a command's exit code; [[ … ]] is the preferred Bash test construct (vs. the POSIX [ … ]). Use -eq/-lt/-gt for integers and ==/!=/</> inside [[ ]] for strings.
score=85
if [[ $score -ge 90 ]]; then
echo "A"
elif [[ $score -ge 80 ]]; then
echo "B"
elif [[ $score -ge 70 ]]; then
echo "C"
else
echo "F"
fi
Output:
B
# File tests
file="/etc/hosts"
if [[ -f "$file" ]]; then
echo "regular file"
elif [[ -d "$file" ]]; then
echo "directory"
elif [[ -L "$file" ]]; then
echo "symlink"
fi
# Common file test operators:
# -e exists -f regular file -d directory
# -r readable -w writable -x executable
# -s size > 0 -L symlink -z string is empty
# -n string non-empty
Output:
regular file
case
case matches a value against patterns using glob syntax. It is cleaner than a long if/elif chain when branching on a string or command output.
day=$(date +%u) # 1=Mon … 7=Sun
case "$day" in
1|2|3|4|5) echo "Weekday" ;;
6) echo "Saturday" ;;
7) echo "Sunday" ;;
*) echo "Unknown" ;;
esac
Output:
Weekday
# Match on file extension
file="archive.tar.gz"
case "$file" in
*.tar.gz) echo "gzipped tar" ;;
*.tar.bz2) echo "bzip2 tar" ;;
*.zip) echo "zip archive" ;;
*) echo "other" ;;
esac
Output:
gzipped tar
Loops
for loops
The C-style for loop and the word-list for loop are the two main forms. Use {start..end} for integer sequences and $(command) for iterating over command output.
# Word-list for loop
for color in red green blue; do
echo "$color"
done
Output:
red
green
blue
# Brace expansion sequence
for i in {1..5}; do
echo "Step $i"
done
Output:
Step 1
Step 2
Step 3
Step 4
Step 5
# C-style for loop
for ((i=0; i<3; i++)); do
echo "i=$i"
done
Output:
i=0
i=1
i=2
# Iterate over files
for f in /etc/*.conf; do
echo "Config: $f"
done
Output:
Config: /etc/hosts.conf
Config: /etc/nsswitch.conf
…
# Bash 5.3+: control glob sort order via $GLOBSORT
# Format: [+-]key where + is ascending (default) and - is descending
# Keys: name, size, mtime, atime, ctime, blocks, numeric, none
GLOBSORT=-mtime # newest files first
for f in *.log; do echo "$f"; done
GLOBSORT=size # smallest first
ls -- *.log # built-in expansion is also sorted by $GLOBSORT
unset GLOBSORT # restore default (alphabetical by name)
Output:
2026-05-25.log
2026-05-24.log
2026-05-23.log
while and until
while runs while its condition is true; until runs until its condition is true (i.e. while it is false). Both support break and continue.
count=0
while [[ $count -lt 3 ]]; do
echo "count=$count"
((count++))
done
Output:
count=0
count=1
count=2
# Read lines from a file
while IFS= read -r line; do
echo ">> $line"
done < /etc/hostname
Output:
>> myhost
# Read lines from a command
while IFS= read -r line; do
echo "user: $line"
done < <(getent passwd | cut -d: -f1 | head -3)
Output:
user: root
user: daemon
user: bin
Functions
A Bash function is a named block of commands that can accept positional parameters and return an integer exit code via return. Functions share the parent shell's environment unless local is used. Variables declared local are scoped to the function and its callees.
greet() {
local name="$1"
local greeting="${2:-Hello}"
echo "$greeting, $name!"
}
greet "Alice Dev"
greet "Alice Dev" "Hi"
Output:
Hello, Alice Dev!
Hi, Alice Dev!
# Return a value via stdout capture
add() {
echo $(( $1 + $2 ))
}
result=$(add 3 7)
echo "Sum: $result"
Output:
Sum: 10
# Return success/failure for use in if
file_exists() {
[[ -f "$1" ]]
}
if file_exists "/etc/hosts"; then
echo "found"
fi
Output:
found
# Variadic function using $@
sum_all() {
local total=0
for n in "$@"; do
(( total += n ))
done
echo "$total"
}
sum_all 1 2 3 4 5
Output:
15
Arrays
Bash supports both indexed arrays (array[0], array[1], …) and associative arrays (dict[key]). Associative arrays require declare -A. Arrays are zero-indexed; negative indices count from the end (Bash 4.3+).
# Indexed array
fruits=("apple" "banana" "cherry")
echo "${fruits[0]}" # first element
echo "${fruits[-1]}" # last element
echo "${fruits[@]}" # all elements
echo "${#fruits[@]}" # number of elements
echo "${!fruits[@]}" # all indices
Output:
apple
cherry
apple banana cherry
3
0 1 2
# Append, modify, delete
fruits+=("date")
fruits[1]="blueberry"
unset fruits[2]
echo "${fruits[@]}"
Output:
apple blueberry date
# Array slice: ${array[@]:offset:length}
nums=(10 20 30 40 50)
echo "${nums[@]:1:3}"
Output:
20 30 40
# Associative array (Bash 4+)
declare -A config
config[host]="myhost"
config[port]="5432"
config[db]="appdb"
echo "${config[host]}:${config[port]}/${config[db]}"
echo "Keys : ${!config[@]}"
echo "Values: ${config[@]}"
Output:
myhost:5432/appdb
Keys : host port db
Values: myhost 5432 appdb
String manipulation
Bash provides built-in string operations through parameter expansion. For complex regex matching and capture, use =~ inside [[ ]] and access matches via $BASH_REMATCH.
str=" Hello, World! "
# Trim leading whitespace
echo "${str#"${str%%[! ]*}"}"
# Length
echo "${#str}"
# Substring: ${var:offset:length}
echo "${str:2:5}"
# Replace
echo "${str/World/Alice Dev}"
# Upper/lowercase (Bash 4+)
echo "${str^^}"
echo "${str,,}"
Output:
Hello, World!
18
Hello
Hello, Alice Dev!
HELLO, WORLD!
hello, world!
# Regex match with capture groups
text="2026-05-04"
if [[ "$text" =~ ^([0-9]{4})-([0-9]{2})-([0-9]{2})$ ]]; then
echo "Year : ${BASH_REMATCH[1]}"
echo "Month: ${BASH_REMATCH[2]}"
echo "Day : ${BASH_REMATCH[3]}"
fi
Output:
Year : 2026
Month: 05
Day : 04
# Split a string into an array using IFS
IFS=',' read -ra parts <<< "one,two,three"
echo "${parts[1]}"
echo "${parts[@]}"
Output:
two
one two three
Arithmetic
Bash integer arithmetic uses (( … )) for statements and $(( … )) for expressions. Floating-point requires an external tool like bc or awk.
a=10
b=3
echo $(( a + b ))
echo $(( a - b ))
echo $(( a * b ))
echo $(( a / b )) # integer division (truncates)
echo $(( a % b )) # modulo
echo $(( a ** b )) # exponentiation (right-associative)
Output:
13
7
30
3
1
1000
# Increment / decrement
(( a++ ))
(( b-- ))
(( a += 5 ))
echo "$a $b"
Output:
16 2
# Conditional arithmetic: (( expr )) returns 0 (true) if expr != 0
if (( a > 10 )); then echo "big"; fi
Output:
big
# Floating-point with bc
result=$(echo "scale=4; 22/7" | bc)
echo "$result"
Output:
3.1428
Process substitution
Process substitution (<(command)) presents the stdout of a command as a temporary file path, allowing commands that require file arguments to consume live command output. It differs from a pipe in that both sides run concurrently and you can use it in file-position arguments. >(command) is the write-side variant.
# diff two command outputs without temp files
diff <(sort /etc/passwd) <(sort /etc/group)
Output:
(diff output between sorted passwd and group files)
# Read from a command in a while loop (avoids a subshell, unlike pipe)
while IFS= read -r line; do
echo ">> $line"
done < <(ls /etc/*.conf 2>/dev/null)
Output:
>> /etc/hosts.conf
>> /etc/nsswitch.conf
# Tee to multiple files and a command simultaneously
echo "log line" | tee >(gzip > log.gz) >(wc -c > size.txt)
Output:
log line
In-shell command substitution (Bash 5.3+)
Classic $(command) and backtick `command` substitution always forks a subshell. Bash 5.3 (July 2025) adds two new forms that execute in the current shell — no fork, no pipe — so variable assignments, cd, and other side effects persist after the substitution completes. The variant with output capture (${ cmd; }) is dramatically faster than $(cmd) in tight loops, and the REPLY variant (${| cmd; }) is ideal for helper functions that "return" a value without the noise of a subshell.
# Output-capturing form: ${ command; } — note the leading space and trailing ;
# Captures stdout of command, just like $(command), but in the current shell.
count=${ wc -l < /etc/passwd; }
echo "users: $count"
Output:
users: 42
# REPLY form: ${| command; } — command runs in the current shell and
# expands to whatever it leaves in $REPLY. No stdout capture, no fork.
get_user() {
REPLY="alice"
}
echo "Hello, ${| get_user; }"
Output:
Hello, alice
# Side effects persist — unlike $(cd /tmp && pwd), this cd actually moves the
# parent shell. Useful for shell-level helpers; use with care in scripts.
pwd
result=${ cd /tmp && pwd; }
echo "result: $result"
pwd
Output:
/home/alice
result: /tmp
/tmp
# Performance win: tight-loop fork avoidance
# $(date +%s) forks once per iteration; ${ … ; } does not.
for i in {1..1000}; do
now=${ printf '%(%s)T\n' -1; }
done
echo "$now"
Output:
1721059200
Traps and signals
trap registers a command to run when the shell receives a signal or exits. Use EXIT to guarantee cleanup code runs regardless of how the script terminates — normal exit, error, or signal.
# Cleanup on exit
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
echo "working with $tmpfile"
# tmpfile is always deleted when the script exits
Output:
working with /tmp/tmp.XXXXXX
# Catch Ctrl+C (SIGINT)
trap 'echo "Interrupted!"; exit 130' INT
for i in {1..10}; do
echo "step $i"
sleep 1
done
Output:
step 1
step 2
Interrupted!
# ERR trap — run on any non-zero exit code
set -euo pipefail
trap 'echo "Error on line $LINENO (exit $?)"' ERR
false # triggers ERR
Output:
Error on line 4 (exit 1)
# Common signal names
trap 'handler' EXIT # script exit (any cause)
trap 'handler' INT # Ctrl+C
trap 'handler' TERM # kill / system shutdown
trap 'handler' HUP # terminal disconnect
trap 'handler' ERR # non-zero exit (with set -e)
trap '' INT # ignore Ctrl+C
trap - INT # restore default INT behaviour
Output: (none — trap definitions)
Script safety options
set -e (exit on error), set -u (treat unset variables as errors), and set -o pipefail (propagate pipe failures) are the standard "safe mode" options for production scripts.
#!/usr/bin/env bash
set -euo pipefail
# -e : exit immediately on error
# -u : treat unset variables as errors
# -o pipefail : pipeline fails if any stage fails
echo "Running safely"
# Unset variable reference would now cause an error:
# echo "$UNDEFINED" # → would exit with "unbound variable"
Output:
Running safely
# Enable xtrace for debugging
set -x # print each command before executing
ls /tmp
set +x # turn off xtrace
Output:
+ ls /tmp
<list of files>
Common pitfalls
- Unquoted variables —
rm $filesplits on spaces and expands globs; always writerm "$file". [ ]vs[[ ]]—[[ ]]is Bash-specific and safer (no word-splitting, supports&&/||/=~); use[ ]only when you need POSIX portability.==vs-eq—==compares strings;-eqcompares integers.[[ "10" == "10.0" ]]is false;(( 10 == 10 ))is true.- Pipe subshells — variables set inside a pipe stage are lost after the pipe:
echo "x" | read val; echo "$val"prints nothing. Use process substitution orlastpipeoption instead. - Integer division —
$(( 7 / 2 ))is3, not3.5; usebcorawkfor floats. $*vs$@— always use"$@"to forward arguments;$*joins all args into one string split by$IFS.set -eand functions —set -edoes not trigger inside anifcondition; a function called in anifcan fail silently.- Associative arrays require Bash 4 — macOS ships with Bash 3.2; install Bash 5 via Homebrew if you need
declare -A. The same applies to${ cmd; }(Bash 5.3+),GLOBSORT(5.3+),EPOCHSECONDS(5.0+),${var^^}(4.0+), and negative array indices (4.3+). #!/usr/bin/env bashvs#!/bin/bash—/bin/bashis 3.2 on macOS and may not exist on Alpine/BusyBox systems. Use#!/usr/bin/env bashto pick up whichever Bash is first in$PATH(e.g. the Homebrew 5.x build at/opt/homebrew/bin/bash).
Real-world recipes
Robust script header
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
Output: (none — header boilerplate; sets up safety options and cleanup)
Parse flags with getopts
usage() { echo "Usage: $0 [-v] [-o output] file" >&2; exit 1; }
verbose=false
output="result.txt"
while getopts ":vo:" opt; do
case "$opt" in
v) verbose=true ;;
o) output="$OPTARG" ;;
:) echo "Option -$OPTARG requires an argument" >&2; usage ;;
?) echo "Unknown option: -$OPTARG" >&2; usage ;;
esac
done
shift $(( OPTIND - 1 ))
[[ $# -lt 1 ]] && usage
echo "Processing $1 → $output (verbose=$verbose)"
Output:
Processing input.txt → result.txt (verbose=false)
Retry with backoff
Run a command up to N times with exponential backoff between attempts.
retry() {
local attempts=$1 delay=1
shift
for ((i=1; i<=attempts; i++)); do
"$@" && return 0
echo "Attempt $i/$attempts failed. Retrying in ${delay}s…" >&2
sleep "$delay"
(( delay *= 2 ))
done
return 1
}
retry 4 curl -sf "https://example.com/api/health"
Output:
Attempt 1/4 failed. Retrying in 1s…
Attempt 2/4 failed. Retrying in 2s…
(succeeds on attempt 3 — returns 0)
Find and process files safely
Use find with -print0 and read -d '' to handle filenames with spaces or special characters.
while IFS= read -r -d '' file; do
echo "Processing: $file"
wc -l "$file"
done < <(find /var/log -name "*.log" -size +1M -print0)
Output:
Processing: /var/log/syslog
42387 /var/log/syslog
Processing: /var/log/kern.log
8904 /var/log/kern.log
Parallel jobs with wait
Run multiple background jobs and collect all their exit codes.
pids=()
for host in myhost web1 web2; do
ping -c1 -W1 "$host" &>/dev/null && echo "$host up" || echo "$host down" &
pids+=("$!")
done
failed=0
for pid in "${pids[@]}"; do
wait "$pid" || (( failed++ ))
done
echo "Done. Failures: $failed"
Output:
myhost up
web1 down
web2 up
Done. Failures: 1
Sources
- Bash 5.3 release announcement (bash-announce, July 2025)
- Bash 5.3 NEWS — terse summary of new features (Chet Ramey)
- Bash 5.3 release available (LWN.net)
- Bash 5.3 Released With Many Improvements (Phoronix)
- Bash Shell 5.3 Released with New Command Substitution (Linuxiac)
- Command Substitution — Bash Reference Manual (GNU)