Shebang & Basics

#!/bin/bash              # Standard shebang
#!/usr/bin/env bash      # Portable shebang (uses PATH lookup)

# This is a comment

set -e                   # Exit on error
set -u                   # Exit on undefined variable
set -o pipefail          # Exit on pipe failure
set -euo pipefail        # All three combined (recommended)

chmod +x script.sh       # Make script executable
./script.sh              # Run script
bash script.sh           # Run without execute permission
source script.sh         # Run in current shell (imports vars/functions)

Run ShellCheck to lint your scripts:

shellcheck script.sh     # Static analysis for common mistakes

Variables

name="world"             # No spaces around =
readonly PI=3.14         # Constant, cannot reassign
local count=0            # Local to function scope
unset name               # Remove variable

Quoting Rules

Syntax Behavior
"double" Expands variables and command substitutions
'single' Literal string, no expansion
`cmd` Command substitution (legacy; prefer $(cmd))

Variable Expansion & Defaults

echo "${var}"            # Explicit expansion (preferred)
echo "${var:-default}"   # Use default if var is unset/empty
echo "${var:=default}"   # Assign default if var is unset/empty
echo "${var:+alt}"       # Use alt if var IS set and non-empty
echo "${var:?error msg}" # Exit with error if var is unset/empty

String Operations

str="Hello, World!"

echo "${#str}"               # Length: 13
echo "${str:7}"              # Substring from offset: World!
echo "${str:7:5}"            # Substring offset+length: World
echo "${str/World/Bash}"     # Replace first: Hello, Bash!
echo "${str//l/L}"           # Replace all: HeLLo, WorLd!
echo "${str#Hello, }"        # Remove shortest prefix: World!
echo "${str##*, }"           # Remove longest prefix: World!
echo "${str%!}"              # Remove shortest suffix: Hello, World
echo "${str%%o*}"            # Remove longest suffix: Hell
echo "${str^^}"              # Uppercase: HELLO, WORLD!
echo "${str,,}"              # Lowercase: hello, world!
echo "${str^}"               # Capitalize first: Hello, World!

Arrays

Indexed Arrays

arr=(one two three)          # Declare
arr[3]="four"                # Append/set by index
arr+=("five")                # Append

echo "${arr[0]}"             # Access element: one
echo "${arr[@]}"             # All elements
echo "${!arr[@]}"            # All indices
echo "${#arr[@]}"            # Length: 5
echo "${arr[@]:1:2}"         # Slice (offset:length): two three

unset 'arr[2]'               # Delete element

Associative Arrays

declare -A map
map[name]="Alice"
map[age]=30

echo "${map[name]}"          # Access: Alice
echo "${!map[@]}"            # All keys
echo "${map[@]}"             # All values

Iteration

for val in "${arr[@]}"; do echo "$val"; done
for key in "${!map[@]}"; do echo "$key=${map[$key]}"; done

Conditionals

if / elif / else

if [[ "$a" == "yes" ]]; then
    echo "yes"
elif [[ "$a" == "no" ]]; then
    echo "no"
else
    echo "unknown"
fi

String Comparisons

Operator Meaning
= or == Equal
!= Not equal
-z "$str" String is empty
-n "$str" String is non-empty
< Less than (lexicographic, use inside [[ ]])
> Greater than (lexicographic, use inside [[ ]])
=~ Regex match (inside [[ ]] only)

Integer Comparisons

Operator Meaning
-eq Equal
-ne Not equal
-lt Less than
-le Less than or equal
-gt Greater than
-ge Greater than or equal

File Tests

Operator Meaning
-f file Is regular file
-d file Is directory
-e file Exists
-r file Is readable
-w file Is writable
-x file Is executable
-s file Exists and is non-empty
-L file Is symbolic link

Logical Operators

[[ "$a" && "$b" ]]          # AND
[[ "$a" || "$b" ]]          # OR
[[ ! "$a" ]]                # NOT

case / esac

case "$input" in
    start)   echo "Starting";;
    stop)    echo "Stopping";;
    restart) echo "Restarting";;
    *)       echo "Unknown: $input";;
esac

Loops

# For-in
for item in a b c; do echo "$item"; done

# C-style for
for ((i = 0; i < 5; i++)); do echo "$i"; done

# While
while [[ "$count" -lt 10 ]]; do
    ((count++))
done

# Until (runs while condition is false)
until [[ "$count" -ge 10 ]]; do
    ((count++))
done

# Infinite loop
while true; do echo "forever"; sleep 1; done

# Iterate over files
for f in *.txt; do echo "$f"; done

# Iterate over lines of a file
while IFS= read -r line; do
    echo "$line"
done < file.txt

# break / continue
for i in {1..10}; do
    [[ "$i" -eq 5 ]] && continue   # Skip 5
    [[ "$i" -eq 8 ]] && break      # Stop at 8
    echo "$i"
