Complete Guide to Bash Scripting

Introduction to Bash Scripting

Bash (Bourne Again Shell) scripting is a powerful way to automate tasks, combine commands, and create complex programs in Unix/Linux environments. Bash scripts are text files containing a series of commands that can be executed sequentially or with control flow logic.

Key Concepts

  • Shebang: The first line #!/bin/bash that tells the system which interpreter to use
  • Variables: Store and manipulate data
  • Control Flow: Conditionals, loops, and case statements
  • Functions: Reusable code blocks
  • Input/Output: Reading user input and displaying output
  • Exit Status: Every command returns a status code (0 for success, non-zero for failure)

1. Bash Script Basics

Script Structure and Shebang

#!/bin/bash
# This is a comment
# Filename: myscript.sh
echo "Hello, World!"
# Make script executable:
# chmod +x myscript.sh
# Run: ./myscript.sh

Script Execution Methods

# Method 1: Make executable and run
chmod +x script.sh
./script.sh
# Method 2: Run with bash interpreter
bash script.sh
# Method 3: Source the script (runs in current shell)
source script.sh
. script.sh
# Method 4: Run in subshell
( bash script.sh )
# Debug mode
bash -x script.sh     # Print each command before execution
bash -v script.sh     # Print input lines as they are read
bash -n script.sh     # Check syntax without executing

Basic Script Template

#!/bin/bash
# Script Name: template.sh
# Description: Template for bash scripts
# Author: Your Name
# Date: $(date +%Y-%m-%d)
# Version: 1.0
# Exit on error, undefined variable, and pipe failure
set -euo pipefail
# Script variables
SCRIPT_NAME=$(basename "$0")
SCRIPT_DIR=$(dirname "$0")
VERSION="1.0"
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
# Usage function
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]
Options:
-h, --help      Show this help message
-v, --verbose   Verbose output
-f, --file      Input file
-o, --output    Output file
Examples:
$SCRIPT_NAME -f input.txt -o output.txt
EOF
}
# Parse command line arguments
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-v|--verbose)
VERBOSE=true
shift
;;
-f|--file)
INPUT_FILE="$2"
shift 2
;;
-o|--output)
OUTPUT_FILE="$2"
shift 2
;;
*)
log_error "Unknown option: $1"
usage
exit 1
;;
esac
done
}
# Main function
main() {
log_info "Starting $SCRIPT_NAME version $VERSION"
# Your script logic here
if [[ -n "${INPUT_FILE:-}" ]]; then
log_info "Processing file: $INPUT_FILE"
fi
log_info "Script completed successfully"
}
# Trap exit for cleanup
cleanup() {
log_info "Cleaning up..."
# Remove temp files, etc.
}
trap cleanup EXIT
# Run main function with all arguments
parse_args "$@"
main

2. Variables and Data Types

Variable Declaration and Usage

#!/bin/bash
# Variable assignment (no spaces around =)
name="John"
age=25
pi=3.14159
# Using variables
echo "Name: $name"
echo "Age: ${age}"  # Braces optional, but useful for complex expressions
# Read-only variables
readonly MAX_RETRIES=5
# Unset variables
unset name
# Command substitution
current_date=$(date +%Y-%m-%d)
files_count=$(ls -1 | wc -l)
# Arithmetic
result=$((5 + 3 * 2))
counter=$((counter + 1))
# String concatenation
greeting="Hello, " "World!"  # This works
full_greeting="Hello, $name!"
# Arrays
fruits=("apple" "banana" "orange")
fruits[3]="grape"
echo "First fruit: ${fruits[0]}"
echo "All fruits: ${fruits[@]}"
echo "Number of fruits: ${#fruits[@]}"
# Associative arrays (bash 4+)
declare -A capitals
capitals["USA"]="Washington"
capitals["France"]="Paris"
echo "Capital of USA: ${capitals["USA"]}"
# Environment variables
echo "Home directory: $HOME"
echo "User: $USER"
echo "Path: $PATH"
# Special variables
echo "Script name: $0"
echo "First argument: $1"
echo "All arguments: $@"
echo "Number of arguments: $#"
echo "Last command exit status: $?"
echo "Process ID: $$"

Variable Scope

#!/bin/bash
# Global variable
global_var="I'm global"
function test_scope() {
# Local variable
local local_var="I'm local"
echo "Inside function: $global_var"  # Can access global
echo "Inside function: $local_var"    # Can access local
# Modify global
global_var="Modified in function"
}
test_scope
echo "Outside function: $global_var"  # Modified
echo "Outside function: $local_var"    # Empty (not accessible)
# Export variables to child processes
export exported_var="Visible to child processes"

3. Input and Output

