Build a System Information Reporter: Master Bash System Commands
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
| Command | Purpose | Cross-Platform |
|---|---|---|
hostname | Get system hostname | Yes |
uname -r | Kernel version | Yes |
uname -m | Architecture (x86_64, arm64) | Yes |
/etc/os-release | Linux distribution info | Linux only |
sw_vers | macOS version | macOS only |
Group 2: Hardware Information
| Command | Purpose | Cross-Platform |
|---|---|---|
/proc/cpuinfo | CPU details | Linux only |
sysctl machdep.cpu | CPU details | macOS only |
free -h | Memory usage | Linux only |
vm_stat | Memory usage | macOS only |
df -h | Disk usage | Yes |
Group 3: Performance Metrics
| Command | Purpose | Cross-Platform |
|---|---|---|
uptime | System uptime and load | Yes |
ps aux | Running processes | Yes |
who | Logged-in users | Yes |
Group 4: Network
| Command | Purpose | Cross-Platform |
|---|---|---|
ip addr | Network interfaces | Linux only |
ifconfig | Network interfaces | macOS (and older Linux) |
curl ifconfig.me | Public IP address | Yes |
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 existssourceloads variables from a filecommand -vchecks if a command exists (returns 0 if found)&>/dev/nullsilences 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:
- Check for the Linux way first
- Fall back to macOS way
- 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 rowbc- Arbitrary precision calculator for floating point mathscale=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 5prevents 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,sedpipeline mastery - Output formatting - Colors, printf alignment, box drawing
- HTML generation - HERE documents and template substitution
- Argument parsing -
getoptsalternative with case statements
Your Challenge
The script works. Now make it yours:
- Add CPU usage - Parse
/proc/stator usetop -bn1 - Add temperature sensors - Check
/sys/class/thermal/on Linux - Add JSON output - Use
jqor build JSON strings manually - Add email option - Pipe HTML to
mailorsendmail - 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