I use diff almost every single day, usually without even thinking about it, because it’s baked into git, into config-management tools, and into every code review workflow I touch. But when I actually sat down to learn diff properly — not just the two or three flags I’d memorized — I realized how much more it could do: recursive directory comparisons, side-by-side views, unified patches I could email to a colleague, context diffs for old-school patch workflows. This guide covers all of that, from the absolute basics to the internals of how the comparison algorithm actually works.
What Is the diff Command?
diff compares two files (or two directories) line by line and reports the differences between them. It’s part of the GNU diffutils package, and it’s the foundation underneath tools you already use constantly: git diff, patch, code review tools, and configuration drift detectors all rely on the same core comparison logic that diff implements directly at the command line.
Basic Syntax
diff [OPTION]... FILE1 FILE2
diff [OPTION]... DIR1 DIR2
diff returns an exit status that scripts rely on constantly:
0— files are identical1— files differ2— an error occurred (e.g., a file doesn’t exist)
A Basic Example
Let’s set up two nearly-identical files:
$ printf 'line1\nline2\nline3\n' > file1.txt
$ printf 'line1\nlineX\nline3\nline4\n' > file2.txt
$ diff file1.txt file2.txt
2c2
< line2
---
> lineX
3a4
> line4
Reading this “normal” format output:
2c2means line 2 of file1 was changed into line 2 of file2.- Lines starting with
<belong to the first file. - Lines starting with
>belong to the second file. 3a4means after line 3 of file1, content was added, becoming line 4 of file2.
This normal format is the default, but it’s rarely what people actually want to read or share — the unified and context formats below are much more common in practice.
Unified Diff Format (-u)
This is the format you see in every git diff and almost every patch file on the internet:
$ diff -u file1.txt file2.txt
--- file1.txt 2026-07-31 01:36:41.930234377 +0000
+++ file2.txt 2026-07-31 01:36:41.930234377 +0000
@@ -1,3 +1,4 @@
line1
-line2
+lineX
line3
+line4
Here, lines prefixed with - were removed, lines prefixed with + were added, and unprefixed lines are unchanged context. The @@ -1,3 +1,4 @@ header tells you the hunk starts at line 1 spanning 3 lines in the original and 4 lines in the new file. This format is compact, human-readable, and directly consumable by patch and git apply.
Context Diff Format (-c)
An older, more verbose format that’s still used in some traditional Unix and BSD workflows:
$ diff -c file1.txt file2.txt
*** file1.txt Fri Jul 31 01:36:41 2026
--- file2.txt Fri Jul 31 01:36:41 2026
***************
*** 1,3 ****
line1
! line2
line3
--- 1,4 ----
line1
! lineX
line3
+ line4
Lines marked with ! indicate a change, + an addition, - a removal, and unmarked lines are context. Context diffs show more surrounding lines by default than unified diffs did historically, though modern unified diffs configured with -U N can match that.
Side-by-Side Format (-y)
When you want to eyeball two files next to each other rather than parse diff markers:
$ diff -y file1.txt file2.txt
line1 line1
line2 | lineX
line3 line3
> line4
The | marks a changed line, and > marks a line only present in the second file. You can control the column width with -W N (e.g. diff -y -W 80), and --suppress-common-lines will hide identical lines so only the differences are shown — very useful for wide files where scrolling through unchanged content wastes screen space.
Full Parameter Reference
| Option | Long Form | Description |
|---|---|---|
-u [N] | --unified[=N] | Unified diff format, with N lines of context (default 3) |
-c [N] | --context[=N] | Context diff format, with N lines of context |
-y | --side-by-side | Side-by-side column output |
-q | --brief | Only report whether files differ, not the details |
-r | --recursive | Recursively compare directories |
-N | --new-file | Treat absent files in one directory as empty during recursive compares |
-i | --ignore-case | Ignore case differences |
-w | --ignore-all-space | Ignore all whitespace when comparing |
-b | --ignore-space-change | Ignore changes in amount of whitespace |
-B | --ignore-blank-lines | Ignore changes where lines are all blank |
-x PATTERN | --exclude=PATTERN | Exclude files matching PATTERN in directory comparisons |
-a | --text | Treat all files as text, even if they look binary |
--color[=WHEN] | Colorize output (always, auto, never) | |
-s | --report-identical-files | Report when two files are the same |
-W N | --width=N | Set output width for -y |
--suppress-common-lines | Hide identical lines in -y output | |
-e | --ed | Output an ed script that transforms file1 into file2 |
-n | --rcs | Output RCS-format diff |
-q: Quick Brief Check
When you only care whether files differ, not the details, -q (or its alias --brief) is faster to read and script against:
$ diff -q file1.txt file2.txt
Files file1.txt and file2.txt differ
$ diff -q file1.txt file1.txt; echo "exit: $?"
exit: 0
Notice there’s no output at all when files are identical — only the exit code changes. This makes -q ideal for scripting conditionals.
-r: Recursive Directory Comparison
This is where diff becomes genuinely powerful for sysadmin work — comparing entire directory trees, like two versions of a config directory or a deployed application:
$ mkdir -p dir1 dir2
$ cp file1.txt dir1/
$ cp file2.txt dir1/common.txt
$ cp file2.txt dir2/common.txt
$ echo "hi" > dir1/onlyindir1.txt
$ diff -r dir1 dir2
Only in dir1: file1.txt
Only in dir1: onlyindir1.txt
Since common.txt is identical in both directories, it’s silently skipped — only differences and unique files are reported. Add -q to -r for a terse summary across a large tree, or drop -q to see full content diffs for every file that differs.
How diff Works Internally
diff‘s core job is solving what’s known in computer science as the longest common subsequence (LCS) problem. Given two sequences of lines, the goal is to find the longest sequence of lines that appears, in order, in both files — everything else is then classified as either removed (present only in file1) or added (present only in file2).
GNU diff implements a variant of the Myers diff algorithm, published by Eugene Myers in 1986, which finds an optimal (shortest) edit script in O(ND) time, where N is the sum of the lengths of the two files and D is the size of the edit script (the number of differing lines). This is dramatically faster than a naive O(N²) or O(N³) dynamic-programming LCS approach for files that are mostly similar — which is the overwhelmingly common case (patches, revisions, config tweaks).
The algorithm works conceptually by modeling the comparison as a graph traversal problem: each point in an edit graph represents a partial alignment between the two files, and the algorithm searches for the shortest path from the top-left corner (start of both files) to the bottom-right corner (end of both files), where diagonal moves represent matching lines (free) and horizontal/vertical moves represent insertions or deletions (cost 1). Myers’ algorithm efficiently prunes this search using a greedy, breadth-first-like expansion by edit distance, which is why diff stays fast even on files with thousands of lines.
Once the edit script is computed, diff formats it according to whichever output style you requested — normal, unified, context, side-by-side, or ed script — by grouping consecutive changes into “hunks” with configurable amounts of surrounding context.
Ignoring Whitespace and Case
Real-world files often differ only in trivial ways — trailing whitespace, indentation style, or capitalization — that you don’t actually care about:
$ diff -w file_with_spaces.txt file_without_spaces.txt # ignore all whitespace
$ diff -b file1.txt file2.txt # ignore amount of whitespace change
$ diff -i file1.txt file2.txt # ignore case
$ diff -B file1.txt file2.txt # ignore blank-line-only changes
Combining these is common in real workflows, e.g. diff -wBi to strip out noise and focus purely on substantive content changes.
Generating and Applying Patches
diff -u output is exactly what patch and git apply expect. A typical patch workflow:
$ diff -u original.conf modified.conf > changes.patch
$ patch original.conf < changes.patch
For entire directory trees, add recursive comparison with proper relative paths using -Nur (new-file, unified, recursive) — this is the classic incantation for generating a patch that can be applied cleanly against a fresh checkout:
$ diff -Nur project-v1/ project-v2/ > project-v1-to-v2.patch
Real-World Use Cases
1. Configuration Drift Detection
Comparing a live server’s config against a known-good baseline is one of the most common sysadmin uses:
$ diff -u /etc/nginx/nginx.conf /backups/nginx.conf.baseline
2. Pre-Deployment Review
Before pushing a config change, diffing the staged version against production catches unintended edits:
$ diff -u /etc/app/config.prod.yaml /etc/app/config.staged.yaml
3. Verifying File Integrity After Transfer
After scp or rsync, a quick diff -q confirms nothing got corrupted in transit:
$ diff -q original.tar.gz copied.tar.gz && echo "Transfer verified OK"
4. Auditing Package or System Changes
Comparing directory snapshots before and after a package installation reveals exactly what changed:
$ diff -rq /etc.before /etc.after
5. Code Review Without git
Sometimes you just have two versions of a script on disk, no repo involved:
$ diff -u deploy.sh.old deploy.sh.new | less
Shell Scripting and Automation
Here’s a small script I use to detect and alert on configuration drift across a fleet of servers, using diff‘s exit status directly in a conditional:
#!/bin/bash
# check_config_drift.sh - compare live config against baseline
set -euo pipefail
BASELINE="/opt/baselines/nginx.conf"
LIVE="/etc/nginx/nginx.conf"
if diff -q "$BASELINE" "$LIVE" > /dev/null; then
echo "OK: nginx.conf matches baseline"
exit 0
else
echo "WARNING: nginx.conf has drifted from baseline"
diff -u "$BASELINE" "$LIVE"
exit 1
fi
This pattern — if diff -q A B > /dev/null; then ... else ... fi — is one of the most common idioms in configuration-management and monitoring scripts, because it lets you branch on file equality cheaply and only pay the cost of generating a full diff when something has actually changed.
diff vs Related Commands
| Command | Purpose |
|---|---|
diff | Line-by-line comparison of two files or directories |
cmp | Byte-by-byte comparison; reports the first differing byte, useful for binary files |
comm | Compares two sorted files line by line, showing lines unique to each and lines common to both |
git diff | Wraps the same diff algorithm but adds version-control context (staged/unstaged, commit ranges) |
vimdiff / meld | Visual, interactive diff tools built on the same underlying comparison logic |
patch | Consumes diff output to apply changes to a file |
rsync -n --itemize-changes | Directory-level comparison focused on sync operations rather than content diffing |
The key distinction to remember: cmp is for binary-safe, byte-level comparison and stops at the first difference; diff is for text, line-oriented comparison and reports the complete set of differing regions.
Troubleshooting Common Issues
Problem: diff reports differences but the files look identical on screen. This is almost always whitespace — trailing spaces, tabs vs. spaces, or line-ending differences (\r\n vs \n). Try diff -w or check line endings with file file1.txt file2.txt (which reports “CRLF line terminators” if present) or cat -A.
Problem: diff -r reports “Only in dirX” for files that should be common. Confirm the filenames genuinely match, including case — diff is case-sensitive by default even with -r. Also verify you’re not being tripped up by symlinks that diff treats differently depending on whether -L or --no-dereference-style handling is in play.
Problem: diff treats a text file as binary and won’t show line-level differences. If a file contains a NUL byte or other binary-like content, diff may default to “Binary files … differ.” Force text-mode comparison with -a.
Problem: Comparing huge files is slow. diff‘s Myers algorithm is efficient for typical edit distances, but a worst-case scenario (two files with almost nothing in common) can degrade toward higher complexity. For huge, mostly-dissimilar files, consider whether you actually need a full diff, or whether a checksum comparison (sha256sum) is sufficient to confirm they differ.
Performance Optimization
For most files, diff is fast enough that performance tuning is unnecessary. For very large files or high-volume automated comparisons:
- Use
-qwhen you only need a yes/no answer — it can short-circuit without building the full edit script in some implementations. - Avoid
-rrecursion over directories containing huge binary artifacts (build outputs,node_modules,.gitinternals) — exclude them with-x. - For comparing large binary files, use
cmpinstead, sincediff‘s line-oriented algorithm isn’t the right tool and will be considerably slower. - If you’re diffing many file pairs in a loop, consider parallelizing with
xargs -Por GNUparallel, sincediffitself is single-threaded per invocation.
Security Implications
diff reads file contents and, when given directory arguments, traverses the filesystem — so the standard cautions apply: be careful comparing directories that might contain symlinks pointing outside the intended tree (diff -r follows symlinks by default in most configurations, which can leak or expose content from unexpected locations). When generating patches from sensitive files for review or transfer, remember the resulting diff contains the actual content of both files’ differing regions — treat patch files with the same sensitivity as the source files themselves. diff does not execute any code from its input, so it’s safe to run against untrusted files from a code-execution standpoint, but the output of a diff can itself be fed into patch, which does modify files — always review a patch before applying it from an untrusted source.
Compatibility Across Distributions
diff is part of GNU diffutils and is present by default on virtually every Linux distribution — Ubuntu, Debian, Fedora, RHEL/CentOS, Arch, openSUSE. The behavior described here is consistent across GNU-based systems. macOS and BSD systems ship a different, POSIX-oriented diff implementation with a smaller feature set (no --color, more limited GNU-style long options), so scripts intended to be portable across GNU/BSD should stick to POSIX-standard flags (-u, -c, -r, -i, -b) and test on both platforms if that matters.
Best Practices
- Default to
-ufor anything you intend to share, patch, or version-control — it’s the universal standard format. - Use
-qin scripts and monitoring checks for cheap boolean comparisons; only generate full diff output when a human needs to read it. - Combine
-w/-b/-B/-ideliberately, and know which one you’re using — silently ignoring whitespace can hide real bugs (e.g. significant indentation in YAML). - Exclude noisy directories (
.git, build artifacts, caches) with-xwhen doing recursive tree comparisons. - Always review a patch file before applying it, especially from an external source —
patchwill happily overwrite files based on whatever the diff says.
Summary
diff is one of the oldest and most quietly essential tools in the Linux ecosystem, implementing an efficient LCS-based comparison (via the Myers algorithm) that underlies everything from git to configuration management to code review tooling. Once you’re comfortable with unified format, recursive directory comparison, and the whitespace-handling flags, you have a tool that scales from “did this one file change” all the way up to full patch generation across entire project trees. It’s worth knowing well beyond the two or three flags most people memorize from muscle memory.
References
- GNU Diffutils Manual — https://www.gnu.org/software/diffutils/manual/html_node/index.html
- GNU Diffutils Manual: Comparing Two Files — https://www.gnu.org/software/diffutils/manual/html_node/Comparing-Two-Files.html
- Linux man-pages project:
man 1 diff— https://man7.org/linux/man-pages/man1/diff.1.html - Eugene W. Myers, “An O(ND) Difference Algorithm and Its Variations,” Algorithmica, 1986
- Ubuntu Manpage Repository — https://manpages.ubuntu.com/manpages/noble/en/man1/diff.1.html