Reading Input

#!/bin/bash
# Read single line
echo "Enter your name:"
read name
echo "Hello, $name!"
# Read with prompt
read -p "Enter your age: " age
# Read password (silent)
read -sp "Enter password: " password
echo
# Read with timeout
read -t 5 -p "Quick! Enter something (5 seconds): " quick_input
# Read multiple variables
read -p "Enter first and last name: " first last
# Read into array
read -a colors -p "Enter colors (space-separated): "
# Read from file
while read line; do
echo "Line: $line"
done < file.txt
# Read with custom IFS
old_IFS=$IFS
IFS=","
read -p "Enter comma-separated values: " val1 val2 val3
IFS=$old_IFS
# Heredoc input
cat << EOF > output.txt
Line 1
Line 2
Line 3
EOF
# Here string
grep "pattern" <<< "this is a test string"

Output and Formatting

#!/bin/bash
# Basic echo
echo "Simple message"
# Echo without newline
echo -n "This will not have a newline"
# Enable escape sequences
echo -e "Line1\nLine2\tTabbed"
# Printf (more formatting control)
printf "Name: %s, Age: %d\n" "John" 25
printf "Price: %.2f\n" 19.995
# Formatting table
printf "%-10s %-8s %s\n" "Name" "Age" "City"
printf "%-10s %-8d %s\n" "John" 25 "New York"
printf "%-10s %-8d %s\n" "Alice" 30 "London"
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
echo -e "${RED}Error message${NC}"
echo -e "${GREEN}Success message${NC}"
# Redirect output
echo "To file" > output.txt          # Overwrite
echo "Append to file" >> output.txt   # Append
# Redirect errors
command 2> error.txt                   # Only errors
command > output.txt 2>&1              # Both stdout and stderr
command &> all_output.txt               # Both (bash 4+)
# Multiple redirections
command > stdout.txt 2> stderr.txt
# Here document
cat <<- EOF > script.sh
# This is a generated script
echo "Hello World"
EOF
# Process substitution
diff <(ls dir1) <(ls dir2)
# Named pipes (FIFO)
mkfifo mypipe
command1 > mypipe &
command2 < mypipe

4. Control Structures

Conditional Statements (if-elif-else)

#!/bin/bash
# Basic if
if [ "$name" = "John" ]; then
echo "Hello John"
fi
# if-else
if [ -f "$file" ]; then
echo "File exists"
else
echo "File does not exist"
fi
# if-elif-else
if [ "$score" -ge 90 ]; then
echo "Grade A"
elif [ "$score" -ge 80 ]; then
echo "Grade B"
elif [ "$score" -ge 70 ]; then
echo "Grade C"
else
echo "Grade F"
fi
# Test conditions
# File tests
[ -f "$file" ]   # File exists and is regular file
[ -d "$dir" ]    # Directory exists
[ -e "$path" ]   # Path exists
[ -r "$file" ]   # File is readable
[ -w "$file" ]   # File is writable
[ -x "$file" ]   # File is executable
[ -s "$file" ]   # File is not empty
[ -L "$link" ]   # File is symbolic link
# String tests
[ -z "$str" ]    # String is empty
[ -n "$str" ]    # String is not empty
[ "$str1" = "$str2" ]  # Strings equal
[ "$str1" != "$str2" ] # Strings not equal
[[ "$str1" == *pattern* ]] # Pattern matching (bash)
# Numeric tests
[ "$a" -eq "$b" ] # Equal
[ "$a" -ne "$b" ] # Not equal
[ "$a" -lt "$b" ] # Less than
[ "$a" -le "$b" ] # Less than or equal
[ "$a" -gt "$b" ] # Greater than
[ "$a" -ge "$b" ] # Greater than or equal
# Logical operators
[ "$a" -gt 0 ] && [ "$a" -lt 10 ]  # AND
[ "$a" -lt 0 ] || [ "$a" -gt 10 ]  # OR
[ ! -f "$file" ]                    # NOT
# Double brackets (bash-specific, more features)
if [[ "$str" == *substring* ]]; then
echo "Contains substring"
fi
if [[ "$str" =~ ^[0-9]+$ ]]; then
echo "All digits"
fi
# Ternary-like expression
result=$([ "$a" -gt "$b" ] && echo "a" || echo "b")

Case Statements

