Bash scripts that don't read or write files are limited to running commands in sequence. The real power of shell scripting comes from processing data: reading log files, generating reports, parsing CSV exports, and manipulating configuration files. In this lesson, you'll master the file I/O techniques every DevOps engineer uses daily — from basic redirects to structured data parsing with built-in tools.
1. Learning Objectives
By the end of this lesson, you will be able to:
- Redirect standard input, output, and error streams effectively
- Read file contents in scripts using
cat,read, andwhileloops - Write and append data to files from within scripts
- Build pipelines that chain multiple commands together
- Use heredocs for multi-line input and script generation
- Parse CSV and structured data with
cut,awk, andread
2. Why This Matters
Real-world scenario: Your application generates 100MB of logs daily. Your monitoring script needs to extract error counts, filter by date range, and email a summary — all automatically. The backup script must write timestamps to a log file. The deploy script needs to read a config file and substitute variables. Every automation task in DevOps involves file I/O. Mastering these patterns turns fragile one-off commands into robust, reusable scripts.
3. Core Concepts
Standard Streams: stdin, stdout, stderr
Every Linux process has three standard streams:
- stdin (0) — Input to the program (keyboard by default)
- stdout (1) — Normal output from the program (terminal by default)
- stderr (2) — Error output from the program (terminal by default)
File descriptors (0, 1, 2) let you redirect each stream independently. This is the foundation of all file I/O in Bash.
The Difference Between > and >>
> (single angle bracket) overwrites the target file. >> (double angle bracket) appends to the target file. This distinction is critical — using > when you meant >> can silently destroy data.
Understanding Pipes
A pipe (|) connects the stdout of one command to the stdin of another. Pipes let you chain simple tools into powerful data processing pipelines without intermediate files.
4. Hands-On Practice
4.1 Redirecting Output
# Write output to a file (overwrite)
echo "Backup started: $(date)" > /tmp/backup.log
cat /tmp/backup.log
# Append output to a file
echo "Processing file 1 of 10" >> /tmp/backup.log
echo "Processing file 2 of 10" >> /tmp/backup.log
cat /tmp/backup.log
# Redirect stderr to a file (separate from stdout)
ls /nonexistent 2> /tmp/errors.log
cat /tmp/errors.log
# Redirect both stdout and stderr to the same file
./deploy.sh > /tmp/deploy.log 2>&1
# Modern syntax (preferred in newer scripts)
./deploy.sh &> /tmp/deploy.log
# Send both to a file AND see output on screen
./deploy.sh 2>&1 | tee /tmp/deploy.log
4.2 Reading Files in Scripts
#!/bin/bash
# Method 1: Read entire file into a variable
CONFIG=$(cat /etc/hostname)
echo "Hostname: $CONFIG"
# Method 2: Read line by line with while loop
while IFS= read -r line; do
echo "Line: $line"
done < /etc/passwd
# Method 3: Read specific fields by delimiter
while IFS=: read -r username password uid gid name home shell; do
echo "User $username has UID $uid and home $home"
done < /etc/passwd
# Method 4: Read file into an array
mapfile -t lines < /etc/hosts
echo "Hosts file has ${#lines[@]} lines"
for i in "${!lines[@]}"; do
if [[ ${lines[$i]} != \#* ]]; then
echo "Active entry $((i+1)): ${lines[$i]}"
fi
done
4.3 Writing and Appending from Scripts
#!/bin/bash
LOG_DIR="/var/log/myapp"
LOG_FILE="${LOG_DIR}/pipeline.log"
mkdir -p "$LOG_DIR"
# Function to log with timestamp
log() {
local level="$1"
local message="$2"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message" >> "$LOG_FILE"
}
log "INFO" "Pipeline started"
log "INFO" "Downloading data..."
# Simulate work
sleep 1
log "INFO" "Processing records..."
sleep 1
log "INFO" "Pipeline completed successfully"
# Show the log
cat "$LOG_FILE"
4.4 Building Pipelines
#!/bin/bash
# A pipeline: count unique IPs in an Apache log
# Step 1: Extract IP addresses (first field, space-separated)
# Step 2: Sort them (necessary before uniq)
# Step 3: Count unique occurrences
# Step 4: Sort by most frequent
# Step 5: Show top 10
grep "GET /api" /var/log/apache2/access.log | \
awk '{print $1}' | \
sort | \
uniq -c | \
sort -rn | \
head -10
# Alternative: chain without line continuations
cat /var/log/apache2/access.log | grep "HTTP/1.1"\" 5 | awk '{print $1}' | sort -u | wc -l
echo "Unique clients with 5xx errors above"
4.5 Heredocs for Multi-Line Input
#!/bin/bash
# Basic heredoc: writes to a file
cat > /tmp/deploy_config.ini << EOF
[server]
host=prod-01.example.com
port=443
user=deploy
[database]
host=db.internal
name=app_prod
pool_size=10
EOF
echo "Config file created"
# Heredoc with variable expansion
date_today=$(date +%Y-%m-%d)
cat << EOF
Report for: $date_today
Server: $(hostname)
----
All systems nominal
EOF
# Quoted delimiter prevents variable expansion
# Note: use 'EOF' (single-quoted) to prevent expansion
cat << 'EOF'
Literal text: $HOME is NOT expanded
$(whoami) is NOT executed
`date` is NOT interpreted
EOF
4.6 Parsing CSV and Structured Data
#!/bin/bash
# Sample CSV data (inline for demo)
CSV_DATA='name,role,department,start_date
John Smith,Engineer,DevOps,2024-01-15
Jane Doe,SRE,Infra,2024-03-01
Bob Wilson,Security Lead,Security,2024-06-10
Alice Brown,Engineer,DevOps,2024-09-01'
echo "$CSV_DATA" > /tmp/team.csv
# Parse with cut (good for simple columns)
echo "=== All engineers (cut) ==="
grep "Engineer" /tmp/team.csv | cut -d',' -f1,3
echo ""
echo "=== Full table with awk ==="
awk -F',' 'NR>1 {printf "%-20s %-15s %s\n", $1, $2, $3}' /tmp/team.csv
echo ""
echo "=== Department report with awk ==="
awk -F',' 'NR>1 {depts[$3]++} END {for (d in depts) printf "%s: %d employees\n", d, depts[d]}' /tmp/team.csv
echo ""
echo "=== Parse in pure Bash ==="
while IFS=',' read -r name role dept date; do
[ "$name" = "name" ] && continue # skip header
echo "$name ($role) - $dept since $date"
done < /tmp/team.csv
# Cleanup
rm /tmp/team.csv
4.7 Complete Project: Log Analyzer Script
#!/bin/bash
# log-analyzer.sh — Analyze NGINX access log and generate report
# Usage: ./log-analyzer.sh /var/log/nginx/access.log
LOG_FILE="${1:-/var/log/nginx/access.log}"
REPORT_DIR="/tmp/reports"
REPORT_FILE="${REPORT_DIR}/access_report_$(date +%Y%m%d).txt"
mkdir -p "$REPORT_DIR"
# Check input file
if [ ! -f "$LOG_FILE" ]; then
echo "ERROR: Log file $LOG_FILE not found" >&2
exit 1
fi
# Start the report
{
echo "========================================"
echo "NGINX Access Log Report"
echo "Generated: $(date)"
echo "Source: $LOG_FILE"
echo "========================================"
echo ""
# Top 10 IP addresses
echo "Top 10 IP Addresses:"
awk '{print $1}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
echo ""
# Top 10 requested URLs
echo "Top 10 Requested URLs:"
awk '{print $7}' "$LOG_FILE" | sort | uniq -c | sort -rn | head -10
echo ""
# HTTP status code distribution
echo "HTTP Status Code Distribution:"
awk '{print $9}' "$LOG_FILE" | sort | uniq -c | sort -rn
echo ""
# 404 errors
echo "404 Errors (not found):"
grep ' "404 ' "$LOG_FILE" | awk '{print $7}' | sort | uniq -c | sort -rn | head -5
echo ""
# Unique user agents
echo "Unique User Agents: $(awk -F'"' '{print $6}' "$LOG_FILE" | sort -u | wc -l)"
echo ""
# Total requests
echo "Total Requests: $(wc -l < "$LOG_FILE")"
} > "$REPORT_FILE" 2>&1
echo "Report saved to: $REPORT_FILE"
cat "$REPORT_FILE"
5. Common Errors & Solutions
| Error | Why It Happens | Solution |
|---|---|---|
command not found in while read loop | Piping while read creates a subshell; variables exit the loop | Use while read ...; do ...; done < file (redirect from file, not pipe) |
| Empty variable after reading file | cat /file | while read loses scope | Use input redirection or mapfile instead of pipe |
| File content overwritten instead of appended | Used > instead of >> | Always check: append with >>, overwrite with > |
IFS= being reset | Forgot to set IFS= read -r in the loop condition | Always prefix with IFS= and use -r to preserve backslashes |
| CSV with quoted fields breaks parsing | cut and read don't understand CSV quoting | Use awk with FPAT or a proper CSV tool like csvkit for complex CSV |
6. Summary Checklist
- Use
>to overwrite,>>to append — never confuse them - Redirect stderr with
2>or combine both with&>or2>&1 - Use
teeto write output to file AND see it on screen - Read files with
while IFS= read -r line; do ...; done < file - Use
mapfile -t arr < fileto read into an array - Pipeline:
cat | filter | sort | transform | output - Heredocs:
<< EOFexpands variables,<< 'EOF'is literal - Parse simple CSV with
cut -d','for single fields,awk -F','for complex processing - Always check if files exist before reading them in scripts
- Use
teeand redirects for transparent debugging
7. Practice Exercise
Challenge: Build a server health report generator
Write a Bash script called health_report.sh that:
- Reads a list of servers from a file called
servers.txt(one hostname per line, lines starting with#are comments) - For each server, pings once (
ping -c 1) and captures the result (up or down) - Checks disk usage via
df -hand reports if any partition is over 80% - Writes a timestamped HTML report to
/tmp/health_YYYYMMDD_HHMMSS.html - Logs each run to
/var/log/health_check.log(with date, server count, up/down summary)
Bonus: Email the report using mail if a server is down.
Try it yourself before checking the solution pattern above.
8. Next Steps
You've mastered file I/O, pipelines, and data processing in Bash. The next lesson will cover Bash Functions and Libraries: Building Modular, Reusable Code — taking your scripts from linear commands to well-organized, reusable tool collections with sourceable libraries and argument parsing.
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment