Bash scripting lets you automate repetitive tasks, manage files, deploy servers, and glue tools together — all from the command line. Whether you're a developer, sysadmin, or DevOps engineer, bash scripting is one of the most useful skills you can have on Linux and macOS.
What you'll learn
| Topic | What you'll be able to do |
|---|---|
| Basics | Write and run your first bash script |
| Variables | Store and use data |
| Input/Output | Read user input, print formatted output |
| Conditionals | Make decisions with if/elif/else |
| Loops | Automate repetitive tasks |
| Functions | Organize and reuse code |
| File handling | Read, write, and process files |
| String/array ops | Manipulate text and lists |
| Real projects | Build backup scripts, log parsers, deploy scripts |
Why bash scripting?
| Use case | Example |
|---|---|
| Automation | Backup files, batch rename, scheduled tasks |
| DevOps | Deploy scripts, CI/CD pipelines, server setup |
| System admin | Monitor disk space, manage processes, parse logs |
| Data processing | ETL scripts, CSV parsing, log analysis |
| Development | Build scripts, test runners, environment setup |
| Job market | Required for Linux/DevOps/Cloud/SRE roles |
Bash vs other scripting languages
| Bash | Python | PowerShell | Ruby | |
|---|---|---|---|---|
| Platform | Linux/macOS (native) | Cross-platform | Windows/cross-platform | Cross-platform |
| System commands | Native — no import | subprocess module |
Cmdlets | system() calls |
| Text processing | Excellent (grep/sed/awk) | Good | Good | Good |
| Learning curve | Medium (quirky syntax) | Low | Medium | Medium |
| Best for | System scripts, pipelines | Complex logic, data | Windows automation | Ruby ecosystem |
| Available by default | Yes (most Linux/macOS) | Often yes | Windows: yes | No |
1. Your First Bash Script
Creating and running a script
Create a file hello.sh:
#!/bin/bash
# My first bash script
echo "Hello, World!"
echo "Current user: $USER"
echo "Current directory: $PWD"
echo "Date: $(date)"
Make it executable and run:
chmod +x hello.sh # grant execute permission
./hello.sh # run it
Output:
Hello, World!
Current user: alice
Current directory: /home/alice
Date: Wed Jul 16 14:32:00 UTC 2026
The shebang line
The first line #!/bin/bash (the "shebang") tells the OS which interpreter to use:
#!/bin/bash # use bash (most common)
#!/bin/sh # use POSIX sh (more portable, fewer features)
#!/usr/bin/env bash # find bash in PATH (better for portability)
Running scripts — three ways
./script.sh # run as executable (requires chmod +x)
bash script.sh # run with bash explicitly (no chmod needed)
source script.sh # run in current shell (variables persist)
2. Variables
Defining and using variables
#!/bin/bash
# Define variables — NO spaces around =
name="Alice"
age=30
pi=3.14159
# Use variables with $
echo "Name: $name"
echo "Age: $age"
echo "Pi: $pi"
# Curly braces (safer in strings)
echo "Hello, ${name}!"
echo "File: ${name}_backup.tar.gz"
Variable types
# String
greeting="Hello, World"
# Integer (bash treats all as strings by default)
count=42
# Readonly variable
readonly MAX_RETRIES=3
# Unset a variable
unset greeting
Special variables
| Variable | Description | Example |
|---|---|---|
$0 |
Script name | ./deploy.sh |
$1, $2, ... |
Positional arguments | ./deploy.sh prod v1.2 → $1=prod |
$# |
Number of arguments | 2 |
$@ |
All arguments (array) | prod v1.2 |
$* |
All arguments (string) | prod v1.2 |
$? |
Exit code of last command | 0 = success, non-zero = error |
$$ |
Current script PID | 12345 |
$! |
PID of last background job | 12346 |
$HOME |
Home directory | /home/alice |
$PWD |
Current directory | /home/alice/projects |
$USER |
Current username | alice |
$PATH |
Executable search path | /usr/bin:/usr/local/bin:... |
#!/bin/bash
echo "Script: $0"
echo "First arg: $1"
echo "Second arg: $2"
echo "All args: $@"
echo "Arg count: $#"
Command substitution
# Capture command output into a variable
current_date=$(date +%Y-%m-%d)
file_count=$(ls -1 | wc -l)
git_branch=$(git branch --show-current)
echo "Date: $current_date"
echo "Files: $file_count"
echo "Branch: $git_branch"
Arithmetic
# Using $(( ))
a=10
b=3
echo $((a + b)) # 13
echo $((a - b)) # 7
echo $((a * b)) # 30
echo $((a / b)) # 3 (integer division)
echo $((a % b)) # 1 (remainder)
echo $((a ** b)) # 1000 (exponentiation)
# Increment
count=0
((count++))
((count += 5))
echo $count # 6
# For floating point, use bc
result=$(echo "scale=2; 10 / 3" | bc)
echo $result # 3.33
3. Input and Output
Reading user input
#!/bin/bash
# Basic input
echo "What is your name?"
read name
echo "Hello, $name!"
# Inline prompt with -p
read -p "Enter your age: " age
echo "You are $age years old."
# Silent input (for passwords)
read -s -p "Enter password: " password
echo # newline after hidden input
echo "Password saved."
# Read with timeout
read -t 10 -p "Enter choice (10s): " choice
if [ $? -ne 0 ]; then
echo "Timed out!"
fi
# Read multiple values
read -p "Enter first and last name: " first last
echo "First: $first, Last: $last"
Output formatting
# echo options
echo "Simple output"
echo -n "No newline at end"
echo -e "Tab:\there\nNewline above"
# printf (more control)
printf "Name: %-15s Age: %3d\n" "Alice" 30
printf "Pi: %.4f\n" 3.14159
printf "%s\n" "line1" "line2" "line3"
# Colors in output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color (reset)
echo -e "${GREEN}Success:${NC} Deployment complete"
echo -e "${RED}Error:${NC} File not found"
echo -e "${YELLOW}Warning:${NC} Low disk space"
Redirecting output
# Redirect stdout to file
echo "Hello" > output.txt # overwrite
echo "World" >> output.txt # append
# Redirect stderr
command 2> errors.txt # stderr to file
command 2>&1 # stderr to stdout
command > output.txt 2>&1 # both to file
command &> output.txt # same (bash shorthand)
# Suppress output
command > /dev/null # discard stdout
command > /dev/null 2>&1 # discard stdout + stderr
# Pipe output
ls -la | grep ".sh"
cat file.txt | wc -l
echo "hello world" | tr '[:lower:]' '[:upper:]'
4. Conditionals
if / elif / else
#!/bin/bash
age=25
if [ $age -lt 18 ]; then
echo "Minor"
elif [ $age -lt 65 ]; then
echo "Adult"
else
echo "Senior"
fi
Comparison operators
Numeric comparisons:
| Operator | Meaning | Example |
|---|---|---|
-eq |
equal | [ $a -eq $b ] |
-ne |
not equal | [ $a -ne $b ] |
-lt |
less than | [ $a -lt $b ] |
-le |
less than or equal | [ $a -le $b ] |
-gt |
greater than | [ $a -gt $b ] |
-ge |
greater than or equal | [ $a -ge $b ] |
String comparisons:
| Operator | Meaning | Example |
|---|---|---|
= |
equal | [ "$a" = "$b" ] |
!= |
not equal | [ "$a" != "$b" ] |
< |
less than (alphabetically) | [[ "$a" < "$b" ]] |
> |
greater than | [[ "$a" > "$b" ]] |
-z |
empty string | [ -z "$a" ] |
-n |
non-empty string | [ -n "$a" ] |
File tests:
| Operator | Meaning |
|---|---|
-f file |
file exists and is a regular file |
-d dir |
directory exists |
-e path |
path exists (file or dir) |
-r file |
file is readable |
-w file |
file is writable |
-x file |
file is executable |
-s file |
file exists and is non-empty |
-L link |
path is a symlink |
# File tests
if [ -f "/etc/hosts" ]; then
echo "File exists"
fi
if [ ! -d "/tmp/backup" ]; then
mkdir /tmp/backup
fi
# Combining conditions
if [ -f "config.yml" ] && [ -r "config.yml" ]; then
echo "Config exists and is readable"
fi
if [ $status -eq 0 ] || [ $retries -gt 3 ]; then
echo "Done"
fi
[[ ]] vs [ ] vs (( ))
# [ ] — POSIX compatible, basic
[ "$name" = "Alice" ]
# [[ ]] — bash extended, supports regex, no word splitting issues (preferred)
[[ "$name" == "Alice" ]]
[[ "$name" =~ ^Al ]] # regex match
[[ -f file && -r file ]] # && inside [[ ]]
# (( )) — arithmetic evaluation
(( count > 0 ))
(( a + b == 10 ))
case statement
#!/bin/bash
read -p "Enter day (1-7): " day
case $day in
1) echo "Monday" ;;
2) echo "Tuesday" ;;
3) echo "Wednesday" ;;
4) echo "Thursday" ;;
5) echo "Friday" ;;
6|7) echo "Weekend" ;;
*) echo "Invalid day" ;;
esac
# case with pattern matching
read -p "Enter file: " filename
case $filename in
*.txt) echo "Text file" ;;
*.sh) echo "Shell script" ;;
*.jpg|*.png|*.gif) echo "Image file" ;;
*) echo "Unknown file type" ;;
esac
5. Loops
for loop
#!/bin/bash
# Loop over list
for fruit in apple banana cherry; do
echo "Fruit: $fruit"
done
# Loop over range
for i in {1..5}; do
echo "Number: $i"
done
# Range with step
for i in {0..20..5}; do
echo $i # 0 5 10 15 20
done
# C-style for loop
for ((i=0; i<5; i++)); do
echo "Index: $i"
done
# Loop over files
for file in *.sh; do
echo "Script: $file"
done
# Loop over command output
for user in $(cat /etc/passwd | cut -d: -f1); do
echo "User: $user"
done
# Loop with array
fruits=("apple" "banana" "cherry")
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
while loop
#!/bin/bash
# Basic while
count=1
while [ $count -le 5 ]; do
echo "Count: $count"
((count++))
done
# Read file line by line
while IFS= read -r line; do
echo "Line: $line"
done < "input.txt"
# Read pipe
ps aux | while read -r line; do
echo "Process: $line"
done
# Infinite loop with break
while true; do
read -p "Enter 'quit' to exit: " input
if [ "$input" = "quit" ]; then
break
fi
echo "You entered: $input"
done
# While with condition update
retries=0
max_retries=3
while [ $retries -lt $max_retries ]; do
if ping -c 1 example.com &>/dev/null; then
echo "Connected!"
break
fi
((retries++))
echo "Retry $retries/$max_retries..."
sleep 2
done
until loop
# Until runs while condition is FALSE (opposite of while)
count=1
until [ $count -gt 5 ]; do
echo "Count: $count"
((count++))
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 == 0 )); then
continue
fi
echo "Odd: $i"
done
6. Functions
Defining and calling functions
#!/bin/bash
# Define function
greet() {
echo "Hello, $1!"
}
# Call function
greet "Alice"
greet "Bob"
# Function with multiple args
add() {
local result=$(( $1 + $2 ))
echo $result
}
sum=$(add 5 3)
echo "5 + 3 = $sum"
Return values
# Bash functions return exit codes (0-255), not values
# Use echo to return data
get_username() {
echo "alice"
}
username=$(get_username)
echo "User: $username"
# Return exit code to signal success/failure
is_even() {
if (( $1 % 2 == 0 )); then
return 0 # success (true)
else
return 1 # failure (false)
fi
}
if is_even 4; then
echo "4 is even"
fi
if ! is_even 5; then
echo "5 is odd"
fi
Local variables
# Without local: variables are global
demo_global() {
x=10 # global!
}
demo_global
echo $x # prints 10
# With local: scoped to function
demo_local() {
local y=20 # local only
}
demo_local
echo $y # empty — y is gone
Practical function example
#!/bin/bash
# Logging functions
log_info() { echo -e "\033[0;32m[INFO]\033[0m $*"; }
log_warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $*" >&2; }
# Check if command exists
command_exists() {
command -v "$1" &>/dev/null
}
# Check root privileges
require_root() {
if [ "$EUID" -ne 0 ]; then
log_error "This script must be run as root"
exit 1
fi
}
# Create directory if it doesn't exist
ensure_dir() {
local dir="$1"
if [ ! -d "$dir" ]; then
mkdir -p "$dir"
log_info "Created directory: $dir"
fi
}
# Usage
if command_exists docker; then
log_info "Docker is installed"
else
log_warn "Docker not found"
fi
ensure_dir "/tmp/myapp/logs"
7. Arrays
Basic array operations
#!/bin/bash
# Define array
fruits=("apple" "banana" "cherry" "date")
# Access elements (0-indexed)
echo "${fruits[0]}" # apple
echo "${fruits[2]}" # cherry
echo "${fruits[-1]}" # date (last element)
# All elements
echo "${fruits[@]}" # apple banana cherry date
echo "${fruits[*]}" # same but as single string
# Array length
echo "${#fruits[@]}" # 4
# Array indices
echo "${!fruits[@]}" # 0 1 2 3
# Slice
echo "${fruits[@]:1:2}" # banana cherry (start at index 1, take 2)
# Add element
fruits+=("elderberry")
fruits[5]="fig"
# Remove element
unset fruits[1] # removes "banana", keeps index gap
# Reindex
fruits=("${fruits[@]}") # removes gaps
# Loop over array
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# Loop with index
for i in "${!fruits[@]}"; do
echo "$i: ${fruits[$i]}"
done
Associative arrays (dictionaries)
#!/bin/bash
# Declare associative array
declare -A person
person["name"]="Alice"
person["age"]="30"
person["city"]="New York"
# Access
echo "${person[name]}"
# All keys
echo "${!person[@]}"
# All values
echo "${person[@]}"
# Check if key exists
if [[ -v person["name"] ]]; then
echo "Name exists: ${person[name]}"
fi
# Iterate
for key in "${!person[@]}"; do
echo "$key: ${person[$key]}"
done
8. String Operations
#!/bin/bash
str="Hello, World!"
# Length
echo "${#str}" # 13
# Uppercase / lowercase
echo "${str^^}" # HELLO, WORLD!
echo "${str,,}" # hello, world!
# Substring
echo "${str:7}" # World!
echo "${str:7:5}" # World
# Find and replace
echo "${str/World/Bash}" # Hello, Bash! (first match)
echo "${str//l/L}" # HeLLo, WorLd! (all matches)
# Remove prefix/suffix
filename="backup-2026-07-16.tar.gz"
echo "${filename#backup-}" # 2026-07-16.tar.gz (remove prefix)
echo "${filename##*.}" # gz (remove longest prefix up to last .)
echo "${filename%.tar.gz}" # backup-2026-07-16 (remove suffix)
echo "${filename%.*}" # backup-2026-07-16.tar (remove last extension)
# Default value if empty
name=""
echo "${name:-"Anonymous"}" # Anonymous (use default if empty)
echo "${name:="Guest"}" # Guest (assign default if empty)
# Check if string contains substring
if [[ "$str" == *"World"* ]]; then
echo "Contains World"
fi
# Regex match
if [[ "$str" =~ ^Hello ]]; then
echo "Starts with Hello"
fi
9. File Handling
Reading and writing files
#!/bin/bash
# Write to file
echo "Line 1" > output.txt
echo "Line 2" >> output.txt
printf "Line 3\nLine 4\n" >> output.txt
# Read entire file
content=$(cat output.txt)
echo "$content"
# Read file line by line
while IFS= read -r line; do
echo ">>> $line"
done < output.txt
# Read into array
mapfile -t lines < output.txt # or readarray
echo "${lines[0]}" # first line
echo "${#lines[@]}" # line count
# Process CSV
while IFS=',' read -r name age city; do
echo "Name: $name, Age: $age, City: $city"
done < data.csv
File and directory operations
#!/bin/bash
# Create
touch newfile.txt
mkdir -p path/to/dir
# Copy
cp source.txt dest.txt
cp -r source_dir/ dest_dir/
# Move / rename
mv old.txt new.txt
mv file.txt /tmp/
# Delete (careful!)
rm file.txt
rm -rf directory/ # recursive, force — dangerous!
# Check before delete
if [ -f "file.txt" ]; then
rm file.txt
echo "Deleted"
fi
# Find files
find . -name "*.log" -type f
find /var/log -name "*.log" -mtime +7 # older than 7 days
find . -size +10M # larger than 10MB
# Get file info
file_size=$(stat -c %s "file.txt") # size in bytes
file_lines=$(wc -l < "file.txt") # line count
file_mod=$(stat -c %Y "file.txt") # modification time (epoch)
10. Error Handling
Exit codes and set options
#!/bin/bash
# Check exit code
cp source.txt dest.txt
if [ $? -ne 0 ]; then
echo "Copy failed!"
exit 1
fi
# Strict mode — recommended for production scripts
set -euo pipefail
# -e: exit on any error
# -u: error on undefined variables
# -o pipefail: pipe fails if any command fails
# Trap errors
trap 'echo "Error on line $LINENO"' ERR
# Cleanup on exit
cleanup() {
echo "Cleaning up..."
rm -f /tmp/myapp_lock
}
trap cleanup EXIT
# Trap signals
trap 'echo "Interrupted!"; exit 130' INT TERM
Error handling patterns
#!/bin/bash
set -euo pipefail
# Function that can fail
deploy_app() {
local env="$1"
if [ -z "$env" ]; then
echo "Error: environment required" >&2
return 1
fi
echo "Deploying to $env..."
# ... deployment commands
}
# Run with error check
if ! deploy_app "production"; then
echo "Deployment failed!" >&2
exit 1
fi
# Or/and patterns
mkdir -p /tmp/backup || { echo "Cannot create backup dir"; exit 1; }
cd /tmp/backup && echo "Changed to backup dir"
# Discard errors for specific commands
rm -f file.txt 2>/dev/null || true
11. Real-World Scripts
Script 1: Backup script
#!/bin/bash
# backup.sh — backup a directory with timestamp
set -euo pipefail
# Configuration
SOURCE_DIR="${1:-$HOME}"
BACKUP_DIR="${2:-/tmp/backups}"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_${TIMESTAMP}.tar.gz"
# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
log() { echo -e "${GREEN}[$(date +%H:%M:%S)]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
# Validate
[ -d "$SOURCE_DIR" ] || error "Source directory not found: $SOURCE_DIR"
mkdir -p "$BACKUP_DIR" || error "Cannot create backup directory"
# Create backup
log "Backing up $SOURCE_DIR..."
tar -czf "${BACKUP_DIR}/${BACKUP_NAME}" \
--exclude="$SOURCE_DIR/node_modules" \
--exclude="$SOURCE_DIR/.git" \
--exclude="$SOURCE_DIR/__pycache__" \
"$SOURCE_DIR"
# Verify
BACKUP_SIZE=$(du -sh "${BACKUP_DIR}/${BACKUP_NAME}" | cut -f1)
log "Backup created: ${BACKUP_DIR}/${BACKUP_NAME} (${BACKUP_SIZE})"
# Cleanup — keep only last 5 backups
cd "$BACKUP_DIR"
ls -t backup_*.tar.gz 2>/dev/null | tail -n +6 | xargs -r rm
log "Old backups cleaned. Remaining: $(ls backup_*.tar.gz 2>/dev/null | wc -l)"
Script 2: Log analyzer
#!/bin/bash
# analyze-logs.sh — parse Apache/Nginx access logs
set -euo pipefail
LOG_FILE="${1:-/var/log/nginx/access.log}"
[ -f "$LOG_FILE" ] || { echo "Log file not found: $LOG_FILE" >&2; exit 1; }
echo "=== Log Analysis: $LOG_FILE ==="
echo "Total requests: $(wc -l < "$LOG_FILE")"
echo ""
echo "=== Top 10 IP addresses ==="
awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
echo ""
echo "=== Top 10 requested URLs ==="
awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
echo ""
echo "=== HTTP status codes ==="
awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn
echo ""
echo "=== Errors (4xx/5xx) ==="
awk '$9 ~ /^[45]/' "$LOG_FILE" | awk '{print $9, $7}' | sort | uniq -c | sort -rn | head -20
Script 3: Deploy script
#!/bin/bash
# deploy.sh — simple deployment script
set -euo pipefail
ENVIRONMENT="${1:-staging}"
APP_DIR="/var/www/myapp"
REPO_URL="git@github.com:user/myapp.git"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
RELEASE_DIR="${APP_DIR}/releases/${TIMESTAMP}"
CURRENT_LINK="${APP_DIR}/current"
log() { echo "[$(date +%H:%M:%S)] $*"; }
error() { echo "[ERROR] $*" >&2; exit 1; }
# Validate environment
case "$ENVIRONMENT" in
staging|production) ;;
*) error "Invalid environment: $ENVIRONMENT (use: staging|production)" ;;
esac
log "Deploying to $ENVIRONMENT..."
# Pull code
log "Fetching latest code..."
mkdir -p "$RELEASE_DIR"
git clone --depth 1 "$REPO_URL" "$RELEASE_DIR"
# Install dependencies
log "Installing dependencies..."
cd "$RELEASE_DIR"
npm ci --production
# Run tests
if [ "$ENVIRONMENT" = "production" ]; then
log "Running tests..."
npm test || error "Tests failed — aborting deploy"
fi
# Symlink to current
log "Switching to new release..."
ln -sfn "$RELEASE_DIR" "$CURRENT_LINK"
# Restart app
log "Restarting application..."
systemctl restart myapp || pm2 restart myapp
# Health check
sleep 3
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/health)
if [ "$HTTP_CODE" != "200" ]; then
error "Health check failed (HTTP $HTTP_CODE) — check logs!"
fi
log "Deploy successful! Release: $TIMESTAMP"
# Keep last 5 releases
ls -t "${APP_DIR}/releases/" | tail -n +6 | xargs -I{} rm -rf "${APP_DIR}/releases/{}"
log "Old releases cleaned up."
Script 4: System health monitor
#!/bin/bash
# monitor.sh — check system health and alert on thresholds
set -euo pipefail
DISK_THRESHOLD=80
CPU_THRESHOLD=90
MEM_THRESHOLD=85
ALERT_EMAIL="${ALERT_EMAIL:-admin@example.com}"
alert() {
local subject="$1"
local message="$2"
echo "$message" | mail -s "$subject" "$ALERT_EMAIL" 2>/dev/null || true
echo "[ALERT] $subject: $message"
}
# Disk usage
check_disk() {
while read -r usage mountpoint; do
usage_num="${usage%\%}"
if (( usage_num >= DISK_THRESHOLD )); then
alert "Disk Alert: $mountpoint" \
"Disk usage at ${usage}% on $mountpoint (threshold: ${DISK_THRESHOLD}%)"
fi
done < <(df -h | awk 'NR>1 && /^\// {print $5, $6}')
}
# CPU usage (1-second sample)
check_cpu() {
local cpu_idle
cpu_idle=$(top -bn1 | grep "Cpu(s)" | awk '{print $8}' | tr -d '%')
local cpu_used=$(echo "100 - $cpu_idle" | bc | cut -d. -f1)
if (( cpu_used >= CPU_THRESHOLD )); then
alert "CPU Alert" "CPU usage at ${cpu_used}% (threshold: ${CPU_THRESHOLD}%)"
fi
echo "CPU: ${cpu_used}%"
}
# Memory usage
check_mem() {
local mem_total mem_used mem_pct
mem_total=$(free -m | awk '/^Mem:/{print $2}')
mem_used=$(free -m | awk '/^Mem:/{print $3}')
mem_pct=$(echo "scale=0; $mem_used * 100 / $mem_total" | bc)
if (( mem_pct >= MEM_THRESHOLD )); then
alert "Memory Alert" "Memory usage at ${mem_pct}% (threshold: ${MEM_THRESHOLD}%)"
fi
echo "Memory: ${mem_pct}% (${mem_used}MB / ${mem_total}MB)"
}
echo "=== System Health Check: $(date) ==="
check_disk
check_cpu
check_mem
echo "=== Done ==="
12. Script Best Practices
Template for production scripts
#!/usr/bin/env bash
# script-name.sh — one-line description
# Usage: ./script-name.sh [options] <arg>
#
# Options:
# -h, --help Show this help message
# -v, --verbose Enable verbose output
# -n, --dry-run Show what would be done without doing it
#
# Examples:
# ./script-name.sh -v myarg
# ./script-name.sh --dry-run myarg
set -euo pipefail
# ===== Constants =====
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
readonly TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
# ===== Defaults =====
VERBOSE=false
DRY_RUN=false
# ===== Colors =====
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m'
# ===== Logging =====
log() { echo -e "${GREEN}[${SCRIPT_NAME}]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
verbose() { $VERBOSE && echo "[DEBUG] $*" || true; }
die() { error "$*"; exit 1; }
# ===== Usage =====
usage() {
grep '^#' "$0" | grep -v '#!/' | sed 's/^# \?//'
exit 0
}
# ===== Arg parsing =====
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help) usage ;;
-v|--verbose) VERBOSE=true ;;
-n|--dry-run) DRY_RUN=true ;;
--) shift; break ;;
-*) die "Unknown option: $1" ;;
*) break ;;
esac
shift
done
# Remaining args in "$@"
}
# ===== Main =====
main() {
parse_args "$@"
log "Starting..."
# Your logic here
log "Done."
}
main "$@"
Common mistakes to avoid
| Mistake | Wrong | Right |
|---|---|---|
| Unquoted variables | if [ $name = "Alice" ] |
if [ "$name" = "Alice" ] |
| Spaces in assignment | name = "Alice" |
name="Alice" |
Missing local |
myfunc() { x=1; } |
myfunc() { local x=1; } |
| No strict mode | #!/bin/bash |
#!/bin/bash + set -euo pipefail |
Parsing ls output |
for f in $(ls *.txt) |
for f in *.txt |
Not quoting $@ |
for arg in $@ |
for arg in "$@" |
| Missing exit codes | No check after cp |
`cp src dst |
| Hardcoded paths | /home/alice/file |
$HOME/file or relative path |
Bash vs related tools
| Tool | When to use |
|---|---|
| Bash | System automation, file processing, DevOps scripts, Linux admin |
| Python | Complex logic, OOP, data processing, when bash becomes unreadable |
| Make | Build systems, dependency-driven task runners |
| Ansible | Multi-server configuration management at scale |
| Terraform | Infrastructure as code (cloud resources) |
| awk | Column-based text processing in one liners |
| sed | Stream editing — find/replace in pipelines |
| jq | JSON processing in bash pipelines |
Learning path
| Stage | Topics | Time |
|---|---|---|
| Beginner | Variables, echo, if/else, basic loops, files | Week 1–2 |
| Intermediate | Functions, arrays, string ops, error handling | Week 3–4 |
| Advanced | Process substitution, signals, subshells, regex | Month 2 |
| Expert | Performance tuning, portability, security hardening | Month 3+ |
6 frequently asked questions
Q: Should I use #!/bin/bash or #!/bin/sh?
Use #!/bin/bash for most scripts — you get arrays, [[ ]], (( )), and other bash features. Use #!/bin/sh only when you need POSIX portability across systems where bash may not be present (embedded Linux, some Docker containers).
Q: When should I switch from bash to Python?
When your script exceeds ~200 lines, needs OOP, handles complex data structures, calls many APIs, or becomes hard to read. The rule of thumb: if you're fighting bash syntax, switch to Python.
Q: How do I debug a bash script?
Add set -x at the top (or run bash -x script.sh) to print every command before executing it. Add set -e to stop on first error. Use echo or printf liberally, and check $? after critical commands.
Q: Is [ ] or [[ ]] better?
Use [[ ]] in bash scripts — it's safer (no word splitting), supports regex with =~, and allows &&/|| inside the brackets. Use [ ] only when writing POSIX sh scripts for portability.
Q: How do I pass arguments to a bash script?
Arguments come in as $1, $2, etc. ($0 is the script name, $@ is all args, $# is the count). Parse complex options with getopts (built-in, POSIX) or a while case loop for long options like --verbose.
Q: How do I schedule a bash script?
Use cron for recurring tasks: crontab -e and add a line like 0 2 * * * /path/to/backup.sh (runs at 2 AM daily). Use @reboot to run at startup. For one-time scheduling, use at. For complex workflows, consider systemd timers or a job scheduler.