#!/bin/bash
# Basic case
case "$option" in
start)
echo "Starting service"
;;
stop)
echo "Stopping service"
;;
restart)
echo "Restarting service"
;;
status)
echo "Checking status"
;;
*)
echo "Unknown option: $option"
exit 1
;;
esac
# Case with multiple patterns
case "$file" in
*.txt|*.text)
echo "Text file"
;;
*.jpg|*.jpeg|*.png|*.gif)
echo "Image file"
;;
*.sh)
echo "Shell script"
;;
*)
echo "Unknown file type"
;;
esac
# Case with ranges and character classes
case "$char" in
[0-9])
echo "Digit"
;;
[a-z])
echo "Lowercase letter"
;;
[A-Z])
echo "Uppercase letter"
;;
?)
echo "Single character"
;;
*)
echo "Other"
;;
esac
# Case with fall-through (using ;& or ;;& in bash)
case "$value" in
pattern1)
echo "Pattern 1"
;&  # Continue to next pattern
pattern2)
echo "Pattern 2"
;;
esac

Loops

#!/bin/bash
# For loop with list
for fruit in apple banana orange; do
echo "I like $fruit"
done
# For loop with range
for i in {1..5}; do
echo "Number: $i"
done
# For loop with step
for i in {1..10..2}; do
echo "Odd: $i"
done
# C-style for loop
for ((i=0; i<10; i++)); do
echo "i = $i"
done
# For loop with array
fruits=("apple" "banana" "orange")
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
# For loop with command output
for file in $(ls *.txt); do
echo "Text file: $file"
done
# While loop
count=1
while [ $count -le 5 ]; do
echo "Count: $count"
((count++))
done
# While loop reading file
while IFS= read -r line; do
echo "Line: $line"
done < file.txt
# Until loop (opposite of while)
until [ $count -gt 10 ]; do
echo "Count: $count"
((count++))
done
# Infinite loops
while true; do
echo "Press Ctrl+C to exit"
sleep 1
done
# Loop control
for i in {1..10}; do
if [ $i -eq 5 ]; then
continue  # Skip rest of iteration
fi
if [ $i -eq 8 ]; then
break     # Exit loop
fi
echo "i = $i"
done
# Nested loops
for i in {1..3}; do
for j in {1..3}; do
echo "($i, $j)"
done
done
# Loop with select menu
select option in "Option 1" "Option 2" "Option 3" "Quit"; do
case $option in
"Option 1")
echo "You chose option 1"
;;
"Option 2")
echo "You chose option 2"
;;
"Option 3")
echo "You chose option 3"
;;
"Quit")
break
;;
*)
echo "Invalid option"
;;
esac
done

5. Functions

Function Definition and Usage

#!/bin/bash
# Simple function
greet() {
echo "Hello, $1!"
}
# Call function
greet "John"
# Function with multiple parameters
add_numbers() {
local sum=$(( $1 + $2 ))
echo $sum
}
result=$(add_numbers 5 3)
echo "Result: $result"
# Function with return value (exit status)
is_file_exists() {
if [ -f "$1" ]; then
return 0  # Success
else
return 1  # Failure
fi
}
if is_file_exists "file.txt"; then
echo "File exists"
fi
# Function returning string
get_user_info() {
local name="John"
local age=25
echo "$name:$age"  # Return as string
}
# Parse returned string
IFS=':' read -r name age <<< "$(get_user_info)"
# Function with local variables
process_data() {
local input="$1"
local temp_var="temporary"
echo "Processing: $input"
# temp_var is not accessible outside
}
# Function with default arguments
log_message() {
local level="${2:-INFO}"  # Default to INFO
echo "[$level] $1"
}
log_message "Starting process"
log_message "Error occurred" "ERROR"
# Recursive function
factorial() {
if [ $1 -le 1 ]; then
echo 1
else
local prev=$(factorial $(( $1 - 1 )))
echo $(( $1 * prev ))
fi
}
echo "Factorial of 5: $(factorial 5)"
# Function library
# Save as lib.sh
#!/bin/bash
function1() { ... }
function2() { ... }
# Source in main script
source ./lib.sh
function1

6. Arrays

Array Operations

#!/bin/bash
# Indexed arrays
fruits=("apple" "banana" "orange")
fruits[3]="grape"
# Access elements
echo "${fruits[0]}"      # First element
echo "${fruits[@]}"      # All elements
echo "${fruits[*]}"      # All elements (as single string)
echo "${#fruits[@]}"     # Array length
echo "${!fruits[@]}"     # Array indices
# Slicing
echo "${fruits[@]:1:2}"  # Elements 1 and 2
# Adding elements
fruits+=("kiwi" "mango")
# Removing elements
unset fruits[1]           # Remove element at index 1
# Associative arrays (bash 4+)
declare -A capitals
capitals=(
["USA"]="Washington"
["France"]="Paris"
["Japan"]="Tokyo"
)
# Access associative arrays
echo "${capitals["USA"]}"
echo "${!capitals[@]}"    # Keys
echo "${capitals[@]}"      # Values
echo "${#capitals[@]}"     # Number of elements
# Loop through array
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
# Loop with index
for i in "${!fruits[@]}"; do
echo "Index $i: ${fruits[$i]}"
done
# Loop through associative array
for country in "${!capitals[@]}"; do
echo "Capital of $country is ${capitals[$country]}"
done
# Array from command output
files=($(ls *.txt))
# Array operations
# Join elements
joined=$(IFS=,; echo "${fruits[*]}")
# Check if element exists
if [[ " ${fruits[@]} " =~ " apple " ]]; then
echo "Apple found"
fi
# Sort array
sorted=($(for fruit in "${fruits[@]}"; do echo $fruit; done | sort))
# Unique values
unique=($(printf "%s\n" "${fruits[@]}" | sort -u))

7. String Manipulation

String Operations

#!/bin/bash
# String length
str="Hello World"
echo "${#str}"  # 11
# Substring extraction
echo "${str:0:5}"   # Hello
echo "${str:6}"     # World
echo "${str: -5}"   # World (negative index)
# Substring replacement
echo "${str/World/Everyone}"  # Replace first match
echo "${str//l/L}"            # Replace all matches
echo "${str/#Hello/Hi}"       # Replace at beginning
echo "${str/%World/Planet}"   # Replace at end
# Pattern removal
echo "${str#He*}"   # Remove shortest matching prefix
echo "${str##H* }"  # Remove longest matching prefix
echo "${str%l*}"    # Remove shortest matching suffix
echo "${str%%l*}"   # Remove longest matching suffix
# Case conversion (bash 4+)
echo "${str^^}"     # Uppercase
echo "${str,,}"     # Lowercase
echo "${str~}"      # Reverse case of first character
echo "${str~~}"     # Reverse case of all characters
# Default values
unset var
echo "${var:-default}"     # Use default if unset/null
echo "${var:=default}"     # Assign default if unset
echo "${var:?error}"       # Error if unset/null
echo "${var:+alternative}" # Use alternative if set
# String splitting
IFS=' ' read -ra words <<< "$str"
# String joining
joined=$(IFS=,; echo "${words[*]}")
# Check if string contains substring
if [[ "$str" == *"World"* ]]; then
echo "Contains World"
fi
# Regular expression matching
if [[ "$str" =~ ^Hello.*World$ ]]; then
echo "Pattern matches"
fi
# Trim whitespace
trimmed=$(echo "$str" | xargs)
# Pad string
printf "%10s\n" "$str"    # Right align
printf "%-10s\n" "$str"    # Left align

8. File Operations

File Handling

#!/bin/bash
# Check file properties
if [ -f "$file" ]; then echo "Regular file"; fi
if [ -d "$dir" ]; then echo "Directory"; fi
if [ -L "$link" ]; then echo "Symbolic link"; fi
if [ -r "$file" ]; then echo "Readable"; fi
if [ -w "$file" ]; then echo "Writable"; fi
if [ -x "$file" ]; then echo "Executable"; fi
if [ -s "$file" ]; then echo "Not empty"; fi
if [ -e "$path" ]; then echo "Exists"; fi
# File comparison
if [ "$file1" -nt "$file2" ]; then echo "file1 newer"; fi
if [ "$file1" -ot "$file2" ]; then echo "file1 older"; fi
if [ "$file1" -ef "$file2" ]; then echo "same file"; fi
# File operations
cp source dest
mv old new
rm file
mkdir -p path/to/dir
rmdir emptydir
rm -rf dir
# Reading files
while IFS= read -r line; do
echo "$line"
done < file.txt
# Writing to files
echo "content" > file.txt
echo "more" >> file.txt
# File descriptors
exec 3< input.txt     # Open for reading
exec 4> output.txt    # Open for writing
exec 5>> append.txt   # Open for appending
read <&3 line
echo "$line" >&4
exec 3<&-             # Close file descriptor
exec 4>&-
# Temporary files
temp_file=$(mktemp)
temp_dir=$(mktemp -d)
trap "rm -f $temp_file; rm -rf $temp_dir" EXIT
# File locking
exec 3>file.lock
flock -n 3 || exit 1
# Critical section
flock -u 3
# Watch file for changes
inotifywait -m file.txt |
while read path action file; do
echo "File changed: $action"
done

9. Error Handling and Debugging

Error Handling Techniques

