← Back to all guides

Bash TLDR

A rapid reference guide to Bash shell scripting. Automate tasks, write scripts, and master the command line.

Bash 5.2 — December 2025

What is Bash?

Bash (Bourne Again SHell) is the default shell on most Linux distributions and macOS. It's a command processor that runs in a text window where users type commands, and it can also read and execute commands from scripts.

Scripting

Automate repetitive tasks, system administration, and build complex workflows with shell scripts.

Portable

Available on virtually every Unix-like system. Write once, run anywhere (with minor adjustments).

Powerful Pipes

Chain commands together with pipes and redirections to build complex data processing pipelines.

Glue Language

Perfect for orchestrating other programs and tools. Let each tool do what it does best.

Basics

Hello World

#!/bin/bash
# This is a comment
echo "Hello, World!"

Running Scripts

# Make script executable
chmod +x script.sh

# Run the script
./script.sh

# Or run with bash explicitly
bash script.sh

# Source (run in current shell, keeps variables)
source script.sh
. script.sh           # same thing

Command Substitution

# Modern syntax (preferred)
today=$(date +%Y-%m-%d)
files=$(ls *.txt)

# Can nest cleanly
result=$(echo $(cat file.txt) | wc -w)

# Legacy syntax (avoid)
today=`date +%Y-%m-%d`

Arithmetic

# Arithmetic expansion
result=$(( 5 + 3 ))
result=$(( a + b ))        # variables don't need $
result=$(( 10 / 3 ))      # integer division = 3
result=$(( 2 ** 8 ))      # exponentiation = 256

# Compound arithmetic (no output, use in conditions)
(( counter++ ))
(( total += 10 ))

# Arithmetic condition
if (( a > b )); then
    echo "a is greater"
fi

# Number bases
echo $(( 0xFF ))         # hex: 255
echo $(( 2#1010 ))       # binary: 10
echo $(( 8#77 ))         # octal: 63
Bash only supports integer arithmetic. For floating point, use bc or awk.

Variables

Declaration & Assignment

# No spaces around =
name="Alice"
count=42

# Use the variable
echo "Hello, $name"
echo "Hello, ${name}"    # braces for clarity

# Readonly (constant)
readonly PI=3.14159

# Export to child processes
export PATH="$PATH:/usr/local/bin"

# Unset a variable
unset name

Special Variables

Variable Description
$0 Script name
$1, $2 ... $9 Positional arguments (use ${10} for 10+)
$# Number of arguments
$@ All arguments as separate words (use "$@")
$* All arguments as single string
$? Exit status of last command (0 = success)
$$ PID of current shell
$! PID of last background process
$_ Last argument of previous command

Parameter Expansion (Defaults)

# Use default if unset or empty
echo "${name:-Guest}"

# Assign default if unset or empty
echo "${name:=Guest}"

# Error if unset or empty
echo "${name:?'name is required'}"

# Use alternate value if set and non-empty
echo "${name:+Hello $name}"

Declare with Types

# Integer
declare -i num=42
num=num+1                # arithmetic automatic

# Lowercase
declare -l lower="HELLO"   # stored as "hello"

# Uppercase
declare -u upper="hello"   # stored as "HELLO"

# Readonly
declare -r constant="value"

# Export
declare -x exported="value"

Quoting

Single vs Double Quotes

name="Alice"

# Double quotes: variables expand
echo "Hello, $name"       # Hello, Alice
echo "Today is $(date)"   # command substitution works

# Single quotes: everything literal
echo 'Hello, $name'       # Hello, $name
echo 'No $(expansion)'    # No $(expansion)

# Escaping in double quotes
echo "The price is \$100"
echo "She said \"Hello\""

# Single quote in single quotes (workaround)
echo 'It'\''s working'     # It's working

ANSI-C Quoting

# $'...' enables escape sequences
echo $'Line1\nLine2'      # actual newline
echo $'Tab\there'         # actual tab
echo $'Ring the \a bell'  # bell character
Always quote your variables: "$var". Unquoted variables undergo word splitting and glob expansion, leading to subtle bugs.

Conditionals

if/elif/else

if [[ "$name" == "Alice" ]]; then
    echo "Hello, Alice!"
elif [[ "$name" == "Bob" ]]; then
    echo "Hello, Bob!"
else
    echo "Hello, stranger!"
fi

Test Commands: [ ] vs [[ ]]

# [ ] - POSIX compatible, more portable
if [ "$a" = "$b" ]; then ...; fi

# [[ ]] - Bash extended test (preferred)
# - No word splitting, safer without quotes
# - Supports && || directly
# - Pattern matching with ==
# - Regex matching with =~
if [[ $a == $b ]]; then ...; fi
if [[ $file == *.txt ]]; then ...; fi
if [[ $str =~ ^[0-9]+$ ]]; then ...; fi

String Comparisons

Operator Description
== or = Equal
!= Not equal
-z "$str" String is empty
-n "$str" String is not empty
< Less than (alphabetically)
> Greater than (alphabetically)

Integer Comparisons

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

File Tests

if [[ -f "$file" ]]; then ...; fi  # regular file exists
if [[ -d "$dir" ]]; then ...; fi   # directory exists
if [[ -e "$path" ]]; then ...; fi  # exists (any type)
if [[ -r "$file" ]]; then ...; fi  # readable
if [[ -w "$file" ]]; then ...; fi  # writable
if [[ -x "$file" ]]; then ...; fi  # executable
if [[ -s "$file" ]]; then ...; fi  # size > 0
if [[ -L "$file" ]]; then ...; fi  # symbolic link

Logical Operators

# In [[ ]]
if [[ $a -gt 0 && $a -lt 100 ]]; then ...; fi
if [[ $a == "yes" || $a == "y" ]]; then ...; fi
if [[ ! -f "$file" ]]; then ...; fi

# Command chaining
command1 && command2   # run command2 only if command1 succeeds
command1 || command2   # run command2 only if command1 fails

case Statement

case "$answer" in
    yes|y|Y)
        echo "Confirmed"
        ;;
    no|n|N)
        echo "Declined"
        ;;
    *)
        echo "Unknown response"
        ;;
esac

# Pattern matching
case "$filename" in
    *.txt)   echo "Text file" ;;
    *.jpg|*.png) echo "Image" ;;
    *)       echo "Unknown" ;;
esac

Loops

for Loop

# Iterate over list
for name in Alice Bob Charlie; do
    echo "Hello, $name"
done

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

# Iterate over command output
for user in $(cat users.txt); do
    echo "User: $user"
done

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

# Range with brace expansion
for i in {1..5}; do
    echo "$i"
done

# Range with step
for i in {0..10..2}; do
    echo "$i"   # 0, 2, 4, 6, 8, 10
done

while Loop

counter=0
while [[ $counter -lt 5 ]]; do
    echo "Count: $counter"
    (( counter++ ))
done

# Read file line by line
while read -r line; do
    echo "Line: $line"
done < file.txt

# Process command output line by line
ls -la | while read -r line; do
    echo "$line"
done

# Infinite loop
while true; do
    # ...
    sleep 1
done

until Loop

# Run until condition is true (opposite of while)
counter=0
until [[ $counter -ge 5 ]]; do
    echo "Count: $counter"
    (( counter++ ))
done

Loop Control

# break - exit loop
for i in {1..10}; do
    if [[ $i -eq 5 ]]; then
        break
    fi
    echo "$i"
done

# continue - skip to next iteration
for i in {1..10}; do
    if [[ $((i % 2)) -eq 0 ]]; then
        continue   # skip even numbers
    fi
    echo "$i"
done

# break/continue with nesting
break 2     # break out of 2 nested loops
continue 2  # continue in outer loop

Functions

Defining Functions

# Standard syntax
greet() {
    echo "Hello, $1!"
}

# Alternative syntax
function greet {
    echo "Hello, $1!"
}

# Call the function
greet "Alice"

Arguments & Return Values

add() {
    local a=$1
    local b=$2
    local sum=$(( a + b ))
    echo "$sum"   # output is the "return value"
}

# Capture output
result=$(add 5 3)
echo "Sum: $result"

# Return status (0-255)
is_even() {
    if [[ $(($1 % 2)) -eq 0 ]]; then
        return 0   # success/true
    else
        return 1   # failure/false
    fi
}

if is_even 42; then
    echo "It's even"
fi

Local Variables

my_function() {
    local local_var="only here"    # scoped to function
    global_var="everywhere"         # global (avoid this)
}

my_function
echo "$local_var"    # empty - not accessible
echo "$global_var"   # "everywhere"

Function with All Args

print_all() {
    echo "Number of args: $#"
    for arg in "$@"; do
        echo "  Arg: $arg"
    done
}

print_all "one" "two" "three"
Always use local for variables inside functions to avoid polluting the global scope.

Arrays

Indexed Arrays

# Create array
fruits=("apple" "banana" "cherry")
numbers=(1 2 3 4 5)

# Access elements
echo "${fruits[0]}"     # apple
echo "${fruits[1]}"     # banana
echo "${fruits[-1]}"    # cherry (last element)

# All elements
echo "${fruits[@]}"     # apple banana cherry

# Array length
echo "${#fruits[@]}"    # 3

# All indices
echo "${!fruits[@]}"    # 0 1 2

# Modify
fruits[1]="blueberry"

# Append
fruits+=("date")

# Slice
echo "${fruits[@]:1:2}"  # 2 elements starting at index 1

Iterating Arrays

# Iterate over values
for fruit in "${fruits[@]}"; do
    echo "$fruit"
done

# Iterate with index
for i in "${!fruits[@]}"; do
    echo "$i: ${fruits[$i]}"
done

Associative Arrays (Bash 4+)

# Must declare first
declare -A colors

# Assign
colors["apple"]="red"
colors["banana"]="yellow"
colors["grape"]="purple"

# Or inline
declare -A sizes=(["small"]=1 ["medium"]=2 ["large"]=3)

# Access
echo "${colors[apple]}"    # red

# All keys
echo "${!colors[@]}"       # apple banana grape

# All values
echo "${colors[@]}"        # red yellow purple

# Iterate
for key in "${!colors[@]}"; do
    echo "$key: ${colors[$key]}"
done

# Check if key exists
if [[ -v colors["apple"] ]]; then
    echo "Key exists"
fi
Always quote array expansions: "${array[@]}". Without quotes, elements with spaces will be split.

String Manipulation

String Length

str="Hello, World!"
echo "${#str}"           # 13

Substring Extraction

str="Hello, World!"

# ${string:offset:length}
echo "${str:0:5}"         # Hello
echo "${str:7}"           # World!
echo "${str: -6}"         # World! (space before -)
echo "${str: -6:5}"       # World

Search & Replace

str="hello world world"

# Replace first match
echo "${str/world/there}"   # hello there world

# Replace all matches
echo "${str//world/there}"  # hello there there

# Replace at start
echo "${str/#hello/hi}"     # hi world world

# Replace at end
echo "${str/%world/earth}"  # hello world earth

# Delete pattern (replace with nothing)
echo "${str// world/}"      # hello

Pattern Deletion

path="/home/user/docs/file.txt"

# Remove shortest match from start
echo "${path#*/}"         # home/user/docs/file.txt

# Remove longest match from start
echo "${path##*/}"        # file.txt (basename)

# Remove shortest match from end
echo "${path%/*}"         # /home/user/docs (dirname)

# Remove longest match from end
echo "${path%%/*}"        # (empty - removes everything)

# Get extension
file="archive.tar.gz"
echo "${file#*.}"         # tar.gz (first .)
echo "${file##*.}"        # gz (last .)

# Remove extension
echo "${file%.*}"         # archive.tar
echo "${file%%.*}"        # archive

Case Conversion (Bash 4+)

str="Hello World"

# Lowercase
echo "${str,}"    # hello World (first char)
echo "${str,,}"   # hello world (all)

# Uppercase
echo "${str^}"    # Hello World (first char)
echo "${str^^}"   # HELLO WORLD (all)

# Toggle case
echo "${str~}"    # hELLO World (first char)
echo "${str~~}"   # hELLO wORLD (all)

I/O & Redirection

Output: echo & printf

# echo
echo "Hello"              # with newline
echo -n "Hello"           # without newline
echo -e "Line1\nLine2"   # interpret escapes

# printf (more control)
printf "%s is %d years old\n" "Alice" 30
printf "%-10s %5d\n" "Item" 42   # padded
printf "%05d\n" 42         # zero-padded: 00042
printf "%.2f\n" 3.14159   # 3.14

