You’ve memorized uname, hostname, and df. You can check disk space in your sleep. But can you build something that pulls all this together into a professional report?

That’s the gap between knowing commands and being dangerous with them.

Today, we’re building a System Information Reporter - a script that generates comprehensive reports about any system, in both terminal and HTML formats. The kind of tool sysadmins actually use.

By the end, you’ll have a cross-platform utility that works on Linux and macOS. Let’s build it.

The Problem: Scattered Information

When you need to know about a system, you run a dozen commands:

uname -a          # OS info
hostname          # System name
free -h           # Memory
df -h             # Disk
ip addr           # Network
uptime            # How long running
ps aux            # Processes

That’s fine for quick checks. But what about:

  • Generating a report for documentation?
  • Comparing systems in your infrastructure?
  • Creating a dashboard-ready HTML output?
  • Running the same script on both Linux and macOS?

Our reporter will handle all of that.

The Toolkit: Commands Grouped by Purpose

Group 1: System Identity

CommandPurposeCross-Platform
hostnameGet system hostnameYes
uname -rKernel versionYes
uname -mArchitecture (x86_64, arm64)Yes
/etc/os-releaseLinux distribution infoLinux only
sw_versmacOS versionmacOS only

Group 2: Hardware Information

CommandPurposeCross-Platform
/proc/cpuinfoCPU detailsLinux only
sysctl machdep.cpuCPU detailsmacOS only
free -hMemory usageLinux only
vm_statMemory usagemacOS only
df -hDisk usageYes

Group 3: Performance Metrics

CommandPurposeCross-Platform
uptimeSystem uptime and loadYes
ps auxRunning processesYes
whoLogged-in usersYes

Group 4: Network

CommandPurposeCross-Platform
ip addrNetwork interfacesLinux only
ifconfigNetwork interfacesmacOS (and older Linux)
curl ifconfig.mePublic IP addressYes

The Walkthrough: Building Step by Step

Step 1: Detect the Operating System

First, we need to know what we’re working with:

get_os_info() {
    if [[ -f /etc/os-release ]]; then
        # Linux: parse the os-release file
        source /etc/os-release
        echo "$PRETTY_NAME"
    elif command -v sw_vers &>/dev/null; then
        # macOS: use sw_vers
        echo "macOS $(sw_vers -productVersion)"
    else
        # Fallback
        uname -s
    fi
}

# Test it
get_os_info
# Output: Ubuntu 22.04.3 LTS  OR  macOS 14.2

What’s happening here?

  • [[ -f file ]] checks if a file exists
  • source loads variables from a file
  • command -v checks if a command exists (returns 0 if found)
  • &>/dev/null silences both stdout and stderr

Step 2: Get CPU Information (Cross-Platform)

get_cpu_info() {
    if [[ -f /proc/cpuinfo ]]; then
        # Linux: parse /proc/cpuinfo
        grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2 | xargs
    elif command -v sysctl &>/dev/null; then
        # macOS: use sysctl
        sysctl -n machdep.cpu.brand_string 2>/dev/null || echo "Unknown"
    else
        echo "Unknown"
    fi
}

get_cpu_cores() {
    if command -v nproc &>/dev/null; then
        nproc
    elif command -v sysctl &>/dev/null; then
        sysctl -n hw.ncpu 2>/dev/null || echo "Unknown"
    else
        grep -c "processor" /proc/cpuinfo 2>/dev/null || echo "Unknown"
    fi
}

# Test
get_cpu_info   # Intel(R) Core(TM) i7-9700K @ 3.60GHz
get_cpu_cores  # 8

Pattern recognized? We always:

  1. Check for the Linux way first
  2. Fall back to macOS way
  3. Have a final fallback for edge cases

Step 3: Memory Information

get_memory_info() {
    if command -v free &>/dev/null; then
        # Linux: use free command
        free -h | awk 'NR==2 {printf "Total: %s, Used: %s, Free: %s", $2, $3, $4}'
    elif command -v vm_stat &>/dev/null; then
        # macOS: calculate from sysctl
        local total=$(sysctl -n hw.memsize 2>/dev/null)
        if [[ -n "$total" ]]; then
            printf "Total: %.1f GB" "$(echo "scale=1; $total/1024/1024/1024" | bc)"
        else
            echo "Unknown"
        fi
    else
        echo "Unknown"
    fi
}

New tools:

  • awk 'NR==2' - Select the second row
  • bc - Arbitrary precision calculator for floating point math
  • scale=1 - One decimal place

Step 4: Disk and Network

get_disk_info() {
    df -h / 2>/dev/null | awk 'NR==2 {printf "Total: %s, Used: %s (%s), Available: %s", $2, $3, $5, $4}'
}

get_network_interfaces() {
    if command -v ip &>/dev/null; then
        # Linux: modern ip command
        ip -4 addr show 2>/dev/null | grep inet | grep -v "127.0.0.1" | awk '{print $NF": "$2}' | head -5
    elif command -v ifconfig &>/dev/null; then
        # macOS/older Linux: ifconfig
        ifconfig 2>/dev/null | grep "inet " | grep -v "127.0.0.1" | awk '{print $2}' | head -5
    else
        echo "Unknown"
    fi
}

get_public_ip() {
    curl -s --connect-timeout 5 https://api.ipify.org 2>/dev/null || \
    curl -s --connect-timeout 5 https://ifconfig.me 2>/dev/null || \
    echo "Unable to fetch"
}

Pro tips:

  • --connect-timeout 5 prevents hanging on network issues
  • Using || for fallback APIs
  • grep -v "127.0.0.1" excludes localhost

Step 5: Format the Terminal Output

Now let’s make it pretty:

# Colors
CYAN='\033[0;36m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
NC='\033[0m'