#!/bin/bash
# Exit on error
set -e          # Exit on any error
set -u          # Exit on undefined variable
set -o pipefail # Exit on pipe failure
# Combined
set -euo pipefail
# Trap errors
trap 'echo "Error on line $LINENO"' ERR
# Trap exit
trap 'cleanup' EXIT
# Trap signals
trap 'echo "Interrupted"; exit 1' INT TERM
# Custom error function
error_exit() {
echo "Error: $1" >&2
exit 1
}
# Usage
[ -f "$file" ] || error_exit "File not found: $file"
# Check command success
if command; then
echo "Success"
else
echo "Failed with exit code $?"
fi
# Retry logic
retry() {
local n=1
local max=5
local delay=2
while true; do
if "$@"; then
break
else
if [[ $n -lt $max ]]; then
((n++))
echo "Command failed. Attempt $n/$max:"
sleep $delay
else
echo "The command has failed after $n attempts."
return 1
fi
fi
done
}
# Debug mode
debug() {
if [ "${DEBUG:-false}" = "true" ]; then
echo "DEBUG: $*" >&2
fi
}
DEBUG=true
debug "Variable value: $var"
# Logging levels
log() {
local level="$1"
shift
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$timestamp] [$level] $*"
}
log "INFO" "Processing started"
log "ERROR" "Something went wrong"
# Assert function
assert() {
if ! "$@"; then
echo "Assertion failed: $*" >&2
exit 1
fi
}
assert [ -f "$config_file" ]

10. Advanced Bash Features

Process Management

#!/bin/bash
# Background processes
long_running_task &
pid=$!
# Wait for specific PID
wait $pid
# Wait for all background jobs
wait
# Job control
jobs           # List jobs
fg %1         # Bring job 1 to foreground
bg %1         # Put job 1 in background
kill %1       # Kill job 1
# Process substitution
diff <(ls dir1) <(ls dir2)
# Coprocesses
coproc myproc { command; }
# Send signal
kill -TERM $pid
kill -0 $pid   # Check if process exists
# Process groups
set -m         # Enable job control
kill -- -$PGID # Kill process group
# Daemonize
daemonize() {
(cd /; exec "$@" </dev/null >/dev/null 2>&1 &)
}
# PID file management
pidfile="/var/run/myscript.pid"
if [ -f "$pidfile" ]; then
pid=$(cat "$pidfile")
if kill -0 "$pid" 2>/dev/null; then
echo "Already running with PID $pid"
exit 1
fi
fi
echo $$ > "$pidfile"

Regular Expressions

