Loops, Functions, and User Input

Summary: in this tutorial, you will learn master bash loops (for, while, until), write reusable functions, handle user input, and build complete automation scripts.

Loops, Functions, and User Input

With conditionals covered, it's time to learn the constructs that make scripts truly powerful: loops for repeating actions, functions for organizing reusable code, and user input for making scripts interactive. Together, these form the backbone of any serious Bash automation.

Loops

Loops repeat commands multiple times—essential for automation.

for Loop

The most common loop for iterating over lists:

# Loop over a list
for color in red green blue yellow; do
    echo "Color: $color"
done
 
# Loop over files
for file in *.txt; do
    echo "Processing: $file"
    wc -l "$file"
done
 
# Loop over files in multiple directories
for file in /var/log/*.log /tmp/*.tmp; do
    [[ -f "$file" ]] && echo "Found: $file"
done
 
# C-style for loop (numeric iteration)
for ((i=0; i<10; i++)); do
    echo "Count: $i"
done
 
# Loop over a range
for i in {1..10}; do
    echo "Number: $i"
done
 
# Range with step
for i in {0..100..5}; do
    echo "Value: $i"  # 0, 5, 10, 15, ..., 100
done
 
# Loop over command output (careful with word splitting!)
for user in $(cut -d: -f1 /etc/passwd); do
    echo "User: $user"
done
 
# Loop over array elements (correct way to handle spaces)
fruits=("apple" "banana" "cherry pie" "date")
for fruit in "${fruits[@]}"; do
    echo "Fruit: $fruit"
done
 
# Loop with both index and value
for i in "${!fruits[@]}"; do
    echo "Index $i: ${fruits[$i]}"
done
 

Handling files with spaces safely:

# WRONG: Breaks with spaces in filenames
for file in $(find . -name "*.txt"); do
    echo "$file"
done
 
# RIGHT: Use while loop with null-terminated output
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
    echo "Processing: $file"
    wc -l "$file"
done
 
# ALTERNATIVE: Use process substitution
while IFS= read -r file; do
    echo "Processing: $file"
done < <(find . -name "*.txt")
 

while Loop

Executes while a condition is true:

# Basic while loop
count=1
while [[ $count -le 5 ]]; do
    echo "Count: $count"
    ((count++))
done
 
# Read a file line by line (BEST way to process files)
while IFS= read -r line; do
    echo "Line: $line"
done < file.txt
 
# Read with specific IFS (e.g., CSV)
while IFS=',' read -r name email phone; do
    echo "Name: $name, Email: $email, Phone: $phone"
done < contacts.csv
 
# Read from command output (creates subshell)
ps aux | while read -r line; do
    echo "$line"
done
 
# Infinite loop (use Ctrl+C to stop)
while true; do
    echo "$(date): Checking status..."
    check_status
    sleep 5
done
 
# Loop until a condition is met
while ! ping -c 1 google.com &>/dev/null; do
    echo "Waiting for network connection..."
    sleep 2
done
echo "Network is up!"
 
# Menu loop
while true; do
    echo "Menu:"
    echo "1) Option 1"
    echo "2) Option 2"
    echo "3) Quit"
    read -p "Choice: " choice
 
    case "$choice" in
        1) echo "You chose option 1" ;;
        2) echo "You chose option 2" ;;
        3) echo "Goodbye!"; break ;;
        *) echo "Invalid choice" ;;
    esac
done
 

until Loop

Opposite of while—executes UNTIL condition becomes true:

# Loop until count exceeds 5
count=1
until [[ $count -gt 5 ]]; do
    echo "Count: $count"
    ((count++))
done
 
# Wait for a file to appear
until [[ -f /tmp/ready.flag ]]; do
    echo "Waiting for ready signal..."
    sleep 1
done
echo "Ready!"
 
# Wait for service to start
until curl -s http://localhost:8080/health &>/dev/null; do
    echo "Waiting for service to start..."
    sleep 2
done
echo "Service is up!"
 
# Retry logic
attempts=0
max_attempts=5
until command_that_might_fail || [[ $attempts -ge $max_attempts ]]; do
    echo "Attempt $((attempts + 1)) failed, retrying..."
    ((attempts++))
    sleep 2
done
 
if [[ $attempts -ge $max_attempts ]]; then
    echo "Failed after $max_attempts attempts"
    exit 1
fi
 

Loop Control: break and continue

# break — exit the loop entirely
for i in {1..100}; do
    if [[ $i -eq 5 ]]; then
        echo "Stopping at 5"
        break
    fi
    echo $i
done
# Output: 1 2 3 4 Stopping at 5
 
# continue — skip to next iteration
for i in {1..10}; do
    if (( i % 2 == 0 )); then
        continue  # Skip even numbers
    fi
    echo $i
done
# Output: 1 3 5 7 9
 
# break with nested loops (break N levels)
for i in {1..3}; do
    for j in {1..3}; do
        echo "$i-$j"
        if [[ $j -eq 2 ]]; then
            break 2    # Break out of BOTH loops
        fi
    done
done
# Output: 1-1 1-2
 
# Practical example: Find first file matching criteria
found=false
for dir in /usr/local/bin /usr/bin /bin; do
    for file in "$dir"/*; do
        if [[ -x "$file" && "$(basename "$file")" == "python3" ]]; then
            echo "Found python3 at: $file"
            found=true
            break 2
        fi
    done
done
[[ "$found" == "false" ]] && echo "python3 not found"
 

Functions

Functions organize code into reusable, named blocks:

Basic Function Syntax

# Method 1: function keyword (optional)
function greet {
    echo "Hello, $1!"
}
 
# Method 2: name() syntax (more common)
greet() {
    echo "Hello, $1!"
}
 
# Call the function
greet "Alice"          # Hello, Alice!
greet "Bob"            # Hello, Bob!
 
# Functions must be defined BEFORE they're called
say_hello    # Error: command not found
 
say_hello() {
    echo "Hello!"
}
 
say_hello    # Now it works
 

Function Parameters

Functions use $1, $2, $@, etc., just like scripts:

create_user() {
    local username="$1"
    local email="$2"
    local role="${3:-user}"    # Default to "user" if not provided
 
    echo "Creating user account"
    echo "  Username: $username"
    echo "  Email: $email"
    echo "  Role: $role"
    echo "  Total arguments: $#"
}
 
create_user "alice" "alice@example.com" "admin"
# Creates admin user
 
create_user "bob" "bob@example.com"
# Creates regular user (role defaults to "user")
 
# Access all arguments
print_all() {
    echo "Number of arguments: $#"
    echo "All arguments: $@"
    for arg in "$@"; do
        echo "  - $arg"
    done
}
 
print_all one two three "four five"
 

Local Variables

# ALWAYS use 'local' for variables inside functions!
 
name="global"
 
change_name() {
    local name="local"     # This is a different variable
    echo "Inside function: $name"    # local
}
 
change_name                        # Inside function: local
echo "Outside function: $name"     # global
 
# Without 'local', function modifies global variable!
bad_function() {
    name="modified"        # Changes the global variable!
}
 
bad_function
echo "After bad_function: $name"   # modified (global was changed!)
 
# Multiple local declarations
process_file() {
    local filename="$1"
    local line_count size status
 
    line_count=$(wc -l < "$filename")
    size=$(du -h "$filename" | cut -f1)
    status="processed"
 
    echo "File: $filename"
    echo "Lines: $line_count"
    echo "Size: $size"
}
 

⚠️ Always use local in functions

Always declare variables inside functions with local. Without it, you'll modify variables in the calling scope, leading to hard-to-debug problems:

# BAD:
process() {
    counter=0    # Modifies global counter!
    for item in "$@"; do
        ((counter++))
    done
}
 
# GOOD:
process() {
    local counter=0    # Local to this function
    for item in "$@"; do
        ((counter++))
    done
}
 

Exception: You may intentionally skip local to return values through global variables, but document this clearly.

Return Values

Bash functions return exit status (0-255), NOT values like other languages:

# Return exit status
is_even() {
    if (( $1 % 2 == 0 )); then
        return 0    # True/success
    else
        return 1    # False/failure
    fi
}
 
# Use in conditionals
if is_even 4; then
    echo "4 is even"        # This runs
fi
 
if is_even 7; then
    echo "7 is even"
else
    echo "7 is odd"         # This runs
fi
 
# Capture exit status
is_even 10
result=$?
if [[ $result -eq 0 ]]; then
    echo "Even"
fi
 

"Return" actual data using echo and command substitution:

get_greeting() {
    local name="$1"
    local hour=$(date +%H)
 
    if (( hour < 12 )); then
        echo "Good morning, $name"
    elif (( hour < 18 )); then
        echo "Good afternoon, $name"
    else
        echo "Good evening, $name"
    fi
}
 
# Capture output
message=$(get_greeting "Alice")
echo "$message"
 
# Calculate and return
calculate_total() {
    local price=$1
    local quantity=$2
    local tax_rate=0.08
 
    local subtotal=$((price * quantity))
    local tax=$(echo "scale=2; $subtotal * $tax_rate" | bc)
    local total=$(echo "scale=2; $subtotal + $tax" | bc)
 
    echo "$total"
}
 
total=$(calculate_total 20 5)
echo "Total: \$$total"
 

Practical Function Examples

# Error logging function
error() {
    echo "ERROR: $*" >&2    # Write to stderr
    exit 1
}
 
# Usage:
[[ -f "$config_file" ]] || error "Config file not found: $config_file"
 
# Logging with timestamps
log() {
    local level="$1"
    shift
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $*" | tee -a app.log
}
 
log "INFO" "Application started"
log "ERROR" "Failed to connect to database"
 
# Backup function
backup_file() {
    local source="$1"
    local backup_dir="${2:-./backups}"
    local timestamp=$(date +%Y%m%d_%H%M%S)
 
    [[ -f "$source" ]] || { echo "Error: $source not found"; return 1; }
 
    mkdir -p "$backup_dir"
    cp "$source" "$backup_dir/$(basename "$source").$timestamp"
    echo "Backed up: $source -> $backup_dir/$(basename "$source").$timestamp"
}
 
backup_file "important.txt"
backup_file "/etc/nginx/nginx.conf" "/backups/nginx"
 
# Cleanup function with confirmation
cleanup() {
    local directory="$1"
    local days="${2:-30}"
    local count
 
    count=$(find "$directory" -type f -mtime +"$days" | wc -l)
 
    if [[ $count -eq 0 ]]; then
        echo "No files to clean up"
        return 0
    fi
 
    echo "Found $count files older than $days days"
    read -p "Delete them? (y/N) " confirm
 
    if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
        find "$directory" -type f -mtime +"$days" -delete
        echo "Cleanup complete"
    else
        echo "Cleanup cancelled"
    fi
}
 
cleanup "/var/log/old" 90
 

User Input

read — Get Input from User

# Basic input
echo -n "Enter your name: "
read name
echo "Hello, $name!"
 
# Prompt with -p flag (cleaner)
read -p "Enter your age: " age
echo "You are $age years old"
 
# Silent input (for passwords)
read -sp "Enter password: " password
echo    # Print newline after hidden input
echo "Password length: ${#password} characters"
 
# Read with timeout
if read -t 5 -p "Quick! Enter something (5 sec): " answer; then
    echo "You entered: $answer"
else
    echo -e "\nToo slow!"
fi
 
# Read into an array
read -p "Enter colors (space-separated): " -a colors
echo "First color: ${colors[0]}"
echo "All colors: ${colors[@]}"
echo "Number of colors: ${#colors[@]}"
 
# Read with default value
read -p "Enter port [8080]: " port
port="${port:-8080}"
echo "Using port: $port"
 
# Read a single character
read -n 1 -p "Continue? (y/n) " answer
echo
if [[ "$answer" == "y" ]]; then
    echo "Continuing..."
else
    echo "Aborted"
    exit 0
fi
 
# Read until specific delimiter
read -p "Enter CSV values: " -d ',' value1
read -p "" -d ',' value2
read -p "" value3
echo "Value 1: $value1"
echo "Value 2: $value2"
echo "Value 3: $value3"
 
# Validation loop
while true; do
    read -p "Enter a number (1-10): " num
    if [[ "$num" =~ ^[0-9]+$ ]] && (( num >= 1 && num <= 10 )); then
        echo "Valid input: $num"
        break
    else
        echo "Invalid input. Try again."
    fi
done
 

select — Create Interactive Menu

#!/bin/bash
 
echo "Select your favorite programming language:"
select lang in Python JavaScript Bash Go Rust Ruby "Quit"; do
    case $lang in
        Python)
            echo "Great choice! Python is versatile and beginner-friendly."
            ;;
        JavaScript)
            echo "The language of the web!"
            ;;
        Bash)
            echo "You're learning it right now!"
            ;;
        Go)
            echo "Fast and great for concurrent programming!"
            ;;
        Rust)
            echo "Memory-safe and blazingly fast!"
            ;;
        Ruby)
            echo "Elegant and expressive!"
            ;;
        "Quit")
            echo "Goodbye!"
            break
            ;;
        *)
            echo "Invalid option $REPLY. Try again."
            ;;
    esac
done
 

Advanced menu with validation:

#!/bin/bash
 
PS3="Enter your choice (1-5): "    # Custom prompt for select
options=("View Files" "Create Backup" "Restore Backup" "Settings" "Exit")
 
select opt in "${options[@]}"; do
    case $REPLY in
        1)
            echo "Listing files..."
            ls -lh
            ;;
        2)
            read -p "Enter directory to backup: " dir
            echo "Creating backup of $dir..."
            tar czf "backup_$(date +%Y%m%d).tar.gz" "$dir"
            echo "Backup complete"
            ;;
        3)
            echo "Restoring backup..."
            # Restore logic here
            ;;
        4)
            echo "Opening settings..."
            # Settings logic here
            ;;
        5)
            echo "Exiting..."
            break
            ;;
        *)
            echo "Invalid option. Please try again."
            ;;
    esac
done
 

Putting It All Together — A Real Script

Here's a comprehensive example combining everything we've learned:

#!/usr/bin/env bash
 
#============================================================================
# Script: file_backup.sh
# Description: Creates timestamped backups with compression and cleanup
# Author: Shell Rag Team
# Date: 2026-02-11
# Version: 1.0.0
#============================================================================
 
# Exit on error, undefined variables, and pipe failures
set -euo pipefail
 
# Configuration
readonly BACKUP_DIR="${BACKUP_DIR:-$HOME/backups}"
readonly LOG_FILE="$BACKUP_DIR/backup.log"
readonly MAX_BACKUPS=10
readonly DATE=$(date +%Y%m%d_%H%M%S)
 
# Functions
 
# Log messages with timestamp
log() {
    local level="$1"
    shift
    local message="$*"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" | tee -a "$LOG_FILE"
}
 
# Display usage information
show_usage() {
    cat << EOF
Usage: $0 [OPTIONS] DIRECTORY...
 
Create timestamped compressed backups of directories.
 
OPTIONS:
    -c DAYS     Cleanup backups older than DAYS (default: 30)
    -o DIR      Output directory (default: ~/backups)
    -v          Verbose mode
    -h          Show this help message
 
EXAMPLES:
    $0 ~/Documents
    $0 -c 7 ~/Documents ~/Pictures
    $0 -o /mnt/backup ~/Projects
 
EOF
}
 
# Create a backup of a directory
create_backup() {
    local source_dir="$1"
    local backup_name
    local backup_path
 
    # Validate source directory
    if [[ ! -d "$source_dir" ]]; then
        log "ERROR" "Source directory does not exist: $source_dir"
        return 1
    fi
 
    # Generate backup filename
    backup_name="backup_$(basename "$source_dir")_${DATE}.tar.gz"
    backup_path="$BACKUP_DIR/$backup_name"
 
    log "INFO" "Creating backup of '$source_dir'..."
 
    # Create the backup
    if tar czf "$backup_path" -C "$(dirname "$source_dir")" "$(basename "$source_dir")" 2>/dev/null; then
        local size
        size=$(du -h "$backup_path" | cut -f1)
        log "SUCCESS" "Backup created: $backup_name ($size)"
        return 0
    else
        log "ERROR" "Failed to create backup of '$source_dir'"
        return 1
    fi
}
 
# Cleanup old backups
cleanup_old_backups() {
    local days="${1:-30}"
    local count
 
    log "INFO" "Searching for backups older than $days days..."
 
    count=$(find "$BACKUP_DIR" -name "backup_*.tar.gz" -type f -mtime +"$days" 2>/dev/null | wc -l)
 
    if [[ $count -eq 0 ]]; then
        log "INFO" "No old backups to clean up"
        return 0
    fi
 
    log "INFO" "Found $count old backup(s)"
    read -p "Delete them? (y/N) " -n 1 -r confirm
    echo
 
    if [[ "$confirm" =~ ^[Yy]$ ]]; then
        find "$BACKUP_DIR" -name "backup_*.tar.gz" -type f -mtime +"$days" -delete
        log "INFO" "Cleanup complete: deleted $count backup(s)"
    else
        log "INFO" "Cleanup cancelled"
    fi
}
 
# Limit total number of backups
limit_backup_count() {
    local max_count="$1"
    local current_count
    local excess
 
    current_count=$(find "$BACKUP_DIR" -name "backup_*.tar.gz" -type f 2>/dev/null | wc -l)
 
    if (( current_count <= max_count )); then
        return 0
    fi
 
    excess=$((current_count - max_count))
    log "INFO" "Removing $excess oldest backup(s) (limit: $max_count)"
 
    find "$BACKUP_DIR" -name "backup_*.tar.gz" -type f -printf '%T+ %p\n' 2>/dev/null | \
        sort | \
        head -n "$excess" | \
        cut -d' ' -f2- | \
        while read -r file; do
            rm "$file"
            log "INFO" "Removed old backup: $(basename "$file")"
        done
}
 
# Main function
main() {
    local cleanup_days=30
    local verbose=false
    local success_count=0
    local failure_count=0
 
    # Parse command-line options
    while getopts ":c:o:vh" opt; do
        case $opt in
            c)
                cleanup_days="$OPTARG"
                if ! [[ "$cleanup_days" =~ ^[0-9]+$ ]]; then
                    echo "Error: -c requires a numeric value" >&2
                    exit 1
                fi
                ;;
            o)
                BACKUP_DIR="$OPTARG"
                ;;
            v)
                verbose=true
                ;;
            h)
                show_usage
                exit 0
                ;;
            \?)
                echo "Error: Invalid option -$OPTARG" >&2
                show_usage
                exit 1
                ;;
            :)
                echo "Error: Option -$OPTARG requires an argument" >&2
                exit 1
                ;;
        esac
    done
    shift $((OPTIND - 1))
 
    # Check for required arguments
    if [[ $# -eq 0 ]]; then
        echo "Error: No directories specified" >&2
        show_usage
        exit 1
    fi
 
    # Create backup directory
    if [[ ! -d "$BACKUP_DIR" ]]; then
        mkdir -p "$BACKUP_DIR" || {
            echo "Error: Cannot create backup directory: $BACKUP_DIR" >&2
            exit 1
        }
    fi
 
    # Initialize log file
    : > "$LOG_FILE"
 
    log "INFO" "========================================="
    log "INFO" "Backup process started"
    log "INFO" "========================================="
    log "INFO" "Backup directory: $BACKUP_DIR"
    log "INFO" "Source directories: $*"
 
    # Create backups
    for dir in "$@"; do
        if create_backup "$dir"; then
            ((success_count++))
        else
            ((failure_count++))
        fi
    done
 
    # Cleanup old backups
    if [[ $cleanup_days -gt 0 ]]; then
        cleanup_old_backups "$cleanup_days"
    fi
 
    # Limit backup count
    limit_backup_count "$MAX_BACKUPS"
 
    # Summary
    log "INFO" "========================================="
    log "INFO" "Backup process completed"
    log "INFO" "Successful: $success_count"
    log "INFO" "Failed: $failure_count"
    log "INFO" "========================================="
 
    # Exit with appropriate code
    [[ $failure_count -eq 0 ]] && exit 0 || exit 1
}
 
# Run main function with all arguments
main "$@"
 

Usage examples:

chmod +x file_backup.sh
 
# Simple backup
./file_backup.sh ~/Documents
 
# Multiple directories
./file_backup.sh ~/Documents ~/Pictures ~/Projects
 
# With cleanup (remove backups older than 7 days)
./file_backup.sh -c 7 ~/Documents
 
# Custom backup location
./file_backup.sh -o /mnt/external/backups ~/Documents
 
# Help
./file_backup.sh -h
 

Exercises

🏋️ Exercise 1: Write Your First Script

Task: Create a script called sysinfo.sh that displays:

  1. Current date and time
  2. Hostname
  3. Current user
  4. System uptime
  5. Disk usage of root partition (/)
  6. Number of running processes
  7. System load average

Format the output in a clean, readable report.

Show Solution
#!/usr/bin/env bash
# sysinfo.sh — System Information Report
 
echo "========================================"
echo "    System Information Report"
echo "========================================"
echo ""
echo "Date/Time:    $(date '+%Y-%m-%d %H:%M:%S')"
echo "Hostname:     $(hostname)"
echo "User:         $(whoami)"
echo "Uptime:       $(uptime -p 2>/dev/null || uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1}')"
echo "Disk (root):  $(df -h / | awk 'NR==2{print $3 " used of " $2 " (" $5 ")"}')"
echo "Processes:    $(ps aux | wc -l) running"
echo "Load Avg:     $(uptime | awk -F'load average:' '{print $2}' | xargs)"
echo ""
echo "========================================"
 
🏋️ Exercise 2: Conditional Logic Challenge

Task: Write a script called file_check.sh that takes a filename as an argument and reports:

  • Whether the file exists
  • If it exists, whether it's a file, directory, or symlink
  • Its size (if file)
  • Its permissions (readable, writable, executable)
  • Number of lines (if it's a text file)

Handle errors gracefully if no argument is provided.

Show Solution
#!/usr/bin/env bash
# file_check.sh — File analysis tool
 
if [[ $# -eq 0 ]]; then
    echo "Usage: $0 <filename>"
    exit 1
fi
 
target="$1"
 
echo "Analyzing: $target"
echo "================================"
 
# Check if exists
if [[ ! -e "$target" ]]; then
    echo "Status: Does not exist"
    exit 1
fi
 
# Determine type
if [[ -L "$target" ]]; then
    echo "Type: Symbolic link -> $(readlink "$target")"
elif [[ -f "$target" ]]; then
    echo "Type: Regular file"
elif [[ -d "$target" ]]; then
    echo "Type: Directory"
else
    echo "Type: Special file"
fi
 
# Size (for files)
if [[ -f "$target" ]]; then
    size=$(du -h "$target" | cut -f1)
    echo "Size: $size"
fi
 
# Permissions
permissions=""
[[ -r "$target" ]] && permissions+="readable "
[[ -w "$target" ]] && permissions+="writable "
[[ -x "$target" ]] && permissions+="executable"
echo "Permissions: ${permissions:-none}"
 
# Line count (for text files)
if [[ -f "$target" ]] && file "$target" 2>/dev/null | grep -q "text"; then
    lines=$(wc -l < "$target")
    echo "Lines: $lines"
fi
 
echo "================================"
 
🏋️ Exercise 3: Loop Practice

Task: Write a script table.sh that generates a multiplication table for a number (1-10). The number should be provided as a command-line argument. If no argument is given, prompt the user for input.

Example output for ./table.sh 7:


Multiplication Table for 7
==========================
7 x  1 =  7
7 x  2 = 14
7 x  3 = 21
...
7 x 10 = 70

Show Solution
#!/usr/bin/env bash
# table.sh — Multiplication table generator
 
# Get number from argument or prompt
if [[ $# -eq 0 ]]; then
    read -p "Enter a number: " num
else
    num=$1
fi
 
# Validate input
if ! [[ "$num" =~ ^[0-9]+$ ]]; then
    echo "Error: '$num' is not a valid number" >&2
    exit 1
fi
 
# Generate table
echo "Multiplication Table for $num"
echo "============================"
for i in {1..10}; do
    result=$((num * i))
    printf "%d x %2d = %3d\n" "$num" "$i" "$result"
done
 
🏋️ Exercise 4: Function Challenge

Task: Write a script with a function is_palindrome that checks if a string is a palindrome (reads the same forwards and backwards). Test it with multiple strings. The function should:

  • Ignore case
  • Ignore spaces
  • Return 0 (true) if palindrome, 1 (false) otherwise
💡 Hint
Use rev command or parameter expansion to reverse strings. Use ${var,,} to convert to lowercase.
Show Solution
#!/usr/bin/env bash
# palindrome.sh — Palindrome checker
 
is_palindrome() {
    local original="$1"
    local cleaned="${original,,}"    # Convert to lowercase
    cleaned="${cleaned// /}"         # Remove all spaces
 
    # Reverse the string
    local reversed
    reversed=$(echo "$cleaned" | rev)
 
    # Compare
    if [[ "$cleaned" == "$reversed" ]]; then
        return 0    # Is palindrome
    else
        return 1    # Not palindrome
    fi
}
 
# Test cases
test_strings=(
    "racecar"
    "hello"
    "madam"
    "A man a plan a canal Panama"
    "world"
    "level"
    "Was it a car or a cat I saw"
    "Never odd or even"
)
 
echo "Palindrome Test Results"
echo "======================="
 
for word in "${test_strings[@]}"; do
    if is_palindrome "$word"; then
        echo "✓ '$word' IS a palindrome"
    else
        echo "✗ '$word' is NOT a palindrome"
    fi
done
 
🏋️ Exercise 5: Complete Script Challenge

Task: Write a complete directory cleaner script clean_old_files.sh that:

  • Takes a directory path and age (in days) as arguments
  • Finds all files older than the specified age
  • Shows the count and total size
  • Asks for confirmation before deleting
  • Logs all actions to a log file
  • Includes proper error handling
Show Solution
#!/usr/bin/env bash
# clean_old_files.sh — Clean old files from directory
 
set -euo pipefail
 
LOG_FILE="$HOME/clean_old_files.log"
 
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
 
show_usage() {
    cat << EOF
Usage: $0 DIRECTORY DAYS
 
Remove files older than DAYS from DIRECTORY.
 
Arguments:
    DIRECTORY    Target directory to clean
    DAYS         Age threshold in days
 
Example:
    $0 /tmp/cache 30
EOF
}
 
main() {
    # Check arguments
    if [[ $# -ne 2 ]]; then
        show_usage
        exit 1
    fi
 
    local directory="$1"
    local days="$2"
 
    # Validate directory
    if [[ ! -d "$directory" ]]; then
        echo "Error: Directory not found: $directory" >&2
        exit 1
    fi
 
    # Validate days
    if ! [[ "$days" =~ ^[0-9]+$ ]]; then
        echo "Error: Days must be a number" >&2
        exit 1
    fi
 
    log "Scanning $directory for files older than $days days"
 
    # Find old files
    local count
    count=$(find "$directory" -type f -mtime +"$days" 2>/dev/null | wc -l)
 
    if [[ $count -eq 0 ]]; then
        log "No files found older than $days days"
        exit 0
    fi
 
    # Calculate total size
    local total_size
    total_size=$(find "$directory" -type f -mtime +"$days" -exec du -ch {} + 2>/dev/null | tail -1 | cut -f1)
 
    # Show summary
    echo "Found: $count files"
    echo "Total size: $total_size"
    echo ""
    echo "Files to be deleted:"
    find "$directory" -type f -mtime +"$days" -exec ls -lh {} \; 2>/dev/null | awk '{print $9, "(" $5 ")"}'
    echo ""
 
    # Confirm
    read -p "Delete these files? (yes/NO) " confirm
    if [[ "$confirm" != "yes" ]]; then
        log "Operation cancelled"
        exit 0
    fi
 
    # Delete files
    log "Deleting $count files..."
    find "$directory" -type f -mtime +"$days" -delete 2>/dev/null
    log "Cleanup complete: deleted $count files ($total_size)"
}
 
main "$@"
 

Summary

You now have the foundation for Bash scripting:

Script Basics:

  • Shebang: #!/usr/bin/env bash for maximum portability
  • Make executable: chmod +x script.sh
  • Run: ./script.sh, bash script.sh, or source script.sh
  • Exit status: 0 = success, non-zero = failure

Conditionals:

  • if [[ condition ]]; then ... fi
  • Prefer [[ ]] over [ ] for safety and features
  • Test files: -f, -d, -e, -r, -w, -x, -s
  • Compare numbers: -eq, -ne, -gt, -ge, -lt, -le
  • Compare strings: ==, !=, <, >, -z, -n
  • case for pattern matching

Loops:

  • for item in list; do ... done
  • while [[ condition ]]; do ... done
  • until [[ condition ]]; do ... done
  • Control: break, continue

Functions:

  • Define: function_name() { ... }
  • Parameters: $1, $2, $@, $#
  • Always use local for variables
  • Return exit status (0-255), echo for data

User Input:

  • read -p "Prompt: " variable
  • read -s for passwords
  • read -t SECONDS for timeout
  • select for menus

Best Practices:

  • Use set -euo pipefail for safety
  • Quote variables: "$var"
  • Use functions to organize code
  • Log actions for debugging
  • Validate input and handle errors
  • Document with comments

In the next tutorial, you'll learn about text processing—using powerful tools like grep, sed, and awk to manipulate and analyze text data.

Was this page helpful?
SR

Written by the ShellRAG Team

The ShellRAG editorial team writes practical, beginner-friendly Bash Shell tutorials with tested code examples and real-world use cases. Every article is technically reviewed for accuracy and updated regularly.

Learn more about us →