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.
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
fiRead 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.
diff -u old.conf new.conf > change.patch
# View the output first; patching is a separate, state-changing step.
head -n 40 change.patchExample 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.
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.
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.txtDirectories 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.
diff -r -q /etc/app/golden /etc/app/current
diff -r -u /etc/app/golden /etc/app/current > app-review.diffWrite app-review.diff in a working directory you control; reports may contain configuration secrets.
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.
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 treeThe 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.
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.logEnter 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.
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
:diffupdateCompare 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.
# 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.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.
diff --old-line-format='' --new-line-format='%L' \
--unchanged-line-format='' before.txt after.txtLine 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.
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.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.
# Inspect three inputs when a common ancestor is available.
diff3 mine.conf base.conf theirs.conf
vimdiff mine.conf base.conf theirs.confUse 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.
:ls
:diffget base.conf
:write
:diffupdateAfter 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.
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.
diff --version 2>/dev/null | head -n 1
vim --version | head -n 1
dpkg-query -W diffutils vim 2>/dev/null || rpm -q diffutils vimTry 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.
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.