#!/bin/bash
# =~ operator for regex matching
if [[ "$string" =~ ^[0-9]+$ ]]; then
echo "All digits"
fi
# Capture groups
if [[ "$string" =~ ([0-9]{3})-([0-9]{4}) ]]; then
echo "Area: ${BASH_REMATCH[1]}"
echo "Number: ${BASH_REMATCH[2]}"
fi
# Common patterns
[[ "$email" =~ ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ ]]
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]
[[ "$date" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]
# Using grep with regex
grep -E "^[0-9]+$" file.txt
grep -P "\d+" file.txt  # Perl-compatible regex
# Using sed with regex
sed -E 's/[0-9]{3}-[0-9]{4}/(xxx)/g' file.txt
# Using awk with regex
awk '/^[0-9]+$/ {print}' file.txt

Here Documents and Here Strings

#!/bin/bash
# Here document
cat << EOF > file.txt
Line 1
Line 2
Line 3
EOF
# Here document with variable expansion
name="John"
cat << EOF
Hello $name,
This is a multi-line
message.
EOF
# Here document without expansion
cat << 'EOF'
Variable $name will not be expanded
EOF
# Here document with leading tabs stripped
cat <<- EOF
This line starts with tabs
which will be stripped.
EOF
# Here string
grep "pattern" <<< "this is a test string"
# Multiple here documents
command1 << EOF1
data1
EOF1
command2 << EOF2
data2
EOF2
# Here document as input
while read line; do
echo "Read: $line"
done << EOF
line1
line2
line3
EOF

11. Practical Script Examples

System Information Script

#!/bin/bash
# system_info.sh - Display system information
set -euo pipefail
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_header() {
echo -e "${BLUE}================================${NC}"
echo -e "${GREEN}$1${NC}"
echo -e "${BLUE}================================${NC}"
}
get_system_info() {
print_header "System Information"
echo "Hostname: $(hostname)"
echo "OS: $(uname -s -r)"
echo "Uptime: $(uptime | awk '{print $3,$4}' | sed 's/,//')"
echo "Load average: $(uptime | awk -F'load average:' '{print $2}')"
print_header "CPU Information"
echo "Model: $(grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2 | xargs)"
echo "Cores: $(nproc)"
echo "Architecture: $(uname -m)"
print_header "Memory Information"
free -h | awk 'NR==1{print $1 "\t" $2 "\t" $3 "\t" $4} NR==2{print $1 "\t" $2 "\t" $3 "\t" $4}'
print_header "Disk Usage"
df -h | grep -E "^/dev/" | awk '{print $1 "\t" $2 "\t" $3 "\t" $4 "\t" $5}'
print_header "Network Information"
ip -4 addr show | grep inet | awk '{print $2 "\t" $NF}' | sed 's/\/[0-9]*//'
print_header "Top 5 Processes by CPU"
ps aux --sort=-%cpu | head -6 | awk '{print $1 "\t" $2 "\t" $3 "%\t" $11}'
}
# Optional: save to file
if [ "$1" = "-f" ]; then
get_system_info > "system_info_$(date +%Y%m%d_%H%M%S).txt"
else
get_system_info
fi

Backup Script

#!/bin/bash
# backup.sh - Automated backup script
set -euo pipefail
# Configuration
BACKUP_DIR="/backup"
SOURCE_DIRS=("/home/user/documents" "/home/user/pictures")
EXCLUDE=("*.tmp" "*.log" ".cache")
RETENTION_DAYS=7
LOG_FILE="/var/log/backup.log"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
NC='\033[0m'
log() {
local level="$1"
shift
local message="$*"
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
echo -e "${timestamp} [${level}] ${message}" | tee -a "$LOG_FILE"
}
error_exit() {
log "ERROR" "$1"
exit 1
}
check_prerequisites() {
# Check if backup directory exists
if [ ! -d "$BACKUP_DIR" ]; then
mkdir -p "$BACKUP_DIR" || error_exit "Cannot create backup directory"
fi
# Check if source directories exist
for dir in "${SOURCE_DIRS[@]}"; do
if [ ! -d "$dir" ]; then
error_exit "Source directory not found: $dir"
fi
done
# Check available space
local available=$(df "$BACKUP_DIR" | awk 'NR==2 {print $4}')
if [ "$available" -lt 1048576 ]; then  # Less than 1GB free
error_exit "Insufficient disk space"
fi
}
create_backup() {
local timestamp=$(date '+%Y%m%d_%H%M%S')
local backup_file="${BACKUP_DIR}/backup_${timestamp}.tar.gz"
log "INFO" "Starting backup to $backup_file"
# Build exclude options
local exclude_opts=()
for pattern in "${EXCLUDE[@]}"; do
exclude_opts+=("--exclude=$pattern")
done
# Create backup
if tar -czf "$backup_file" "${exclude_opts[@]}" "${SOURCE_DIRS[@]}" 2>> "$LOG_FILE"; then
local size=$(du -h "$backup_file" | cut -f1)
log "INFO" "Backup completed successfully. Size: $size"
# Create checksum
md5sum "$backup_file" > "${backup_file}.md5"
log "INFO" "Checksum created"
else
error_exit "Backup failed"
fi
}
rotate_backups() {
log "INFO" "Rotating backups older than $RETENTION_DAYS days"
find "$BACKUP_DIR" -name "backup_*.tar.gz" -type f -mtime +"$RETENTION_DAYS" -print0 |
while IFS= read -r -d '' file; do
rm -f "$file" "${file}.md5"
log "INFO" "Removed old backup: $(basename "$file")"
done
}
verify_backup() {
local latest=$(ls -t "$BACKUP_DIR"/backup_*.tar.gz 2>/dev/null | head -1)
if [ -n "$latest" ]; then
log "INFO" "Verifying latest backup: $(basename "$latest")"
# Verify checksum
if md5sum -c "${latest}.md5" >> "$LOG_FILE" 2>&1; then
log "INFO" "Checksum verification passed"
else
log "ERROR" "Checksum verification failed"
return 1
fi
# Test archive integrity
if tar -tzf "$latest" > /dev/null 2>&1; then
log "INFO" "Archive integrity check passed"
else
log "ERROR" "Archive integrity check failed"
return 1
fi
else
log "WARNING" "No backup found to verify"
fi
}
# Main execution
main() {
log "INFO" "=== Backup Script Started ==="
check_prerequisites
create_backup
verify_backup
rotate_backups
log "INFO" "=== Backup Script Completed ==="
}
# Trap errors
trap 'error_exit "Script interrupted"' INT TERM
trap 'log "INFO" "Script completed"' EXIT
# Run main
main "$@"

File Organizer Script

#!/bin/bash
# organize.sh - Organize files by type
set -euo pipefail
# Configuration
TARGET_DIR="${1:-.}"
ORGANIZE_BY="${2:-extension}"  # extension, date, size
DRY_RUN=false
# Color output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
NC='\033[0m'
usage() {
cat << EOF
Usage: $0 [directory] [option]
Options:
-e, --extension    Organize by file extension (default)
-d, --date        Organize by modification date
-s, --size        Organize by file size
-n, --dry-run     Show what would be done without actually moving
-h, --help        Show this help message
EOF
}
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1" >&2
}
move_file() {
local file="$1"
local dest="$2"
if [ "$DRY_RUN" = true ]; then
log "Would move: $(basename "$file") -> $dest/"
else
mkdir -p "$dest"
mv -v "$file" "$dest/" | sed 's/^/  /'
fi
}
organize_by_extension() {
log "Organizing by extension in $TARGET_DIR"
find "$TARGET_DIR" -maxdepth 1 -type f | while read -r file; do
# Skip if file is in a subdirectory
if [ "$(dirname "$file")" != "$TARGET_DIR" ]; then
continue
fi
# Get extension
ext="${file##*.}"
# Handle files without extension
if [ "$ext" = "$file" ] || [ -z "$ext" ]; then
ext="no_extension"
fi
# Convert to lowercase
ext=$(echo "$ext" | tr '[:upper:]' '[:lower:]')
# Create destination directory
dest="$TARGET_DIR/$ext"
move_file "$file" "$dest"
done
}
organize_by_date() {
log "Organizing by date in $TARGET_DIR"
find "$TARGET_DIR" -maxdepth 1 -type f | while read -r file; do
if [ "$(dirname "$file")" != "$TARGET_DIR" ]; then
continue
fi
# Get modification date
year=$(date -r "$file" +%Y)
month=$(date -r "$file" +%m)
dest="$TARGET_DIR/$year/$month"
move_file "$file" "$dest"
done
}
organize_by_size() {
log "Organizing by size in $TARGET_DIR"
find "$TARGET_DIR" -maxdepth 1 -type f | while read -r file; do
if [ "$(dirname "$file")" != "$TARGET_DIR" ]; then
continue
fi
# Get file size in bytes
size=$(stat -c%s "$file")
# Categorize by size
if [ "$size" -lt 1024 ]; then
category="tiny"          # < 1KB
elif [ "$size" -lt 1048576 ]; then
category="small"          # < 1MB
elif [ "$size" -lt 10485760 ]; then
category="medium"         # < 10MB
elif [ "$size" -lt 104857600 ]; then
category="large"          # < 100MB
else
category="huge"           # > 100MB
fi
dest="$TARGET_DIR/$category"
move_file "$file" "$dest"
done
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-e|--extension)
ORGANIZE_BY="extension"
shift
;;
-d|--date)
ORGANIZE_BY="date"
shift
;;
-s|--size)
ORGANIZE_BY="size"
shift
;;
-n|--dry-run)
DRY_RUN=true
shift
;;
-h|--help)
usage
exit 0
;;
-*)
error "Unknown option: $1"
usage
exit 1
;;
*)
TARGET_DIR="$1"
shift
;;
esac
done
# Validate target directory
if [ ! -d "$TARGET_DIR" ]; then
error "Directory not found: $TARGET_DIR"
exit 1
fi
# Main execution
log "Starting file organization in: $TARGET_DIR"
case "$ORGANIZE_BY" in
extension)
organize_by_extension
;;
date)
organize_by_date
;;
size)
organize_by_size
;;
esac
log "Organization complete!"
# Show summary
if [ "$DRY_RUN" = false ]; then
log "Final directory structure:"
tree -L 2 "$TARGET_DIR" 2>/dev/null || ls -R "$TARGET_DIR" | head -20
fi

