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, and while loops
  • 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, and read

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
Output redirection patterns

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
Reading files with while loops and arrays

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"
Logging function with file append

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"
Real-world log analysis pipeline

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
Heredoc patterns: variable expansion vs literal

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
Parsing CSV data with cut, awk, and read

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"
Complete log analyzer script

5. Common Errors & Solutions

ErrorWhy It HappensSolution
command not found in while read loopPiping while read creates a subshell; variables exit the loopUse while read ...; do ...; done < file (redirect from file, not pipe)
Empty variable after reading filecat /file | while read loses scopeUse input redirection or mapfile instead of pipe
File content overwritten instead of appendedUsed > instead of >>Always check: append with >>, overwrite with >
IFS= being resetForgot to set IFS= read -r in the loop conditionAlways prefix with IFS= and use -r to preserve backslashes
CSV with quoted fields breaks parsingcut and read don't understand CSV quotingUse 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 &> or 2>&1
  • Use tee to write output to file AND see it on screen
  • Read files with while IFS= read -r line; do ...; done < file
  • Use mapfile -t arr < file to read into an array
  • Pipeline: cat | filter | sort | transform | output
  • Heredocs: << EOF expands 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 tee and redirects for transparent debugging

7. Practice Exercise

Challenge: Build a server health report generator

Write a Bash script called health_report.sh that:

  1. Reads a list of servers from a file called servers.txt (one hostname per line, lines starting with # are comments)
  2. For each server, pings once (ping -c 1) and captures the result (up or down)
  3. Checks disk usage via df -h and reports if any partition is over 80%
  4. Writes a timestamped HTML report to /tmp/health_YYYYMMDD_HHMMSS.html
  5. 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.

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen July 21, 2026

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment