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:

  • Automatically detect whether I’m comparing two files or two directories
  • Use color to make additions and deletions visually obvious
  • Give me a quick summary count of changed lines before showing full detail
  • Handle directory comparisons recursively with a clean file-by-file breakdown

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

  • diff -u produces unified diff format, the same format you see in Git output and patch files — it shows a few lines of context around each change, prefixed with - for removed lines and + for added lines. This is far more readable than the default diff format, which only shows raw line numbers and change indicators.
  • The existence checks at the top ([[ ! -e "$FILE_A" ... ]]) prevent a confusing raw diff error message and instead give the user a clear, friendly explanation.

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

  • command -v colordiff &>/dev/null checks whether colordiff is installed and falls back gracefully to plain diff if not — this is the same availability-check idiom we’ve used throughout this series.
  • grep -c '^>' counts lines in the raw diff output that start with > (additions in classic diff format), and ^< counts removed lines. || true prevents set -e from exiting the script when grep finds zero matches (which grep treats as a “failure” exit code, even though finding nothing is a perfectly valid outcome here).
  • We deliberately run diff twice — once without -u for counting, once with -u for the readable display — because unified format is harder to reliably grep-count without over- or under-counting context lines.
  • The final $DIFF_CMD -u "$FILE_A" "$FILE_B" || true also needs || true because diff exits with status 1 (not 0) whenever it finds any differences — which is normal, expected behavior, not an actual error, but set -e doesn’t know that distinction.

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

  • diff -rq combines -r (recurse into subdirectories) with -q (quiet — report only that files differ, not the full line-by-line detail), which gives a clean, scannable summary perfect for large directory trees.
  • diff -rq produces three kinds of lines: Only in DIR: file (a file exists on one side only), and Files DIR_A/file and DIR_B/file differ (a file exists in both but has different content). We grep -c for each pattern to build our summary counts.
  • The else ... : branch uses :, Bash’s “no-op” command, as a placeholder — in the full combined script below we’ll fill this in properly with the single-file logic from Step 2.

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

  • Comparing configuration files before and after a system update to see exactly what changed.
  • Auditing two “identical” deployment folders on different servers to catch configuration drift.
  • Reviewing backup snapshots by diffing two extracted backup archives to see what changed between them.
  • Verifying migration scripts by diffing a directory before and after a batch rename/reorganize operation.

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

  • Diffing files can expose sensitive content in output/logs — be careful piping diff output containing secrets (like config files with embedded credentials) into shared logs or chat tools.
  • Symlink handling — diff -r follows symlinks by default in some implementations; be aware of this when comparing directories that contain links to sensitive locations outside the intended scope.
  • Large binary files — diff isn’t designed for binary comparison; it will report “files differ” but not show meaningful content. Use cmp or checksums (sha256sum) for binary integrity checks instead.

Optimization Tips

  • For huge directory trees, diff -rq (quiet mode) is significantly faster than full -u recursive diffing, since it stops analyzing a file’s content the moment it detects any difference rather than computing the full line-by-line delta.
  • Exclude irrelevant directories (like .git or node_modules) with diff -rq --exclude=.git --exclude=node_modules to speed up comparisons and reduce noise.
  • If you’re comparing the same two large directories repeatedly, consider caching checksums (sha256sum -r) instead of re-running full diffs each time.

Troubleshooting

  • Script exits immediately with no output — remember that diff returns exit code 1 when differences are found, which combined with set -e will kill the script unless you add || true after diff calls, as shown throughout this article.
  • Colors don’t appear even with colordiff installed — some terminals or piped output (like into less) strip color codes; try colordiff -u file_a file_b | less -R, where -R tells less to preserve raw color codes.
  • “Only in” summary counts seem off — make sure your grep patterns exactly match the literal directory path strings as diff prints them; trailing slashes in your input paths can cause mismatches.

Common Mistakes to Avoid

  • Forgetting that diff‘s exit code of 1 means “differences found,” not “error” — don’t let set -e silently kill your script on this expected case.
  • Running diff on binary files and being confused by meaningless output — always check file type first with the file command.
  • Not excluding version control directories (.git) when diffing project folders, leading to enormous, unhelpful output.

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

Total
1
Shares

Leave a Reply

Previous Post
How to Create a Bash File Finder

How to Create a Bash File Finder

Next Post
How to Create a Bash File Backup Tool

How to Create a Bash File Backup Tool

Related Posts