12. Best Practices and Tips

Script Header Template

#!/usr/bin/env bash
#===============================================================================
#
#         FILE: script.sh
#        USAGE: ./script.sh [options]
#
#  DESCRIPTION: Brief description of what the script does
#
#      OPTIONS: see usage() function
# REQUIREMENTS: List required commands/packages
#         BUGS: ---
#        NOTES: ---
#       AUTHOR: Your Name, [email protected]
#      VERSION: 1.0
#      CREATED: $(date +%Y-%m-%d)
#     REVISION: ---
#===============================================================================
set -euo pipefail
# Script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
# Source configuration if exists
[ -f "$SCRIPT_DIR/config.sh" ] && source "$SCRIPT_DIR/config.sh"
# Default values
VERBOSE=false
DEBUG=false
#===============================================================================
# FUNCTIONS
#===============================================================================
usage() {
cat << EOF
Usage: $SCRIPT_NAME [OPTIONS]
Options:
-h, --help      Show this help message
-v, --verbose   Verbose output
-d, --debug     Debug mode
EOF
}
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
error() {
log "ERROR: $*" >&2
exit 1
}
debug() {
[ "$DEBUG" = true ] && log "DEBUG: $*"
}
#===============================================================================
# MAIN SCRIPT
#===============================================================================
main() {
log "Starting $SCRIPT_NAME"
# Your code here
log "Finished $SCRIPT_NAME"
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
usage
exit 0
;;
-v|--verbose)
VERBOSE=true
shift
;;
-d|--debug)
DEBUG=true
shift
;;
--)
shift
break
;;
-*)
error "Unknown option: $1"
;;
*)
break
;;
esac
done
# Trap exit
trap 'error "Script interrupted"' INT TERM
trap 'log "Script completed"' EXIT
# Run main
main "$@"

