FIELD NOTES / UNIX & LINUXNew tools on weekdays. Beyond the man page.
$diff

diff’s most important output is sometimes 1

Read a patch, detect configuration drift and move hunks with vimdiff—without confusing a changed file with a failed comparison.

THE DEEP DIVE15 MIN READ
01

The three outcomes matter

For diff, exit 1 often means the command did exactly what you asked: it found a change. Put that command into an unattended script and a useful result can become an apparent failure—or an error can disappear behind || true.

diff exits 0 when inputs compare equal, 1 when they differ, and greater than 1 when something went wrong. A nonzero result therefore does not automatically mean a broken command. In a shell with set -e, a normal difference can abort a script unless you handle the status explicitly.

Use a conditional and inspect the actual code. Do not silence every failure with || true: that also hides unreadable files and other trouble. A deployment check should treat differences as a review condition and errors as a failure to compare.

TERMINALSHELL
if diff -q before.conf after.conf; then
  echo 'same'
else
  rc=$?
  if [ "$rc" -eq 1 ]; then echo 'changed'; else echo "diff failed: $rc" >&2; exit "$rc"; fi
fi
02

Read a unified hunk, not just its colors

diff -u supplies context around changes. --- names the old file and +++ names the new; @@ -a,b +c,d @@ identifies old and new line ranges. A leading space is shared context, - removes from the old side, and + adds on the new side. These markers are data in the patch format, not an instruction to run a command.

Context lets patch locate a hunk even when nearby line numbers have shifted, but it is not proof that applying a patch will do what you intend. Review destination paths, especially for patches from outside your team, and test against a disposable checkout.

TERMINALSHELL
diff -u old.conf new.conf > change.patch
# View the output first; patching is a separate, state-changing step.
head -n 40 change.patch

Example hunk: @@ -1 +1 @@ -port=8080 +port=9090 The old and new ranges each contain one line. The minus and plus mark a removal and addition.

03

Normalization changes the question

Whitespace and line endings can overwhelm a configuration comparison. -b ignores changes in the amount of whitespace and -w ignores all whitespace for GNU diff and common implementations; -B suppresses hunks whose changed lines are all blank; a hunk that also changes nonblank lines can still show blank-line changes. These are useful review lenses, but never use a normalized comparison as proof that executable code or indentation-sensitive data is equivalent.

The --strip-trailing-cr option is GNU-specific. When portability matters, compare the original bytes and explain any normalization as a second view. Diff reports line-oriented changes; for binary data, cmp or a checksum may express the question more clearly. See the sed and awk pipeline for the contract behind a normalization stage.

TERMINALSHELL
diff -u old.conf new.conf
diff -u -b old.conf new.conf
# GNU-specific for CRLF versus LF review:
diff -u --strip-trailing-cr old.txt new.txt
04

Directories need a scope and a policy

Given two directories, diff compares corresponding names at the top level. Add -r to recurse. It reports missing files as well as changed files, which can make it a useful deployment inventory. -N treats a missing file as empty; that may help a patch but can obscure the difference between absent and zero-length in a compliance check.

Ignore patterns and symlink handling vary by implementation. Do not make a destructive synchronization decision from a single diff output without checking scope, mount points, permissions, and whether generated files were excluded.

TERMINALSHELL
diff -r -q /etc/app/golden /etc/app/current
diff -r -u /etc/app/golden /etc/app/current > app-review.diff

Write app-review.diff in a working directory you control; reports may contain configuration secrets.

05

A patch is a proposal, not a trusted program

A unified diff can be applied by patch or interpreted by another tool. That is a separate operation with file writes. Paths in an untrusted patch can refer to files you did not mean to edit; an installer that invokes patch with elevated privileges expands the impact of a malicious or mistaken patch.

Generate patches from a controlled source, inspect file headers and hunk content, and apply in a disposable tree first. Modern patch tools offer safety checks, but their exact path rules and defaults differ; avoid promising a universal flag that neutralizes every crafted patch.

A historical case makes the distinction concrete: CVE-2018-1000156 involved GNU patch’s handling of legacy ed-format input, with command execution demonstrated in the original oss-security disclosure. The flaw was in patch, not diff’s comparison engine. The published demonstration establishes exploitability, not confirmed malicious use in the wild.

TERMINALSHELL
diff -u old.conf new.conf > change.patch
# Review paths and hunks before a dry run in a disposable directory.
patch --dry-run -p0 < change.patch   # GNU patch; verify target tree
06

The algorithm can give more than one valid answer

There may be several equally short edit scripts for repeated lines. A diff hunk is a coherent explanation of a change, not a unique mathematical identity of the files. Large or repetitive inputs can be expensive, and differing algorithms or options may choose different hunk boundaries.

If a patch is hard to review, reduce unrelated formatting changes, split the change into smaller units, or compare with a different lens. For structured data, parse the format before comparing selected fields; line diff does not know semantic equivalence.

TERMINALSHELL
diff -u before.conf after.conf
# GNU diff: favor speed in a difficult comparison if exact hunk choice is less important.
diff --speed-large-files -u before.log after.log
07

Enter vimdiff deliberately

vimdiff file1 file2 starts Vim in diff mode; vim -d is equivalent. It opens a view into two editable buffers with synchronized windows. This is not a frontend that merely displays the output of the diff utility: Vim may use an internal diff library, or an external diff program depending on options.

Navigate changes with ]c and [c. do obtains the current hunk from the other buffer into the active buffer, while dp puts the active hunk into the other buffer. The direction depends on which window is active. Before the first transfer, use Ctrl-W w to confirm where you are and :ls or the status line to confirm the buffer name.

TERMINALSHELL, THEN VIM COMMANDS
vimdiff old.conf new.conf
]c       next change
[c       previous change
do       obtain from other buffer into current
dp       put current change into other buffer
:diffupdate
08

Compare streams without creating temporary files

On Bash and other shells with process substitution, diff can compare the outputs of two commands as if they were files. This is convenient for sorting configuration inventories, but process substitution is a shell feature, not a diff option, and failure in either producer may be missed if you only inspect diff status.

For a high-stakes comparison, materialize and check both producer outputs first. A clean diff between two empty outputs is no proof that both commands succeeded.

TERMINALSHELL
# Bash process substitution; convenient for an exploratory comparison.
diff -u <(sort before.txt) <(sort after.txt)
# For an audit, check command status before comparing saved outputs.
09

Print added lines instead of a patch

GNU diff’s line-format options can output only lines added to the second file. Compare two inventories and it becomes a compact report generator. Its status still follows the usual contract: 1 for differences, not a failed comparison.

This also exposes a less obvious privilege boundary. diff reads its inputs and can reproduce their content. Allowing privileged diff on arbitrary paths can disclose files even if the policy only calls it a comparison tool. That is ordinary functionality exposed through a bad permission policy, not a diff CVE.

TERMINALSHELL
diff --old-line-format='' --new-line-format='%L' \
     --unchanged-line-format='' before.txt after.txt
10

Line endings, final newlines, and binary data

A file without a final newline is different from one with it, and unified output marks that condition. CRLF and LF files may appear changed on every line even when their visible text matches. Normalize a copy for a second view, while retaining an exact byte comparison for the record.

For binary data, diff may only report that files differ. cmp reports the first differing byte, and hashes can help compare large artifacts when both were read successfully. None of these tools understands the semantic meaning of a serialized object.

TERMINALSHELL
diff -u unix.txt windows.txt
cmp -s image-a.bin image-b.bin; echo $?
# 0 same, 1 different, >1 trouble for cmp as well.
11

Three-way merge is a separate problem

Two-way diff says how two versions differ; it does not know which changes came from a shared base. For a merge conflict, a base plus two independently edited versions supplies the missing context. diff3 and merge tools can report conflicts; vimdiff can show multiple buffers and let a human choose hunks.

Do not confuse a visually clean vimdiff view with a resolved merge on disk. Save the intended buffer, verify conflict markers are gone, and run application tests. A syntactically clean file can still be semantically wrong.

TERMINALSHELL
# Inspect three inputs when a common ancestor is available.
diff3 mine.conf base.conf theirs.conf
vimdiff mine.conf base.conf theirs.conf
12

Use vimdiff for a merge, then verify with diff

For a three-way merge, Vim can show more than two buffers, but the source of a hunk becomes ambiguous. Use the explicit :diffget buffer-name or :diffput buffer-name instead of a directionless shortcut. Save the intended output buffer, and run an ordinary diff or a test after leaving the editor.

Vimdiff displays differences in buffers, including edits not yet written to disk. A shell diff immediately afterward reads files on disk. That difference explains many apparent disagreements between the two tools. :diffupdate refreshes the display, while :write persists a buffer.

TERMINALVIM / EX COMMANDS
:ls
:diffget base.conf
:write
:diffupdate

After leaving Vim, diff -u expected.conf merged.conf compares the files saved on disk. :diffget base.conf names the source buffer from the preceding three-file example.

13

Security and vulnerability scope

An attacker can hide significant changes in a large patch or in files excluded from a comparison. A workflow that accepts a patch and writes with elevated privileges needs path and content review. Vimdiff inherits Vim security considerations: opening an untrusted file may process editor settings or a vulnerable modeline on affected versions, as the Vim modeline discussion explains.

In the sources checked for this article, no specific GNU diff executable CVE is presented as a current issue. Do not borrow a vulnerability from a similarly named JavaScript diff library or from another utility in the diffutils package and attribute it to diff. Check vendor package advisories for the installed implementation and release.

TERMINALSHELL
diff --version 2>/dev/null | head -n 1
vim --version | head -n 1
dpkg-query -W diffutils vim 2>/dev/null || rpm -q diffutils vim
14

Try a change in both directions

Copy a small configuration file into two disposable locations and alter one setting. Compare the pair with diff -u, deliberately capture exit 1, then open both in vimdiff and obtain or put a hunk. Save only the intended side and rerun diff. The exercise teaches the distinction among report, editor buffer, on-disk file, and script status.

When comparing production configuration, do the reading under the least privilege needed. Diff output itself may contain secrets; treat reports, CI logs, and stored patches according to the sensitivity of their inputs.

TERMINALSHELL
workdir=$(mktemp -d) || exit 1
cp app.conf "$workdir/before.conf"
cp app.conf "$workdir/after.conf"
diff -u "$workdir/before.conf" "$workdir/after.conf"
vimdiff "$workdir/before.conf" "$workdir/after.conf"

Edit only these copies; remove the temporary directory after review.

Back to latest issue
HELP SHAPE THE NEXT ISSUE

What belongs in your toolbox?

Choose a command you want to know better. We start with familiar tools and work toward the less obvious ones.

READER’S CHOICE

What should we cover next?

Vote for a future deep dive, or suggest a command we missed.

One command name, up to 32 characters. Suggestions are reviewed by a person. A new article starts a fresh poll.

Loading poll…