done

Functions

greet() {
    local name="$1"            # Local variable
    echo "Hello, $name"
}
greet "Alice"                  # Call: Hello, Alice

# Arguments
demo() {
    echo "Func name: $0"       # Script name (not function name)
    echo "First arg: $1"
    echo "All args: $@"
    echo "Arg count: $#"
}

# Return values (0-255 exit code; use stdout for data)
add() {
    echo $(( $1 + $2 ))
}
result=$(add 3 4)              # Capture: 7

# Passing arrays
process_arr() {
    local -a items=("$@")
    for item in "${items[@]}"; do echo "$item"; done
}
my_arr=(x y z)
process_arr "${my_arr[@]}"

Input / Output

echo "Hello"                   # Print with newline
echo -n "No newline"           # Suppress newline
echo -e "Tab:\tNew line:\n"    # Interpret escapes

# printf (formatted output)
printf "Name: %s, Age: %d\n" "Alice" 30
printf "%05d\n" 42             # Zero-padded: 00042
printf "%.2f\n" 3.14159        # Two decimals: 3.14

# Read input
read -p "Enter name: " name
read -s -p "Password: " pass   # Silent input
read -t 5 -p "Quick: " ans     # 5-second timeout
read -a arr <<< "a b c"        # Read into array

# Here document
cat <<EOF
Hello, $name
Today is $(date)
EOF

# Here document (no expansion)
cat <<'EOF'
Literal $name and $(date)
EOF

# Here string
grep "pattern" <<< "$var"

Arithmetic

echo $(( 5 + 3 ))             # 8
echo $(( 10 % 3 ))            # 1 (modulo)
echo $(( 2 ** 10 ))           # 1024 (exponent)

x=5
(( x += 3 ))                  # x is now 8
(( x++ ))                     # x is now 9

let "y = 10 / 3"              # Integer division: 3

# Floating point with bc
echo "scale=2; 10 / 3" | bc   # 3.33

Special Variables

Variable Description
$0 Script name / path
$1 - $9 Positional arguments
${10} 10th+ argument (braces required)
$# Number of arguments
$@ All arguments (individually quoted)
$* All arguments (as single string)
$? Exit status of last command
$$ PID of current script
$! PID of last background command
$_ Last argument of previous command
$RANDOM Random integer 0-32767
$LINENO Current line number
$SECONDS Seconds since script started

Process & Job Control

# Background execution
long_task &                    # Run in background
pid=$!                         # Capture its PID
wait "$pid"                    # Wait for it to finish
wait                           # Wait for all background jobs

# Subshells
(cd /tmp && ls)                # Runs in subshell; cwd unchanged after

# Command substitution
now=$(date +%Y-%m-%d)

# Trap & signal handling
cleanup() { rm -f "$tmpfile"; }
trap cleanup EXIT              # Run on script exit
trap cleanup SIGINT SIGTERM    # Run on Ctrl-C or kill

# Common signals: EXIT, ERR, SIGINT, SIGTERM, SIGHUP

Useful Patterns

Check If Command Exists

command_exists() { command -v "$1" &>/dev/null; }
if command_exists git; then echo "git is installed"; fi

Parse CLI Arguments with getopts

while getopts ":v:o:h" opt; do
    case "$opt" in
        v) verbose="$OPTARG";;
        o) output="$OPTARG";;
        h) echo "Usage: $0 [-v level] [-o file] [-h]"; exit 0;;
        :) echo "Option -$OPTARG requires an argument" >&2; exit 1;;
        \?) echo "Invalid option: -$OPTARG" >&2; exit 1;;
    esac
done
shift $((OPTIND - 1))         # Remaining args in $@

Logging Function

log() {
    local level="$1"; shift
    printf "[%s] [%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$*" >&2
}
log INFO "Script started"
log ERROR "Something failed"

Error Handling with trap

set -euo pipefail
on_error() {
    echo "Error on line $1, exit code $2" >&2
}
trap 'on_error $LINENO $?' ERR

Temp Files with mktemp

tmpfile=$(mktemp)              # Create temp file
tmpdir=$(mktemp -d)            # Create temp directory
trap 'rm -rf "$tmpfile" "$tmpdir"' EXIT

Progress Indicator

spinner() {
    local chars='|/-\'
    while :; do
        for (( i=0; i<${#chars}; i++ )); do
            printf "\r%s" "${chars:$i:1}"
            sleep 0.1
        done
    done
}
spinner &
SPIN_PID=$!
# ... do work ...
kill "$SPIN_PID" 2>/dev/null
printf "\rDone.\n"

Parallel Execution

pids=()
for url in "${urls[@]}"; do
    curl -sO "$url" &
    pids+=($!)
done
for pid in "${pids[@]}"; do wait "$pid"; done
echo "All downloads complete"