Coding Standards

#!/bin/bash
# 1. Use meaningful variable names
# Good
user_name="John"
file_count=$(ls -1 | wc -l)
# Bad
n="John"
c=$(ls -1 | wc -l)
# 2. Use local variables in functions
process_file() {
local file="$1"
local temp_var="temporary"
# ...
}
# 3. Quote variables
# Good
rm -f "$file"
# Bad
rm -f $file  # Breaks if filename has spaces
# 4. Use [[ ]] for tests (bash)
# Good
[[ "$name" == "John" ]] && [[ -f "$file" ]]
# 5. Use (( )) for arithmetic
# Good
((result = a + b))
# Bad
result=$((a + b))  # Also acceptable, but (( )) is cleaner for conditions
# 6. Use functions for reusable code
cleanup() {
rm -f "$temp_file"
rm -rf "$temp_dir"
}
# 7. Error handling
command || { echo "Command failed"; exit 1; }
# 8. Use readonly for constants
readonly MAX_RETRIES=5
# 9. Use arrays for lists
files=("$file1" "$file2" "$file3")
# 10. Document complex code
# This function calculates fibonacci numbers recursively
# Args: n - position in fibonacci sequence
# Returns: fibonacci number
fibonacci() {
# implementation
}

Performance Tips

#!/bin/bash
# 1. Avoid unnecessary subshells
# Slow
cat file.txt | while read line; do
echo "$line"
done
# Fast
while read line; do
echo "$line"
done < file.txt
# 2. Use bash builtins instead of external commands
# Slow
count=$(echo "$string" | wc -c)
# Fast
count=${#string}
# 3. Avoid loops when possible
# Slow
for file in *; do
mv "$file" "${file}.bak"
done
# Fast
rename 's/$/.bak/' *
# 4. Use parameter expansion instead of sed/awk
# Slow
new_string=$(echo "$string" | sed 's/foo/bar/g')
# Fast
new_string="${string//foo/bar}"
# 5. Group commands
# Slow
command1
command2
command3 > output.txt
# Fast
{
command1
command2
command3
} > output.txt
# 6. Use process substitution to avoid pipes
# Slow
command1 | command2 | command3
# Fast (if possible)
command3 < <(command2 < <(command1))
# 7. Limit external command calls
# Slow
for i in {1..100}; do
echo "$i"
done | grep "5"
# Fast
for i in {1..100}; do
[[ "$i" == *5* ]] && echo "$i"
done
# 8. Use exec for redirections
exec 3> output.txt
echo "to file" >&3
exec 3>&-

Conclusion

Bash scripting is a powerful skill for system administration, automation, and development:

Key Takeaways

  1. Start with shebang: Always start with #!/bin/bash
  2. Use functions: Organize code into reusable functions
  3. Error handling: Always check for errors and handle them gracefully
  4. Quote variables: Prevent word splitting and pathname expansion
  5. Use arrays: For lists of items
  6. Validate input: Check arguments and user input
  7. Document code: Comments and usage information
  8. Test thoroughly: Test edge cases and error conditions

Common Patterns

PatternUse Case
set -euo pipefailStrict mode for robustness
[[ -f "$file" ]]File existence check
while read lineReading files line by line
for i in {1..10}Numeric loops
case $var inMultiple condition checks
trap cleanup EXITAutomatic cleanup
"${var:-default}"Default values
"${var##*/}"Basename extraction

Best Practices Checklist

  • [ ] Use #!/usr/bin/env bash for portability
  • [ ] Set strict mode: set -euo pipefail
  • [ ] Quote all variable expansions
  • [ ] Use local variables in functions
  • [ ] Check return codes of commands
  • [ ] Provide usage information
  • [ ] Validate input parameters
  • [ ] Use meaningful variable names
  • [ ] Add comments for complex logic
  • [ ] Handle signals and cleanup
  • [ ] Test with shellcheck for errors
  • [ ] Use version control for scripts

Bash scripting is an essential skill that grows with practice. Start with simple scripts and gradually incorporate more advanced features as you become comfortable with the basics.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper