How to Create a Bash File Diff Tool

How to Create a Bash File Diff Tool

How to Create a Bash File Diff Tool

I use diff constantly, but plain diff output has always felt a little unfriendly — walls of < and > symbols that take a second too long to parse, especially when comparing entire directories instead of single files. So I built a Bash wrapper around diff that adds color, summaries, and directory-level comparison in one easy command. This article walks through the whole build.

Why Wrap diff Instead of Using It Raw

diff is powerful but bare-bones by default. I wanted a tool that would:

Prerequisites

sudo apt install diffutils colordiff

colordiff is a thin wrapper that adds syntax coloring to diff output — entirely optional, but it makes a noticeable difference in readability.

Step 1: Basic File Comparison

#!/usr/bin/env bash
set -euo pipefail

FILE_A="${1:-}"
FILE_B="${2:-}"

if [[ -z "$FILE_A" || -z "$FILE_B" ]]; then
    echo "Usage: $0 <file-or-dir-A> <file-or-dir-B>"
    exit 1
fi

if [[ ! -e "$FILE_A" || ! -e "$FILE_B" ]]; then
    echo "Error: one or both paths do not exist."
    exit 1
fi

diff -u "$FILE_A" "$FILE_B"

Explaining the Core Command

Step 2: Adding Color and a Change Summary

if command -v colordiff &>/dev/null; then
    DIFF_CMD="colordiff"
else
    DIFF_CMD="diff"
fi

ADDED=$(diff "$FILE_A" "$FILE_B" | grep -c '^>' || true)
REMOVED=$(diff "$FILE_A" "$FILE_B" | grep -c '^<' || true)

echo "Comparing: $FILE_A vs $FILE_B"
echo "Lines added:   $ADDED"
echo "Lines removed: $REMOVED"
echo "-------------------------------"

$DIFF_CMD -u "$FILE_A" "$FILE_B" || true

What’s Going On Here

Step 3: Extending to Directory Comparison

This is where the tool becomes genuinely more convenient than raw diff. Let’s detect directories and handle them recursively:

if [[ -d "$FILE_A" && -d "$FILE_B" ]]; then
    echo "Comparing directories recursively..."
    echo "-------------------------------------"

    diff -rq "$FILE_A" "$FILE_B" || true

    echo ""
    echo "Summary:"
    ONLY_A=$(diff -rq "$FILE_A" "$FILE_B" | grep -c "^Only in $FILE_A" || true)
    ONLY_B=$(diff -rq "$FILE_A" "$FILE_B" | grep -c "^Only in $FILE_B" || true)
    DIFFER=$(diff -rq "$FILE_A" "$FILE_B" | grep -c "^Files .* differ" || true)

    echo "Files only in $FILE_A: $ONLY_A"
    echo "Files only in $FILE_B: $ONLY_B"
    echo "Files that differ:     $DIFFER"
else
    # single-file comparison logic from Step 2 goes here
    :
fi

Understanding Directory Diff

Step 4: Adding a Detailed Mode for Directories

Sometimes the quiet summary isn’t enough — you want to see the actual line-by-line diff for each changed file too:

DETAILED="${3:-}"

if [[ "$DETAILED" == "--detailed" ]]; then
    echo ""
    echo "Detailed differences:"
    echo "----------------------"
    diff -rq "$FILE_A" "$FILE_B" | grep "^Files .* differ" | while IFS= read -r line; do
        FA=$(echo "$line" | awk '{print $2}')
        FB=$(echo "$line" | awk '{print $4}')
        echo ""
        echo "### $FA vs $FB ###"
        $DIFF_CMD -u "$FA" "$FB" || true
    done
fi

Here we parse each “Files X and Y differ” line with awk, pulling out the second and fourth whitespace-separated fields (the two file paths), then run a full unified diff on just that pair — giving you drill-down detail only for files that actually changed, rather than dumping every file’s diff regardless of relevance.

Full Combined Script

#!/usr/bin/env bash
set -euo pipefail

FILE_A="${1:-}"
FILE_B="${2:-}"
MODE="${3:-}"

if [[ -z "$FILE_A" || -z "$FILE_B" ]]; then
    echo "Usage: $0 <path-A> <path-B> [--detailed]"
    exit 1
fi

if [[ ! -e "$FILE_A" || ! -e "$FILE_B" ]]; then
    echo "Error: one or both paths do not exist."
    exit 1
fi

if command -v colordiff &>/dev/null; then
    DIFF_CMD="colordiff"
else
    DIFF_CMD="diff"
fi

if [[ -d "$FILE_A" && -d "$FILE_B" ]]; then
    echo "Comparing directories: $FILE_A vs $FILE_B"
    echo "-------------------------------------------"
    diff -rq "$FILE_A" "$FILE_B" || true

    echo ""
    echo "Summary:"
    echo "Files only in $FILE_A: $(diff -rq "$FILE_A" "$FILE_B" | grep -c "^Only in $FILE_A" || true)"
    echo "Files only in $FILE_B: $(diff -rq "$FILE_A" "$FILE_B" | grep -c "^Only in $FILE_B" || true)"
    echo "Files that differ:     $(diff -rq "$FILE_A" "$FILE_B" | grep -c "^Files .* differ" || true)"

    if [[ "$MODE" == "--detailed" ]]; then
        diff -rq "$FILE_A" "$FILE_B" | grep "^Files .* differ" | while IFS= read -r line; do
            FA=$(echo "$line" | awk '{print $2}')
            FB=$(echo "$line" | awk '{print $4}')
            echo ""
            echo "### $FA vs $FB ###"
            $DIFF_CMD -u "$FA" "$FB" || true
        done
    fi
else
    ADDED=$(diff "$FILE_A" "$FILE_B" | grep -c '^>' || true)
    REMOVED=$(diff "$FILE_A" "$FILE_B" | grep -c '^<' || true)
    echo "Comparing: $FILE_A vs $FILE_B"
    echo "Lines added:   $ADDED"
    echo "Lines removed: $REMOVED"
    echo "-------------------------------"
    $DIFF_CMD -u "$FILE_A" "$FILE_B" || true
fi

Real-World Use Cases

Automation Ideas

Run a nightly config-drift check between a known-good reference config and the live one, alerting if they diverge:

0 6 * * * /usr/local/bin/fdiff.sh /etc/nginx /backups/reference-nginx-config | grep -q "differ" && echo "Config drift detected!" | mail -s "Nginx Config Drift" you@example.com

Security Considerations

Optimization Tips

Troubleshooting

Common Mistakes to Avoid

Frequently Asked Questions

How is this different from git diff? git diff is scoped to a Git repository and its tracked history; this tool works on any two arbitrary files or directories regardless of version control.

Can I generate a patch file from this? Yes — diff -u fileA fileB > changes.patch produces a standard unified diff patch file that can later be applied with patch -p1 < changes.patch.

Does this handle very large files well? For files in the multi-gigabyte range, diff can be slow and memory-intensive; consider cmp -l for a byte-level comparison or specialized big-file diff tools if this becomes a bottleneck.

Summary

We built a Bash diff tool that wraps diff with color output, quick line-change summaries, automatic directory-vs-file detection, and an optional detailed drill-down mode for directory comparisons. The main lesson here is understanding diff‘s exit code behavior and output format well enough to parse it reliably — once you have that, building a genuinely more usable interface around it is mostly a matter of good defaults and clear formatting.

References

Exit mobile version