Input: read

# Basic read
read name
echo "Hello, $name"

# With prompt
read -p "Enter your name: " name

# Silent (for passwords)
read -sp "Password: " password
echo   # newline after hidden input

# Read into array
read -a words <<< "one two three"

# Read with timeout
read -t 5 -p "Quick! (5s): " answer

# Read n characters
read -n 1 -p "Press any key: " key

# Raw input (no backslash interpretation)
read -r line   # always use -r!

Redirection

# Output redirection
command > file.txt      # overwrite
command >> file.txt     # append

# Input redirection
command < file.txt

# Stderr redirection
command 2> errors.txt        # stderr to file
command 2>> errors.txt       # append stderr
command 2>/dev/null         # discard stderr

# Combine stdout and stderr
command &> output.txt        # both to file
command > file.txt 2>&1     # stderr to stdout
command 2>&1 | less          # both through pipe

# Discard all output
command >/dev/null 2>&1

Here Documents & Here Strings

# Here document (multi-line input)
cat <<EOF
This is line 1
Variable: $name
This is line 3
EOF

# Literal (no expansion)
cat <<'EOF'
$variables are literal
$(commands) too
EOF

# Strip leading tabs
cat <<-EOF
	Indented with tabs
	They will be stripped
EOF

# Here string (single-line input)
grep "pattern" <<< "$variable"
bc <<< "5 + 3"   # 8

Pipes

# Chain commands
cat file.txt | grep "pattern" | sort | uniq

# Process substitution
diff <(sort file1) <(sort file2)

# Tee - write to file AND stdout
command | tee output.txt | less
command | tee -a output.txt   # append

Error Handling

Exit Codes

# 0 = success, 1-255 = failure
command
echo "Exit code: $?"

# Exit with specific code
exit 0    # success
exit 1    # generic failure

# Check command success
if command; then
    echo "Success"
else
    echo "Failed"
fi

Strict Mode

#!/bin/bash
set -euo pipefail

# -e: exit on any command failure
# -u: error on undefined variables
# -o pipefail: pipe fails if any command fails

# Optional: safer word splitting
IFS=$'\n\t'

Error Handling Patterns

# Handle individual command failure
command || { echo "Failed" >&2; exit 1; }

# Command chain with fallback
command1 && command2 || echo "Something failed"

# Explicit check
if ! command; then
    echo "Command failed" >&2
    exit 1
fi

# Check command exists
if ! command -v jq >/dev/null 2>&1; then
    echo "jq is required but not installed" >&2
    exit 1
fi

trap - Signal Handling & Cleanup

# Cleanup on exit
cleanup() {
    echo "Cleaning up..."
    rm -f "$tmpfile"
}
trap cleanup EXIT

tmpfile=$(mktemp)
# ... script continues, cleanup runs on exit

# Handle Ctrl+C (SIGINT)
trap 'echo "Interrupted"; exit 1' INT

# Error with line number
trap 'echo "Error on line $LINENO"' ERR

# Ignore signal
trap '' SIGINT   # ignore Ctrl+C

# Reset to default
trap - SIGINT

Input Validation

# Check argument count
if [[ $# -lt 1 ]]; then
    echo "Usage: $0 <filename>" >&2
    exit 1
fi

# Check file exists
if [[ ! -f "$1" ]]; then
    echo "Error: File '$1' not found" >&2
    exit 1
fi

# Validate numeric input
if ! [[ "$1" =~ ^[0-9]+$ ]]; then
    echo "Error: '$1' is not a number" >&2
    exit 1
fi

getopts - Parse Options

usage() {
    echo "Usage: $0 [-v] [-f file] [-n count] arg"
}

verbose=0
file=""
count=1

while getopts "hvf:n:" opt; do
    case "$opt" in
        h) usage; exit 0 ;;
        v) verbose=1 ;;
        f) file="$OPTARG" ;;
        n) count="$OPTARG" ;;
        ?) usage; exit 1 ;;
    esac
done

# Shift past options
shift $(( OPTIND - 1 ))

# Remaining args in $@
echo "Positional args: $@"
Write to stderr for errors: echo "Error" >&2. This keeps error messages separate from normal output.