Excalibur's Sheath

Scripting: Modularity, Parsing, and Reporting

Sep 21, 2025 By: Jordan McGilvrayhomelab,scripting,bash,perl,python,automation,sysadmin,linux,monitoring,logs,dry-run,error-handling

Homelab: Security, Automation, and Monitoring: Part 3 of 6

In our last guide, we built a foundation in scripting and regular expressions. We explored how Bash, Python, and Perl each approach text processing and automation, and we walked through practical examples of turning one-liners into basic scripts.

That first step was about learning how to bend the shell and your chosen language to your will. But anyone who has spent time with homelab scripting knows that quick fixes don’t always age well. What starts as a clever shortcut can turn into a messy problem if it isn’t structured with care.

This week we’ll go beyond quick hacks and focus on writing scripts you can rely on over time. We’ll explore how to make your code modular with functions, flexible with arguments, and safer with error handling. These practices don’t just prevent failure—they save time when you need to extend or troubleshoot your work later.

We’ll also cover arrays, lists, and dictionaries, showing how they let you scale scripts from one host or service to many. To bring it all together, we’ll build a small but complete monitoring script as a practical case study. By the end, you’ll be better equipped to write scripts that are more than disposable helpers—they’ll be trusted tools in your homelab. Along the way, we’ll see how Bash, Perl, and Python each shine at different parts of the workflow, helping you pick the right tool for each task.

Permissions and Cron: The Foundation for All Scripts

Before writing a single line of code, it’s critical to set up a secure and reliable environment. Scripts that write logs, parse files, or modify system data must be protected from accidental damage and unauthorized access.

File Permissions

  • Scripts should be executable but not writable by unauthorized users:
chmod 755 /path/to/script.sh
  • Log files containing sensitive information should be restricted:
chmod 700 /var/log/sys_monitor.log

Tip: Use ls -l to verify permissions after changes.

  • Reason: Prevents accidental or malicious modification and ensures automation runs smoothly under cron.

Shebang and Bash Safety

Start all Bash scripts with a proper shebang and strict mode:

#!/bin/bash
set -euo pipefail

Explanation:

  • set -eExit immediately if a command fails.
  • set -uTreat unset variables as errors.
  • set -o pipefailFail pipelines if any command fails.

Callout: This ensures your scripts fail fast, preventing silent errors that could propagate bad data.

Cron Scheduling

Cron automates script execution at fixed intervals. Key points:

  • User crontab: crontab -e for the current user.
  • System crontab: /etc/crontab for root or system-wide tasks.
  • Environment: Cron has a minimal environment; always specify full paths.
  • Log rotation: Ensure log files are writable and rotated securely.

Example Cron Job:

0 0 * * * /home/user/scripts/sys_monitor.sh >> /var/log/sys_monitor.log 2>&1
  • >> appends standard output to the log.
  • 2>&1 redirects standard error to the same log.

Pro Tip: Always test cron jobs with --dry-run in your script before scheduling them.

Bash Scripting: The First Step in Automation

Bash is excellent for system-level tasks, file operations, and tool integration.

Example: Monitoring CPU Usage

#!/bin/bash
set -euo pipefail
HUMAN_LOG="/var/log/sys_monitor.log"
log_error() {
    echo "ERROR [$(date '+%F %T')]: $*" >> "$HUMAN_LOG"
}
CPU_FILE="/proc/stat"
# Dry-run support
DRY_RUN=false
if [[ "${1:-}" == "--dry-run" ]]; then
    DRY_RUN=true
    echo "Running in dry-run mode."
fi
# Function to log CPU usage
log_cpu() {
    local cpu_idle=$(grep '^cpu ' $CPU_FILE | awk '{print $5}')
    local cpu_total=$(grep '^cpu ' $CPU_FILE | awk '{sum=0; for(i=2;i<=NF;i++) sum+=$i; print sum}')
    if $DRY_RUN; then
        echo "[DRY-RUN] CPU Idle: $cpu_idle, Total: $cpu_total"
    else
        echo "$(date '+%F %T'),$cpu_idle,$cpu_total" >> /var/log/cpu_metrics.csv || log_error "Failed writing CPU metrics"
    fi
}
# Execute monitoring
log_cpu

Key Points:

  • Supports dry-run testing (--dry-run) to prevent accidental file writes.
  • Logs errors for failed operations.
  • Uses clear comments for every calculation step.

Callout: Dry-run mode is your first line of defense against accidental data changes.

Perl Scripting: Parsing Logs with Multiple Entries

Perl excels at text processing, regular expressions, and CSV manipulation.

Example: Extract New Entries from a Log File

