Toolmingo
Guides20 min read

Bash Scripting Tutorial for Beginners (2025): Learn Shell Scripting Step by Step

Complete bash scripting tutorial for beginners. Learn shell scripting from scratch: variables, loops, conditionals, functions, file handling, and build real automation scripts. Free guide with examples.

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.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools