Complete reference guide for Bash shell scripting. Master variables, conditionals, loops, functions, I/O redirection, and advanced scripting patterns.
Essential constructs at a glance for quick lookup and daily scripting.
# Variables
var="value" # Assignment (no spaces around =)
echo "$var" # Use with $
readonly CONST="immutable" # Read-only variable
export PATH="/usr/bin:$PATH" # Export to environment
# Conditionals
if [[ condition ]]; then
commands
elif [[ condition ]]; then
commands
else
commands
fi
# Loops
for item in list; do commands; done
while [[ condition ]]; do commands; done
until [[ condition ]]; do commands; done
# Functions
function_name() {
local var="$1"
echo "$var"
}
# Common Tests
[[ -f file ]] # File exists
[[ -d dir ]] # Directory exists
[[ -z "$var" ]] # String is empty
[[ "$a" == "$b" ]] # String equality
[[ $num -eq 5 ]] # Numeric equality
# I/O Redirection
cmd > file # Redirect stdout to file
cmd >> file # Append stdout to file
cmd 2> file # Redirect stderr to file
cmd &> file # Redirect both stdout and stderr
cmd < file # Redirect file to stdin
cmd1 | cmd2 # Pipe stdout of cmd1 to stdin of cmd2
# Basic assignment (no spaces around =)
name="John Doe"
count=42
empty=""
# Command substitution
current_dir=$(pwd)
files=$(ls -1)
date_old=`date` # Deprecated style, use $() instead
# Read-only variables
readonly PI=3.14159
declare -r MAX_SIZE=1000
# Unsetting variables
unset var_name
# Export to child processes
export PATH="/usr/local/bin:$PATH"
export DB_HOST="localhost"
# Export with declaration
export VAR="value"
# List all exported variables
export -p
# Temporary environment variable (one command only)
FOO=bar command args
var="hello"
# Basic expansion
echo "$var" # hello
echo "${var}" # hello (preferred in complex expressions)
# Default values
echo "${undefined:-default}" # Use default if undefined or empty
echo "${undefined-default}" # Use default only if undefined
echo "${var:-fallback}" # hello (var is set)
# Assign default
echo "${new_var:=assigned}" # Assigns and returns "assigned"
# Alternative value (use if set)
echo "${var:+alternative}" # alternative (var is set)
# Error if undefined
echo "${must_exist:?Error: variable not set}"
# String length
echo "${#var}" # 5
# Substring extraction
text="Hello World"
echo "${text:0:5}" # Hello (offset 0, length 5)
echo "${text:6}" # World (offset 6 to end)
echo "${text: -5}" # World (last 5 chars, note space)
# Pattern removal (shortest match)
path="/home/user/file.tar.gz"
echo "${path#*/}" # home/user/file.tar.gz
echo "${path%/*}" # /home/user
# Pattern removal (longest match)
echo "${path##*/}" # file.tar.gz (basename)
echo "${path%%/*}" # (empty)
# Pattern replacement
text="Hello World"
echo "${text/World/Universe}" # Replace first
echo "${text//o/0}" # Replace all
echo "${text/#Hello/Hi}" # Replace at start
echo "${text/%World/Earth}" # Replace at end
# Case conversion (Bash 4+)
echo "${text,,}" # hello world (lowercase all)
echo "${text^^}" # HELLO WORLD (uppercase all)
# Declaration
fruits=("apple" "banana" "cherry")
empty_array=()
# Alternative declaration
declare -a array
array[0]="first"
array[1]="second"
# Access elements
echo "${fruits[0]}" # apple
echo "${fruits[@]}" # All elements
# Array length
echo "${#fruits[@]}" # 3 (number of elements)
# Array slicing
echo "${fruits[@]:1}" # banana cherry (from index 1)
echo "${fruits[@]:1:1}" # banana (from index 1, length 1)
# Append to array
fruits+=("date")
# Iterate over array
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# Iterate with indices
for i in "${!fruits[@]}"; do
echo "Index $i: ${fruits[$i]}"
done
# Declaration (must declare explicitly)
declare -A colors
colors[red]="#FF0000"
colors[green]="#00FF00"
colors[blue]="#0000FF"
# Alternative declaration
declare -A person=(
[name]="John"
[age]="30"
[city]="NYC"
)
# Access elements
echo "${colors[red]}" # #FF0000
# Check if key exists
if [[ -v colors[red] ]]; then
echo "Key exists"
fi
# All keys
echo "${!colors[@]}" # red green blue
# Iterate over keys and values
for key in "${!colors[@]}"; do
echo "$key: ${colors[$key]}"
done
# Basic if
if [[ condition ]]; then
commands
fi
# If-else
if [[ condition ]]; then
commands
else
commands
fi
# If-elif-else
if [[ condition1 ]]; then
commands
elif [[ condition2 ]]; then
commands
else
commands
fi
str1="hello"
str2="world"
# Equality
[[ "$str1" == "hello" ]] # True
[[ "$str1" != "world" ]] # True
# Pattern matching (only with [[)
[[ "$str1" == h* ]] # True (glob pattern)
# Regex matching (only with [[)
[[ "$str1" =~ ^he.* ]] # True (regex)
# Lexicographic comparison
[[ "$str1" < "$str2" ]] # True (hello < world)
# Empty/non-empty
[[ -z "$empty" ]] # True (string is empty)
[[ -n "$str1" ]] # True (string is not empty)
a=10
b=20
# Arithmetic comparisons
[[ $a -eq 10 ]] # Equal
[[ $a -ne 20 ]] # Not equal
[[ $a -lt 20 ]] # Less than
[[ $a -le 10 ]] # Less than or equal
[[ $a -gt 5 ]] # Greater than
[[ $a -ge 10 ]] # Greater than or equal
# Arithmetic evaluation (( ))
if (( a < b )); then
echo "a is less than b"
fi
# Arithmetic with assignment
(( a++ )) # Increment
(( a += 5 )) # Add 5
(( result = a * b )) # Multiply
| Test | Description | Example |
|---|---|---|
-e |
Exists (file or directory) | [[ -e "$file" ]] |
-f |
Regular file exists | [[ -f "$file" ]] |
-d |
Directory exists | [[ -d "$dir" ]] |
-L |
Symbolic link exists | [[ -L "$link" ]] |
-r |
Readable | [[ -r "$file" ]] |
-w |
Writable | [[ -w "$file" ]] |
-x |
Executable | [[ -x "$file" ]] |
-s |
File exists and is not empty | [[ -s "$file" ]] |
-nt |
Newer than | [[ file1 -nt file2 ]] |
-ot |
Older than | [[ file1 -ot file2 ]] |
# AND
[[ condition1 && condition2 ]]
[[ -f file.txt && -r file.txt ]]
# OR
[[ condition1 || condition2 ]]
[[ -f file1 || -f file2 ]]
# NOT
[[ ! condition ]]
[[ ! -f file.txt ]]
# Combining
[[ ( condition1 || condition2 ) && condition3 ]]
case "$var" in
pattern1)
commands
;;
pattern2)
commands
;;
*)
default commands
;;
esac
# Example with patterns
case "$1" in
start)
echo "Starting..."
;;
stop)
echo "Stopping..."
;;
restart)
echo "Restarting..."
;;
*)
echo "Usage: $0 {start|stop|restart}"
exit 1
;;
esac
# Pattern matching
case "$filename" in
*.txt)
echo "Text file"
;;
*.jpg|*.png|*.gif)
echo "Image file"
;;
[Mm]akefile)
echo "Makefile"
;;
*)
echo "Unknown file type"
;;
esac
# List iteration
for item in one two three; do
echo "$item"
done
# Array iteration
fruits=("apple" "banana" "cherry")
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# Glob expansion (files)
for file in *.txt; do
echo "Processing $file"
done
# Brace expansion
for i in {1..10}; do
echo "Number $i"
done
for letter in {a..z}; do
echo "$letter"
done
# C-style syntax
for (( i=0; i<10; i++ )); do
echo "$i"
done
# Multiple variables
for (( i=0, j=10; ido
echo "i=$i, j=$j"
done
# Basic while
count=0
while [[ $count -lt 5 ]]; do
echo "$count"
(( count++ ))
done
# Read from file line by line
while IFS= read -r line; do
echo "Line: $line"
done < file.txt
# Infinite loop
while true; do
echo "Running..."
sleep 1
done
# Runs until condition becomes true
count=0
until [[ $count -ge 5 ]]; do
echo "$count"
(( count++ ))
done
# Wait for file to appear
until [[ -f /tmp/ready ]]; do
echo "Waiting..."
sleep 1
done
# 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 -eq 5 ]]; then
continue
fi
echo "$i" # Skips 5
done
# Method 1: function keyword
function my_function {
echo "Hello from function"
}
# Method 2: POSIX style (preferred)
my_function() {
echo "Hello from function"
}
# Call function
my_function
# Access arguments
greet() {
echo "Hello, $1!" # First argument
echo "Second arg: $2" # Second argument
}
greet "Alice" "Bob"
# Special variables
show_args() {
echo "Script name: $0"
echo "First arg: $1"
echo "All args: $@"
echo "Arg count: $#"
echo "Last exit code: $?"
echo "PID: $$"
}
# Return exit status (0-255)
is_even() {
local num=$1
if (( num % 2 == 0 )); then
return 0 # Success (true)
else
return 1 # Failure (false)
fi
}
if is_even 4; then
echo "Even"
fi
# Return string via stdout
get_user() {
echo "john_doe"
}
user=$(get_user)
echo "User: $user"
# With local (function scope)
good_function() {
local var="I'm local"
echo "$var"
}
good_function # I'm local
echo "$var" # (empty, var is local to function)
# Multiple local declarations
calculate() {
local a=$1
local b=$2
local result=$(( a + b ))
echo "$result"
}
# Factorial
factorial() {
local n=$1
if (( n <= 1 )); then
echo 1
else
local prev=$(factorial $(( n - 1 )))
echo $(( n * prev ))
fi
}
result=$(factorial 5)
echo "5! = $result" # 120
path="/home/user/documents/file.tar.gz"
# Remove shortest match from beginning (#)
echo "${path#*/}" # home/user/documents/file.tar.gz
# Remove longest match from beginning (##)
echo "${path##*/}" # file.tar.gz (basename)
# Remove shortest match from end (%)
echo "${path%/*}" # /home/user/documents (dirname)
# Remove longest match from end (%%)
echo "${path%%.*}" # /home/user/documents/file
# Extension manipulation
filename="document.txt"
echo "${filename%.*}" # document (remove extension)
echo "${filename##*.}" # txt (get extension)
text="Hello World, Hello Universe"
# Replace first occurrence (/)
echo "${text/Hello/Hi}" # Hi World, Hello Universe
# Replace all occurrences (//)
echo "${text//Hello/Hi}" # Hi World, Hi Universe
# Replace at beginning (/#)
echo "${text/#Hello/Hi}" # Hi World, Hello Universe
# Replace at end (/%)
echo "${text/%Universe/Galaxy}" # Hello World, Hello Galaxy
# Delete pattern (replace with empty)
echo "${text//Hello/}" # World, Universe
text="Hello World"
# Syntax: ${var:offset:length}
echo "${text:0:5}" # Hello (start at 0, length 5)
echo "${text:6:5}" # World (start at 6, length 5)
echo "${text:6}" # World (start at 6, to end)
# Negative offset (from end)
echo "${text: -5}" # World (last 5 chars, note space)
# Get first/last characters
echo "${text:0:1}" # H (first char)
echo "${text: -1}" # d (last char)
text="Hello World"
# Lowercase
echo "${text,,}" # hello world (all lowercase)
echo "${text,}" # hello World (first char lowercase)
# Uppercase
echo "${text^^}" # HELLO WORLD (all uppercase)
echo "${text^}" # Hello World (first char uppercase)
# Using IFS (Internal Field Separator)
text="one,two,three"
IFS=',' read -ra parts <<< "$text"
echo "${parts[0]}" # one
echo "${parts[1]}" # two
# Split into variables
IFS=':' read -r user pass <<< "john:secret"
echo "User: $user" # john
echo "Pass: $pass" # secret
# Redirect stdout to file (overwrite)
echo "text" > file.txt
ls -l > listing.txt
# Redirect stdout to file (append)
echo "more text" >> file.txt
date >> log.txt
# Redirect stderr to file
command 2> error.log
# Redirect both stdout and stderr to file
command &> output.txt
command > output.txt 2>&1 # Equivalent
# Discard output
command > /dev/null # Discard stdout
command 2> /dev/null # Discard stderr
command &> /dev/null # Discard both
# Redirect file to stdin
command < input.txt
wc -l < file.txt
# Read from file
while read -r line; do
echo "$line"
done < file.txt
# Basic heredoc
cat << EOF
This is a multi-line
text block that ends
with EOF
EOF
# Variable expansion in heredoc
name="Alice"
cat << EOF
Hello, $name!
Current directory: $(pwd)
EOF
# Suppress variable expansion (quote delimiter)
cat << 'EOF'
This is $literal text
$(pwd) is not expanded
EOF
# Heredoc to file
cat > config.txt << EOF
setting1=value1
setting2=value2
EOF
# Pass string as stdin
grep "pattern" <<< "text to search"
wc -w <<< "count these words"
# With variable
text="hello world"
tr ' ' '\n' <<< "$text"
# Read into variable
read -r var <<< "value"
echo "$var" # value
# Pipe stdout to next command
ls -l | grep ".txt"
cat file.txt | sort | uniq
# Multiple pipes
ps aux | grep python | grep -v grep | awk '{print $2}'
# Tee (write to file and stdout)
command | tee output.txt # Overwrite
command | tee -a output.txt # Append
# Compare output of two commands
diff <(ls dir1) <(ls dir2)
# Read command output line by line
while read -r line; do
echo "$line"
done < <(find . -name "*.txt")
# Run in background
command &
# Get job PID
sleep 60 &
echo $! # PID of last background job
# Store PID
long_task &
pid=$!
echo "Running with PID: $pid"
| Command | Description |
|---|---|
jobs |
List all jobs |
jobs -l |
List jobs with PIDs |
fg |
Bring last job to foreground |
fg %1 |
Bring job number 1 to foreground |
bg |
Send last stopped job to background |
bg %1 |
Send job number 1 to background |
kill %1 |
Kill job number 1 |
disown |
Remove last job from job table |
# Wait for all background jobs
command1 &
command2 &
command3 &
wait
echo "All done"
# Wait for specific PID
long_task &
pid=$!
wait $pid
echo "Task complete"
# Parallel execution with wait
for i in {1..5}; do
(
echo "Task $i starting"
sleep $((RANDOM % 5))
echo "Task $i done"
) &
done
wait
# Trap signal and execute command
trap 'echo "Caught signal"' SIGINT SIGTERM
# Trap script exit
trap 'echo "Script exiting"' EXIT
# Cleanup on exit
cleanup() {
echo "Cleaning up..."
rm -f /tmp/tempfile.*
}
trap cleanup EXIT
# Common pattern: cleanup temporary files
tmpfile=$(mktemp)
trap "rm -f $tmpfile" EXIT
| Signal | Number | Description |
|---|---|---|
SIGINT |
2 | Interrupt (Ctrl+C) |
SIGTERM |
15 | Terminate (default kill) |
SIGKILL |
9 | Kill (cannot be caught) |
SIGHUP |
1 | Hangup |
SIGQUIT |
3 | Quit (Ctrl+\) |
# Send signal
kill -SIGTERM $pid
kill -15 $pid
kill $pid # Default: SIGTERM
# Current process ID
echo $$
# Parent process ID
echo $PPID
# Last background job PID
command &
echo $!
# Exit status of last command
command
echo $?
#!/bin/bash # Use bash
#!/usr/bin/env bash # Find bash in PATH (portable)
#!/bin/sh # POSIX shell (limited features)
#!/bin/bash -e # Exit on error
#!/bin/bash -x # Debug mode
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# -e: Exit on error
# -u: Exit on undefined variable
# -o pipefail: Pipeline fails if any command fails
# IFS: Set safe Internal Field Separator
| Option | Short | Description |
|---|---|---|
set -e |
errexit |
Exit on error |
set -u |
nounset |
Exit on undefined variable |
set -o pipefail |
- | Pipeline fails if any command fails |
set -x |
xtrace |
Print commands before execution |
set -f |
noglob |
Disable filename expansion |
#!/usr/bin/env bash
#
# Script: script_name.sh
# Description: What this script does
#
set -euo pipefail
IFS=$'\n\t'
# Constants
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
# Variables
verbose=false
dry_run=false
# Functions
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS] ARGS
Description of what the script does.
OPTIONS:
-h, --help Show this help message
-v, --verbose Verbose output
-n, --dry-run Dry run mode
EXAMPLES:
$SCRIPT_NAME -v file.txt
EOF
exit 0
}
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2
}
error() {
echo "[ERROR] $*" >&2
exit 1
}
cleanup() {
log "Cleaning up..."
}
trap cleanup EXIT
# Main script
main() {
log "Script starting"
# Your code here
log "Script complete"
}
main "$@"
# Basic getopts
while getopts "hvo:" opt; do
case $opt in
h)
usage
;;
v)
verbose=true
;;
o)
output="$OPTARG"
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
esac
done
# Shift to positional arguments
shift $((OPTIND - 1))
echo "Remaining args: $*"
# Create temp file
tmpfile=$(mktemp)
echo "Temp file: $tmpfile"
# Create temp directory
tmpdir=$(mktemp -d)
# Cleanup temp file
trap "rm -f $tmpfile" EXIT
# Temp file with template
tmpfile=$(mktemp /tmp/myapp.XXXXXX)
# Custom error handler
error_exit() {
echo "Error: $1" >&2
exit 1
}
[[ -f "$file" ]] || error_exit "File not found: $file"
# Try-catch pattern
if ! command; then
echo "Command failed" >&2
exit 1
fi
# Error trap
handle_error() {
echo "Error on line $1" >&2
exit 1
}
trap 'handle_error $LINENO' ERR
# Check if command exists
command -v git &> /dev/null || {
echo "Error: git is not installed" >&2
exit 1
}
# Check multiple commands
for cmd in git curl jq; do
command -v "$cmd" &> /dev/null || {
echo "Error: $cmd is not installed" >&2
exit 1
}
done
# Check if root
[[ $EUID -eq 0 ]] || {
echo "Error: This script must be run as root" >&2
exit 1
}
# Basic search
grep "pattern" file.txt
# Case-insensitive
grep -i "pattern" file.txt
# Show line numbers
grep -n "pattern" file.txt
# Invert match (exclude lines)
grep -v "pattern" file.txt
# Recursive search
grep -r "pattern" /path/to/dir
# Context lines
grep -A 3 "pattern" file.txt # 3 lines after
grep -B 3 "pattern" file.txt # 3 lines before
grep -C 3 "pattern" file.txt # 3 lines before and after
# Substitute (first occurrence per line)
sed 's/old/new/' file.txt
# Substitute all occurrences
sed 's/old/new/g' file.txt
# In-place edit
sed -i 's/old/new/g' file.txt
# Delete lines
sed '/pattern/d' file.txt # Delete matching lines
sed '5d' file.txt # Delete line 5
sed '$d' file.txt # Delete last line
# Print lines
sed -n '5p' file.txt # Print line 5
sed -n '/pattern/p' file.txt # Print matching lines
# Print columns
awk '{print $1}' file.txt # First column
awk '{print $1, $3}' file.txt # Columns 1 and 3
awk '{print $NF}' file.txt # Last column
# Custom delimiter
awk -F':' '{print $1}' /etc/passwd
# Pattern matching
awk '/pattern/ {print $1}' file.txt
# Sum column
awk '{sum += $1} END {print sum}' numbers.txt
# Count lines
awk 'END {print NR}' file.txt
# Basic sort
sort file.txt
# Reverse sort
sort -r file.txt
# Numeric sort
sort -n numbers.txt
# Sort by column
sort -k2 file.txt # Sort by 2nd column
# Unique sort
sort -u file.txt # Sort and remove duplicates
# Remove duplicate consecutive lines
uniq file.txt
# Count occurrences
uniq -c file.txt
# Character translation
echo "hello" | tr 'a-z' 'A-Z' # Lowercase to uppercase
# Delete characters
echo "hello123" | tr -d '0-9' # Remove digits
# Squeeze repeats
echo "hello world" | tr -s ' ' # Single space
# Replace newlines with spaces
tr '\n' ' ' < file.txt
# Basic usage
echo "file1 file2 file3" | xargs rm
# One argument at a time
echo "1 2 3" | xargs -n1 echo
# Replace string
echo "file1 file2" | xargs -I {} mv {} {}.bak
# Parallel execution
cat urls.txt | xargs -P 4 -n1 curl -O
# Handle spaces in filenames
find . -name "*.txt" -print0 | xargs -0 rm
ShellCheck is a static analysis tool for shell scripts that finds bugs, security issues, and poor practices.
# Check script
shellcheck script.sh
# Ignore specific warnings in script:
# shellcheck disable=SC2086
variable=$unquoted
Ubuntu/Debian: apt install shellcheck
macOS: brew install shellcheck
# Print commands before execution
set -x
bash -x script.sh
# Custom debug output
set -x
PS4='+ ${BASH_SOURCE}:${LINENO}: '
# Debug function
debug() {
[[ $DEBUG ]] && echo "[DEBUG] $*" >&2
}
# Usage
DEBUG=1 bash script.sh
# DryRun pattern
DRY_RUN=false
run() {
if $DRY_RUN; then
echo "Would run: $*"
else
"$@"
fi
}
Always quote variables to prevent word splitting on spaces.
# WRONG: splits on spaces
for file in $files; do
echo "$file"
done
# CORRECT:
for file in "${files[@]}"; do
echo "$file"
done
Variables set in pipes run in subshells and don't affect the parent.
# WRONG: read runs in subshell
echo "value" | read var
echo "$var" # Empty!
# CORRECT: use here string
read var <<< "value"
echo "$var" # value
Use [[ ]] for modern Bash. It's safer and supports more features.
# WRONG with [ ]
[ -f $file ] # Breaks if file contains spaces
# CORRECT with [[ ]]
[[ -f $file ]] # No quotes needed
# Use built-ins instead of external commands
# SLOW:
basename=$(basename "$path")
dirname=$(dirname "$path")
# FAST:
basename="${path##*/}"
dirname="${path%/*}"
# SLOW:
length=$(echo "$var" | wc -c)
# FAST:
length="${#var}"
# Use mapfile/readarray for large files
# FAST:
mapfile -t lines < file.txt
# Find and replace in multiple files
find . -name "*.txt" -exec sed -i 's/old/new/g' {} +
# Find large files
find / -type f -size +100M 2>/dev/null
# Backup with timestamp
cp file.txt "file.txt.$(date +%Y%m%d_%H%M%S).bak"
# Get process using most memory
ps aux | sort -rk4 | head -1
# Create directory and cd into it
mkcd() { mkdir -p "$1" && cd "$1"; }
# Extract any archive
extract() {
case "$1" in
*.tar.gz) tar xzf "$1" ;;
*.zip) unzip "$1" ;;
*.tar.bz2) tar xjf "$1" ;;
*) echo "Unknown format" ;;
esac
}
# Generate random password
openssl rand -base64 32
# Get external IP
curl -s ifconfig.me
# Batch rename files
for file in *.txt; do mv "$file" "${file%.txt}.md"; done
man bash