print_terminal_report() {
    local hostname=$(hostname)
    local os_info=$(get_os_info)
    local kernel=$(uname -r)
    local arch=$(uname -m)
    local cpu_info=$(get_cpu_info)
    local cpu_cores=$(get_cpu_cores)
    local memory_info=$(get_memory_info)
    local disk_info=$(get_disk_info)
    local uptime_info=$(uptime | awk -F'up ' '{print $2}' | awk -F',' '{print $1}')
    local load_avg=$(uptime | awk -F'load average:' '{print $2}')
    local public_ip=$(get_public_ip)

    echo ""
    echo -e "${CYAN}╔══════════════════════════════════════════════════════════════════╗${NC}"
    echo -e "${CYAN}${NC}                    SYSTEM INFORMATION REPORT                      ${CYAN}${NC}"
    echo -e "${CYAN}╠══════════════════════════════════════════════════════════════════╣${NC}"
    echo -e "${CYAN}${NC}  Generated: $(date '+%Y-%m-%d %H:%M:%S')                                  ${CYAN}${NC}"
    echo -e "${CYAN}╚══════════════════════════════════════════════════════════════════╝${NC}"
    echo ""

    echo -e "${YELLOW}─── System ───${NC}"
    printf "  %-20s : %s\n" "Hostname" "$hostname"
    printf "  %-20s : %s\n" "OS" "$os_info"
    printf "  %-20s : %s\n" "Kernel" "$kernel"
    printf "  %-20s : %s\n" "Architecture" "$arch"
    echo ""

    echo -e "${YELLOW}─── Hardware ───${NC}"
    printf "  %-20s : %s\n" "CPU" "$cpu_info"
    printf "  %-20s : %s\n" "Cores" "$cpu_cores"
    printf "  %-20s : %s\n" "Memory" "$memory_info"
    printf "  %-20s : %s\n" "Disk (/)" "$disk_info"
    echo ""

    echo -e "${YELLOW}─── Performance ───${NC}"
    printf "  %-20s : %s\n" "Uptime" "$uptime_info"
    printf "  %-20s : %s\n" "Load Average" "$load_avg"
    echo ""

    echo -e "${YELLOW}─── Network ───${NC}"
    printf "  %-20s : %s\n" "Public IP" "$public_ip"
}

Formatting tricks:

  • printf "%-20s" - Left-align in 20 characters
  • Unicode box-drawing characters for professional look
  • ANSI color codes for visual hierarchy

Step 6: Generate HTML Report

The real power move - creating a styled HTML report:

generate_html_report() {
    local hostname=$(hostname)
    local os_info=$(get_os_info)
    # ... gather all data ...

    cat > "system_report.html" << 'HTMLEOF'
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>System Report</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, sans-serif;
            background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
            color: #eee;
            padding: 20px;
        }
        .card {
            background: rgba(255,255,255,0.05);
            border-radius: 15px;
            padding: 25px;
            margin: 20px 0;
        }
        .info-row {
            display: flex;
            justify-content: space-between;
            padding: 10px 0;
            border-bottom: 1px solid rgba(255,255,255,0.1);
        }
    </style>
</head>
<body>
    <h1>System Information Report</h1>
    <div class="card">
        <h2>System</h2>
        <div class="info-row">
            <span>Hostname</span>
            <span>HOSTNAME_PLACEHOLDER</span>
        </div>
        <!-- More rows... -->
    </div>
</body>
</html>
HTMLEOF

    # Replace placeholders with actual values
    sed -i "s/HOSTNAME_PLACEHOLDER/$hostname/g" system_report.html
}

HERE document tip: Use << 'EOF' (quoted) to prevent variable expansion inside, then use sed to replace placeholders. This keeps your HTML clean and readable.

The Complete Script Structure

#!/bin/bash
set -euo pipefail

REPORT_FILE="system_report_$(date +%Y%m%d_%H%M%S).html"
OUTPUT_MODE="both"  # terminal, html, both

# All the helper functions from above...

main() {
    while [[ $# -gt 0 ]]; do
        case $1 in
            -t|--terminal) OUTPUT_MODE="terminal"; shift ;;
            -w|--html)     OUTPUT_MODE="html"; shift ;;
            -o|--output)   REPORT_FILE="$2"; shift 2 ;;
            -h|--help)     usage; exit 0 ;;
            *)             echo "Unknown: $1"; exit 1 ;;
        esac
    done

    case "$OUTPUT_MODE" in
        terminal) print_terminal_report ;;
        html)     generate_html_report ;;
        both)     print_terminal_report; generate_html_report ;;
    esac
}

main "$@"

Usage:

./sysinfo.sh              # Both terminal and HTML
./sysinfo.sh -t           # Terminal only
./sysinfo.sh -w           # HTML only
./sysinfo.sh -o custom.html  # Custom output file

What You Just Learned

You built a professional-grade system reporter. Along the way, you mastered:

  • Cross-platform scripting - Detecting and adapting to Linux vs macOS
  • System commands - uname, hostname, free, df, ip, ps
  • Data extraction - grep, awk, cut, sed pipeline mastery
  • Output formatting - Colors, printf alignment, box drawing
  • HTML generation - HERE documents and template substitution
  • Argument parsing - getopts alternative with case statements

Your Challenge

The script works. Now make it yours:

  1. Add CPU usage - Parse /proc/stat or use top -bn1
  2. Add temperature sensors - Check /sys/class/thermal/ on Linux
  3. Add JSON output - Use jq or build JSON strings manually
  4. Add email option - Pipe HTML to mail or sendmail
  5. Add comparison mode - Store previous reports and diff them

The system doesn’t care what scripts you write. But the humans who maintain it will thank you for tools like this.


Next in the series: Building a User Management Tool in Bash