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#cliupdated 05-25-2026

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.

FileRead by
/etc/profileLogin shells (system-wide)
/etc/profile.d/*.shSourced from /etc/profile
/etc/bash.bashrcInteractive shells (Debian/Ubuntu only; not POSIX-standard)
~/.bash_profileLogin shells — preferred over .profile if present
~/.bash_loginLogin shells — fallback if .bash_profile is missing
~/.profileLogin shells — fallback if neither of the above exists
~/.bashrcInteractive non-login shells
~/.bash_logoutRead when a login shell exits
~/.inputrcReadline key-bindings and completion options
~/.bash_historyCommand history (path controlled by $HISTFILE)
$BASH_ENVNon-interactive shells source the file this points to
$ENVPOSIX-mode non-interactive shells source this file

Output: (none — file reference table)

bash
# 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:

vbnet
autocd          off
cdable_vars     off
checkwinsize    on
bash
# 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)

bash
# 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.

bash
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:

vbnet
Alice Dev
42
Ready: true
bash
# 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:

code
abc ABC
bash
# Unset a variable
unset name

# Check if a variable is set
if [[ -v name ]]; then echo "set"; else echo "unset"; fi

Output:

bash
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.

bash
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:

csharp
/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
bash
# 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:

yaml
8080
8080
8080
override
bash
# 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:

code
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).

bash
$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)

bash
#!/usr/bin/env bash
# Example: show positional parameters
echo "Script: $0"
echo "Args  : $#"
echo "All   : $@"
echo "First : $1"

Output:

yaml
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.

bash
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:

css
B
bash
# 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:

csharp
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.

bash
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:

code
Weekday
bash
# 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:

code
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.

bash
# Word-list for loop
for color in red green blue; do
    echo "$color"
done

Output:

code
red
green
blue
bash
# Brace expansion sequence
for i in {1..5}; do
    echo "Step $i"
done

Output:

vbnet
Step 1
Step 2
Step 3
Step 4
Step 5
bash
# C-style for loop
for ((i=0; i<3; i++)); do
    echo "i=$i"
done

Output:

ini
i=0
i=1
i=2
bash
# Iterate over files
for f in /etc/*.conf; do
    echo "Config: $f"
done

Output:

bash
Config: /etc/hosts.conf
Config: /etc/nsswitch.conf
…
bash
# 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:

bash
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.

bash
count=0
while [[ $count -lt 3 ]]; do
    echo "count=$count"
    ((count++))
done

Output:

ini
count=0
count=1
count=2
bash
# Read lines from a file
while IFS= read -r line; do
    echo ">> $line"
done < /etc/hostname

Output:

ruby
>> myhost
bash
# Read lines from a command
while IFS= read -r line; do
    echo "user: $line"
done < <(getent passwd | cut -d: -f1 | head -3)

Output:

makefile
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.

bash
greet() {
    local name="$1"
    local greeting="${2:-Hello}"
    echo "$greeting, $name!"
}

greet "Alice Dev"
greet "Alice Dev" "Hi"

Output:

code
Hello, Alice Dev!
Hi, Alice Dev!
bash
# Return a value via stdout capture
add() {
    echo $(( $1 + $2 ))
}

result=$(add 3 7)
echo "Sum: $result"

Output:

makefile
Sum: 10
bash
# Return success/failure for use in if
file_exists() {
    [[ -f "$1" ]]
}

if file_exists "/etc/hosts"; then
    echo "found"
fi

Output:

code
found
bash
# 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:

code
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+).

bash
# 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:

code
apple
cherry
apple banana cherry
3
0 1 2
bash
# Append, modify, delete
fruits+=("date")
fruits[1]="blueberry"
unset fruits[2]

echo "${fruits[@]}"

Output:

bash
apple blueberry date
bash
# Array slice: ${array[@]:offset:length}
nums=(10 20 30 40 50)
echo "${nums[@]:1:3}"

Output:

code
20 30 40
bash
# 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:

yaml
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.

bash
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:

code
Hello, World!  
18
Hello
  Hello, Alice Dev!  
  HELLO, WORLD!  
  hello, world!  
bash
# 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:

yaml
Year : 2026
Month: 05
Day  : 04
bash
# Split a string into an array using IFS
IFS=',' read -ra parts <<< "one,two,three"
echo "${parts[1]}"
echo "${parts[@]}"

Output:

sql
two
one two three

Arithmetic

Bash integer arithmetic uses (( … )) for statements and $(( … )) for expressions. Floating-point requires an external tool like bc or awk.

bash
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:

yaml
13
7
30
3
1
1000
bash
# Increment / decrement
(( a++ ))
(( b-- ))
(( a += 5 ))

echo "$a $b"

Output:

code
16 2
bash
# Conditional arithmetic: (( expr )) returns 0 (true) if expr != 0
if (( a > 10 )); then echo "big"; fi

Output:

code
big
bash
# Floating-point with bc
result=$(echo "scale=4; 22/7" | bc)
echo "$result"

Output:

code
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.

bash
# diff two command outputs without temp files
diff <(sort /etc/passwd) <(sort /etc/group)

Output:

sql
(diff output between sorted passwd and group files)
bash
# 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:

ruby
>> /etc/hosts.conf
>> /etc/nsswitch.conf
bash
# Tee to multiple files and a command simultaneously
echo "log line" | tee >(gzip > log.gz) >(wc -c > size.txt)

Output:

arduino
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.

bash
# 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:

bash
users: 42
bash
# 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:

code
Hello, alice
bash
# 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:

bash
/home/alice
result: /tmp
/tmp
bash
# 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:

code
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.

bash
# Cleanup on exit
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT

echo "working with $tmpfile"
# tmpfile is always deleted when the script exits

Output:

bash
working with /tmp/tmp.XXXXXX
bash
# Catch Ctrl+C (SIGINT)
trap 'echo "Interrupted!"; exit 130' INT

for i in {1..10}; do
    echo "step $i"
    sleep 1
done

Output:

arduino
step 1
step 2
Interrupted!
bash
# ERR trap — run on any non-zero exit code
set -euo pipefail
trap 'echo "Error on line $LINENO (exit $?)"' ERR

false   # triggers ERR

Output:

vbnet
Error on line 4 (exit 1)
bash
# 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.

bash
#!/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:

sql
Running safely
bash
# Enable xtrace for debugging
set -x        # print each command before executing
ls /tmp
set +x        # turn off xtrace

Output:

bash
+ ls /tmp
<list of files>

Common pitfalls

  1. Unquoted variablesrm $file splits on spaces and expands globs; always write rm "$file".
  2. [ ] vs [[ ]][[ ]] is Bash-specific and safer (no word-splitting, supports &&/||/=~); use [ ] only when you need POSIX portability.
  3. == vs -eq== compares strings; -eq compares integers. [[ "10" == "10.0" ]] is false; (( 10 == 10 )) is true.
  4. Pipe subshells — variables set inside a pipe stage are lost after the pipe: echo "x" | read val; echo "$val" prints nothing. Use process substitution or lastpipe option instead.
  5. Integer division$(( 7 / 2 )) is 3, not 3.5; use bc or awk for floats.
  6. $* vs $@ — always use "$@" to forward arguments; $* joins all args into one string split by $IFS.
  7. set -e and functionsset -e does not trigger inside an if condition; a function called in an if can fail silently.
  8. 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+).
  9. #!/usr/bin/env bash vs #!/bin/bash/bin/bash is 3.2 on macOS and may not exist on Alpine/BusyBox systems. Use #!/usr/bin/env bash to 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

bash
#!/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

bash
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:

ini
Processing input.txt → result.txt (verbose=false)

Retry with backoff

Run a command up to N times with exponential backoff between attempts.

bash
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:

sql
Attempt 1/4 failed. Retrying in 1s…
Attempt 2/4 failed. Retrying in 2s…
(succeeds on attempt 3returns 0)

Find and process files safely

Use find with -print0 and read -d '' to handle filenames with spaces or special characters.

bash
while IFS= read -r -d '' file; do
    echo "Processing: $file"
    wc -l "$file"
done < <(find /var/log -name "*.log" -size +1M -print0)

Output:

c
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.

bash
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:

yaml
myhost up
web1 down
web2 up
Done. Failures: 1

Sources