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
Task: Create a script called sysinfo.sh that displays:
- Current date and time
- Hostname
- Current user
- System uptime
- Disk usage of root partition (/)
- Number of running processes
- 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 "========================================"
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 "================================"
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
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
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
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 bashfor maximum portability - Make executable:
chmod +x script.sh - Run:
./script.sh,bash script.sh, orsource 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 casefor pattern matching
Loops:
for item in list; do ... donewhile [[ condition ]]; do ... doneuntil [[ condition ]]; do ... done- Control:
break,continue
Functions:
- Define:
function_name() { ... } - Parameters:
$1,$2,$@,$# - Always use
localfor variables - Return exit status (0-255), echo for data
User Input:
read -p "Prompt: " variableread -sfor passwordsread -t SECONDSfor timeoutselectfor menus
Best Practices:
- Use
set -euo pipefailfor 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.
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 →