#!/usr/bin/env perl
use strict;
use warnings;
use Time::Piece;
my $log_file = '/var/log/sys_monitor.log';
my $csv_file = '/var/log/sys_monitor.csv';
my $last_file = '/var/log/.last_processed';
# Load last processed timestamp
my $last_time = 0;
if (-e $last_file) {
    open my $lf, '<', $last_file or die "Cannot open $last_file: $!";
    $last_time = <$lf>;
    close $lf;
}
open my $log, '<', $log_file or die "Cannot open $log_file: $!";
open my $csv, '>>', $csv_file or die "Cannot write to $csv_file: $!";
while (<$log>) {
    if (/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),(\d+),(\d+)/) {
        my ($timestamp, $idle, $total) = ($1, $2, $3);
        my $epoch = Time::Piece->strptime($timestamp, '%Y-%m-%d %H:%M:%S')->epoch;
        next if $epoch <= $last_time;
        print $csv "$timestamp,$idle,$total\n";
    }
}
close $csv;
close $log;
# Update last processed timestamp
open my $lf, '>', $last_file or die "Cannot update $last_file: $!";
print $lf time();
close $lf;

Highlights:

  • Processes multiple log entries in one run.
  • Tracks last processed entry to avoid duplicates.
  • Includes error handling for file operations.
  • Clear comments explain parsing and timestamp logic.
  • Pro Tip: Add dry-run by replacing print $csv with warn "[DRY-RUN] $timestamp,$idle,$total\n";.

Python Scripting: Modern Automation and Safe Parsing

Python provides robust file handling, exception management, and CSV processing.

Example: Reading and Appending Metrics

#!/usr/bin/env python3
import csv
import sys
from datetime import datetime
CSV_FILE = '/var/log/cpu_metrics.csv'
LOG_FILE = '/var/log/monitor-errors.log'
DRY_RUN = '--dry-run' in sys.argv
def log_error(message):
    with open(LOG_FILE, 'a') as f:
        f.write(f"{datetime.now()}: {message}\n")
try:
    with open(CSV_FILE, newline='') as csvfile:
        reader = csv.reader(csvfile)
        data = list(reader)
except FileNotFoundError:
    log_error(f"{CSV_FILE} not found.")
    data = []
# Example: append new metric
new_metric = [datetime.now().strftime("%Y-%m-%d %H:%M:%S"), 42, 100]
if DRY_RUN:
    print("[DRY-RUN] Would append:", new_metric)
else:
    try:
        with open(CSV_FILE, 'a', newline='') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(new_metric)
    except Exception as e:
        log_error(f"Failed writing CSV: {e}")

Key Points:

  • Uses try/except to log errors safely.
  • Supports dry-run testing before modifying live CSV.
  • Encourages testing with sample data first.

Callout: Testing scripts incrementally builds confidence and reduces risk.

Testing and Dry-Run Philosophy

Across all three languages:

  • Dry-run simulates script actions before affecting files.
  • Error logging captures issues without stopping the workflow.
  • Start small:
    • Test with 5–10 lines of log data.
    • Increase complexity incrementally.

Example Approach:

  1. Copy live logs to a test file.
  2. Run the script with --dry-run.
  3. Check outputs and error logs.
  4. Remove --dry-run to commit changes.

Pro Tip: Always review dry-run output carefully before writing to production logs.

Recap: Unified Best Practices

Topic Best Practice
Permissions Scripts 755, logs 700; minimize user access.
Shebangs Bash: #!/bin/bash + set -euo pipefail; Perl/Python: #!/usr/bin/env ...
Error Handling Capture and log errors; fail safely.
Dry-Run Testing Always provide a test mode to preview changes.
Cron Scheduling Specify full paths; log stdout & stderr; respect environment differences.
Multi-Entry Logs Track last processed entry to avoid duplicates (Perl/Python).
Comments Explain why and how, not just what.

Conclusion

Automating Linux tasks with Bash, Perl, and Python lets you move from repetitive manual work to efficient, repeatable processes. Each language has unique strengths: Bash excels at system-level operations and quick file handling, Perl shines in text parsing and log analysis, and Python provides a modern, readable approach to structured data and reporting. Combining these tools thoughtfully allows coverage of a wide range of administrative tasks with reliability and clarity.

A strong foundation in permissions, cron, and safe scripting practices is essential. Protecting scripts and logs prevents accidental or malicious changes, while using shebangs and strict modes in Bash ensures predictable behavior. Logging and error handling across all three languages provides a safety net, helping identify problems early without disrupting workflows.

Testing and dry-run strategies are critical to build confidence. Simulating operations before committing changes reduces risk, encourages experimentation, and ensures that automation is both safe and effective. Clear comments and structured code make scripts easier to maintain and adapt.

Finally, moving from beginner to intermediate scripting is a hands-on journey. Practice with small data sets, experiment with metrics, and leverage the strengths of Bash, Perl, and Python. You’ll not only develop technical skills but also cultivate a mindset for reliable, secure, and maintainable automation—a foundation for more advanced system administration projects.

More from the "Homelab: Security, Automation, and Monitoring" Series: