Advanced Bash Scripting for Sysadmins

Why Master Bash?

Bash scripting is essential for:

Shebang and Basics

#!/bin/bash
# Always include shebang as first line
set -euo pipefail  # Exit on error, undefined vars, pipe failures

Script Permissions

chmod +x script.sh
./script.sh

Variables and Parameters

#!/bin/bash

# Variable assignment (no spaces!)
NAME="John"
AGE=30

# Using variables
echo "Hello, $NAME"
echo "Age: ${AGE}"

# Default values
${VAR:-default}      # Use default if VAR unset
${VAR:=default}      # Set and use default
${VAR:?Error}        # Exit with error if unset

# String operations
${VAR#prefix}        # Remove prefix
${VAR%suffix}        # Remove suffix
${VAR/old/new}       # Replace first occurrence
${VAR//old/new}      # Replace all occurrences
${VAR:0:5}          # Substring (first 5 chars)

# Script arguments
$0          # Script name
$1, $2, ... # Positional arguments
$@          # All arguments (array)
$#          # Number of arguments

Functions

#!/bin/bash

# Function definition
backup_files() {
    local source=$1
    local dest=$2
    local timestamp=$(date +%Y%m%d_%H%M%S)
    
    echo "Backing up $source to $dest"
    tar -czf "$dest/backup_$timestamp.tar.gz" "$source"
    return 0
}

# Function call
backup_files "/home/user/documents" "/backups"

# Capture return value
if backup_files "/var/www" "/backups"; then
    echo "Backup successful"
else
    echo "Backup failed"
fi

Conditionals

#!/bin/bash

# Test conditions
if [[ -f /etc/passwd ]]; then
    echo "File exists"
fi

# Multiple conditions
if [[ $AGE -gt 18 ]] && [[ $AGE -lt 65 ]]; then
    echo "Working age"
elif [[ $AGE -ge 65 ]]; then
    echo "Retirement age"
else
    echo "Young age"
fi

# String comparison
if [[ "$OS" == "Linux" ]]; then
    echo "Running on Linux"
fi

# File tests
[[ -e $file ]]    # Exists
[[ -f $file ]]    # Regular file
[[ -d $file ]]    # Directory
[[ -r $file ]]    # Readable
[[ -w $file ]]    # Writable
[[ -x $file ]]    # Executable
[[ -s $file ]]    # Non-empty

# Arithmetic
[[ $x -eq $y ]]   # Equal
[[ $x -ne $y ]]   # Not equal
[[ $x -lt $y ]]   # Less than
[[ $x -gt $y ]]   # Greater than

Loops

#!/bin/bash

# For loop
for i in {1..10}; do
    echo "Iteration $i"
done

# Array iteration
servers=("web1" "web2" "web3")
for server in "${servers[@]}"; do
    echo "Pinging $server"
    ping -c 1 "$server"
done

# While loop
counter=0
while [[ $counter -lt 5 ]]; do
    echo "Count: $counter"
    ((counter++))
done

# Until loop
until [[ $counter -eq 0 ]]; do
    echo "Countdown: $counter"
    ((counter--))
done

# Break and continue
for i in {1..10}; do
    if [[ $i -eq 5 ]]; then
        continue  # Skip this iteration
    fi
    if [[ $i -eq 8 ]]; then
        break     # Exit loop
    fi
    echo $i
done

Arrays

#!/bin/bash

# Array declaration
arr=(1 2 3 4 5)
arr=([0]=10 [1]=20 [2]=30)

# Access elements
echo ${arr[0]}         # First element
echo ${arr[@]}         # All elements
echo ${#arr[@]}        # Array length

# Add element
arr+=(6)

# Iterate
for item in "${arr[@]}"; do
    echo "$item"
done

# Find index
for i in "${!arr[@]}"; do
    echo "Index $i: ${arr[$i]}"
done

String Processing

#!/bin/bash

# String length
STR="Hello World"
echo ${#STR}

# Substring
echo ${STR:0:5}     # "Hello"
echo ${STR:6}       # "World"

# Pattern matching
if [[ $STR =~ ^Hello ]]; then
    echo "Starts with Hello"
fi

# Case conversion
echo ${STR^^}       # Uppercase
echo ${STR,,}       # Lowercase

# String splitting
IFS=',' read -ra PARTS <<< "one,two,three"
for part in "${PARTS[@]}"; do
    echo "$part"
done

Error Handling

#!/bin/bash

set -euo pipefail  # Exit on any error

# Trap errors
trap 'echo "Error on line $LINENO"' ERR
trap 'cleanup' EXIT

cleanup() {
    echo "Cleaning up..."
    rm -f /tmp/tempfile
}

# Command substitution with error checking
output=$(command 2>&1) || {
    echo "Command failed: $output"
    exit 1
}

# Try-catch pattern
run_command() {
    if output=$(command 2>&1); then
        echo "Success: $output"
    else
        echo "Error: $output"
        return 1
    fi
}

Practical Examples

System Monitoring Script

#!/bin/bash

check_disk_space() {
    local threshold=$1
    df -h | awk 'NR>1 {print $5}' | while read usage; do
        usage_percent=${usage%\%}
        if [[ $usage_percent -gt $threshold ]]; then
            echo "WARNING: Disk usage at $usage_percent%"
        fi
    done
}

check_memory() {
    local mem_available=$(free -h | awk '/^Mem:/ {print $7}')
    echo "Available memory: $mem_available"
}

# Run checks
check_disk_space 80
check_memory

Log Rotation Script

#!/bin/bash

rotate_logs() {
    local logdir=$1
    local keep_days=$2
    
    find "$logdir" -name "*.log" -type f | while read logfile; do
        gzip "$logfile"
    done
    
    # Remove old logs
    find "$logdir" -name "*.log.gz" -mtime +$keep_days -delete
}

# Usage
rotate_logs "/var/log/myapp" 30

Backup with Encryption

#!/bin/bash

backup_encrypted() {
    local source=$1
    local dest=$2
    local password=$3
    
    tar -czf - "$source" | \
        openssl enc -aes-256-cbc -salt -pass pass:"$password" \
        -out "$dest/backup_$(date +%s).tar.gz.enc"
}

# Usage (read password securely)
read -sp "Backup password: " PASSWORD
backup_encrypted "/home/user/data" "/backups" "$PASSWORD"

Parallel Processing

#!/bin/bash

process_files() {
    local max_jobs=4
    local job_count=0
    
    for file in *.txt; do
        # Wait if max jobs reached
        while [[ $(jobs -r -p | wc -l) -ge $max_jobs ]]; do
            sleep 1
        done
        
        # Process file in background
        process_file "$file" &
    done
    
    # Wait for all background jobs
    wait
}

Debugging

#!/bin/bash

# Enable debugging
bash -x script.sh

# Or in script
set -x  # Enable debug
set +x  # Disable debug

# Debug specific sections
PS4='${BASH_SOURCE}:${LINENO}: '
set -x
complex_operation
set +x

# Use trap for debugging
trap 'echo "DEBUG: $BASH_COMMAND"' DEBUG

Best Practices

  1. Use shellcheck: Lint your scripts

    apt install shellcheck
    shellcheck script.sh
    
  2. Quote variables: Prevents word splitting

    rm -rf $dir   # DANGEROUS!
    rm -rf "$dir" # Safe
    
  3. Use local variables: In functions

    local var="value"
    
  4. Handle signals: Cleanup on exit

    trap cleanup EXIT INT TERM
    
  5. Document code: Use comments

    # Process files and backup
    for file in "${files[@]}"; do
    
  6. Test thoroughly: Use shellcheck and test cases

    bash -n script.sh  # Syntax check
    

Performance Tips

# Avoid subshells (slow)
cat file | while read line; do  # Subshell!
    process "$line"
done

# Better: redirection
while read line; do
    process "$line"
done < file

# Batch commands
echo {1..1000} | xargs command  # Parallel execution

Conclusion

Bash scripting is powerful when you understand its features. Start simple, test thoroughly, and gradually increase complexity.

Resources