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 -uproduces 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 defaultdiffformat, which only shows raw line numbers and change indicators.- The existence checks at the top (
[[ ! -e "$FILE_A" ... ]]) prevent a confusing rawdifferror 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/nullchecks whethercolordiffis installed and falls back gracefully to plaindiffif 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.|| truepreventsset -efrom exiting the script whengrepfinds zero matches (whichgreptreats as a “failure” exit code, even though finding nothing is a perfectly valid outcome here).- We deliberately run
difftwice — once without-ufor counting, once with-ufor the readable display — because unified format is harder to reliablygrep-count without over- or under-counting context lines. - The final
$DIFF_CMD -u "$FILE_A" "$FILE_B" || truealso needs|| truebecausediffexits with status1(not0) whenever it finds any differences — which is normal, expected behavior, not an actual error, butset -edoesn’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 -rqcombines-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 -rqproduces three kinds of lines:Only in DIR: file(a file exists on one side only), andFiles DIR_A/file and DIR_B/file differ(a file exists in both but has different content). Wegrep -cfor 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 -rfollows 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 —
diffisn’t designed for binary comparison; it will report “files differ” but not show meaningful content. Usecmpor checksums (sha256sum) for binary integrity checks instead.
Optimization Tips
- For huge directory trees,
diff -rq(quiet mode) is significantly faster than full-urecursive 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
.gitornode_modules) withdiff -rq --exclude=.git --exclude=node_modulesto 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
diffreturns exit code1when differences are found, which combined withset -ewill kill the script unless you add|| trueafter diff calls, as shown throughout this article. - Colors don’t appear even with
colordiffinstalled — some terminals or piped output (like intoless) strip color codes; trycolordiff -u file_a file_b | less -R, where-Rtellslessto preserve raw color codes. - “Only in” summary counts seem off — make sure your grep patterns exactly match the literal directory path strings as
diffprints them; trailing slashes in your input paths can cause mismatches.
Common Mistakes to Avoid
- Forgetting that
diff‘s exit code of1means “differences found,” not “error” — don’t letset -esilently 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
filecommand. - 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.