Linux diff and patch Commands
last modified October 23, 2026
The diff command compares files line by line and prints the
differences. The patch command takes such a list of
differences, a patch, and applies it to the original files. Together
they were the way to distribute changes to source code long before version
control systems existed, and their unified format is still used by Git, code
review tools, and mailing lists.
The examples use GNU diffutils 3.8 and GNU patch 2.7.6. Most of them work
with two small fruit lists and with two versions of a small Python project,
v1 and v2.
Comparing two files
In its simplest form, diff takes two files and lists what has
to be changed in the first to make it identical to the second.
diff fruits1.txt fruits2.txt
The default, or normal, format consists of commands: 2c2
means that line 2 of the first file is changed into line 2 of the second
file, and 4a5 means that after line 4 of the first file, line 5
of the second file is added. A d marks deleted lines. Lines
from the first file start with <, lines from the second file
with >. This format is compact but carries no context, so it
is rarely used for patches.
$ diff fruits1.txt fruits2.txt 2c2 < banana --- > blueberry 4a5 > fig
Exit status
diff reports the result in its exit status, which makes it
useful in scripts.
diff fruits1.txt fruits1.txt; echo "exit: $?" diff fruits1.txt fruits2.txt > /dev/null; echo "exit: $?" diff fruits1.txt missing.txt; echo "exit: $?" diff -q fruits1.txt fruits2.txt diff -s fruits1.txt fruits1.txt
The status is 0 when the files are identical, 1 when they differ, and 2 on
trouble, such as a missing file. Note that a difference is not an error, so
in a script with set -e, a diff that finds changes
terminates the script. The -q option only reports whether the
files differ, and -s also reports identical files.
$ diff fruits1.txt fruits1.txt; echo "exit: $?" exit: 0 $ diff fruits1.txt fruits2.txt > /dev/null; echo "exit: $?" exit: 1 $ diff fruits1.txt missing.txt; echo "exit: $?" diff: missing.txt: No such file or directory exit: 2 $ diff -q fruits1.txt fruits2.txt Files fruits1.txt and fruits2.txt differ $ diff -s fruits1.txt fruits1.txt Files fruits1.txt and fruits1.txt are identical
Unified format
The -u option prints differences in the unified format, the
standard format for patches.
diff -u fruits1.txt fruits2.txt
The header names the old file after --- and the new file after
+++, together with their modification times. Each change is a
hunk that starts with a line like @@ -1,5 +1,6 @@: the
hunk covers 5 lines starting at line 1 in the old file and 6 lines starting
at line 1 in the new file. In the body, removed lines start with
-, added lines with +, and unchanged context lines
with a space. A changed line appears as a removal followed by an addition.
$ diff -u fruits1.txt fruits2.txt --- fruits1.txt 2026-10-20 09:15:00.000000000 +0200 +++ fruits2.txt 2026-10-22 16:40:00.000000000 +0200 @@ -1,5 +1,6 @@ apple -banana +blueberry cherry date +fig elderberry
Unified diff of source code
The unified format is easy to read for code changes.
diff -u v1/src/app.py v2/src/app.py
By default, each change is shown with three lines of context before and
after it. Changes that are close together share their context and form one
hunk, which is why we get a single hunk here. The context lets
patch find the right place even when line numbers have
shifted, as we see later.
$ diff -u v1/src/app.py v2/src/app.py
--- v1/src/app.py 2026-09-01 10:00:00.000000000 +0200
+++ v2/src/app.py 2026-10-20 14:30:00.000000000 +0200
@@ -1,15 +1,15 @@
import sys
-def greet(name):
- print("Hello " + name)
+def greet(name, greeting="Hello"):
+ print(f"{greeting}, {name}!")
def main():
if len(sys.argv) < 2:
- print("usage: app.py NAME")
+ print("usage: app.py NAME [GREETING]", file=sys.stderr)
return 1
- greet(sys.argv[1])
+ greet(*sys.argv[1:3])
return 0
Number of context lines
The -U option sets the number of context lines.
diff -U 1 v1/src/app.py v2/src/app.py
With one line of context, the changes are far enough apart to form two
separate hunks. -U 0 shows only the changed lines, and a large
value, such as -U 1000, shows the whole file with changes
marked, which is handy for reviewing short files.
$ diff -U 1 v1/src/app.py v2/src/app.py
--- v1/src/app.py 2026-09-01 10:00:00.000000000 +0200
+++ v2/src/app.py 2026-10-20 14:30:00.000000000 +0200
@@ -3,4 +3,4 @@
-def greet(name):
- print("Hello " + name)
+def greet(name, greeting="Hello"):
+ print(f"{greeting}, {name}!")
@@ -9,5 +9,5 @@
if len(sys.argv) < 2:
- print("usage: app.py NAME")
+ print("usage: app.py NAME [GREETING]", file=sys.stderr)
return 1
- greet(sys.argv[1])
+ greet(*sys.argv[1:3])
return 0
Context format
The older context format, produced with -c, shows the old and
the new version of each hunk separately.
diff -c fruits1.txt fruits2.txt
Changed lines are marked with !, added lines with
+, and removed lines with -. The format is longer
than the unified one, and new patches rarely use it, but
patch still understands it.
$ diff -c fruits1.txt fruits2.txt *** fruits1.txt 2026-10-20 09:15:00.000000000 +0200 --- fruits2.txt 2026-10-22 16:40:00.000000000 +0200 *************** *** 1,5 **** apple ! banana cherry date elderberry --- 1,6 ---- apple ! blueberry cherry date + fig elderberry
Side-by-side output
The -y option shows the files in two columns, and
-W sets the total width.
diff -y -W 40 fruits1.txt fruits2.txt diff -y -W 40 --suppress-common-lines fruits1.txt fruits2.txt
The middle column marks changed lines with |, added lines with
>, and removed lines with <. The
--suppress-common-lines option hides lines that are the same.
For interactive side-by-side comparison, vimdiff and graphical
tools such as Meld are more comfortable.
$ diff -y -W 40 fruits1.txt fruits2.txt apple apple banana | blueberry cherry cherry date date > fig elderberry elderberry $ diff -y -W 40 --suppress-common-lines fruits1.txt fruits2.txt banana | blueberry > fig
Ignoring whitespace
Differences in whitespace and line endings often hide the real changes.
The file settings-win.conf was saved on Windows with CRLF line
endings, an extra space, and a trailing empty line.
diff settings.conf settings-win.conf | cat -A diff --strip-trailing-cr settings.conf settings-win.conf diff --strip-trailing-cr -b -B settings.conf settings-win.conf; echo "exit: $?"
cat -A makes the carriage returns visible as ^M;
because of them, every line differs. The --strip-trailing-cr
option ignores them, -b ignores changes in the amount of
whitespace, and -B ignores blank lines. With all three, the
files are considered equal. The -w option ignores all
whitespace, and -i ignores case.
$ diff settings.conf settings-win.conf | cat -A 1,2c1,3$ < host = localhost$ < port = 8080$ ---$ > host = localhost^M$ > port = 8080^M$ > ^M$ $ diff --strip-trailing-cr settings.conf settings-win.conf 2c2,3 < port = 8080 --- > port = 8080 > $ diff --strip-trailing-cr -b -B settings.conf settings-win.conf; echo "exit: $?" exit: 0
Comparing directories
With two directories, diff compares files with the same names.
The -r option descends into subdirectories.
diff -r v1 v2 diff -rq v1 v2
For each pair of differing files, diff prints a line with the
equivalent command and the differences. Files that exist on one side only
are reported as "Only in". Combined with -q, we get a quick
overview of which files differ.
$ diff -r v1 v2
diff -r v1/config.ini v2/config.ini
3,4c3,4
< version = 1.0
< debug = true
---
> version = 1.1
> debug = false
Only in v2: docs
diff -r v1/src/app.py v2/src/app.py
4,5c4,5
< def greet(name):
< print("Hello " + name)
---
> def greet(name, greeting="Hello"):
> print(f"{greeting}, {name}!")
10c10
< print("usage: app.py NAME")
---
> print("usage: app.py NAME [GREETING]", file=sys.stderr)
12c12
< greet(sys.argv[1])
---
> greet(*sys.argv[1:3])
$ diff -rq v1 v2
Files v1/config.ini and v2/config.ini differ
Only in v2: docs
Files v1/src/app.py and v2/src/app.py differ
New files and exclusions
By default, files that exist only on one side are just reported. The
-N option treats them as empty on the other side, so their
whole content appears in the diff.
diff -ruN v1 v2 | grep -E '^(diff|---|\+\+\+)' diff -rq -x '*.md' -x docs v1 v2
The grep keeps only the header lines. The new
CHANGES.md is compared with an empty file, shown with the date
of the Unix epoch, so a patch made with -N creates the file.
-ruN is the usual combination for making a patch of a
directory tree. The -x option excludes files and directories
by a name pattern, for example build outputs.
$ diff -ruN v1 v2 | grep -E '^(diff|---|\+\+\+)' diff -ruN v1/config.ini v2/config.ini --- v1/config.ini 2026-09-01 10:00:00.000000000 +0200 +++ v2/config.ini 2026-10-20 14:30:00.000000000 +0200 diff -ruN v1/docs/CHANGES.md v2/docs/CHANGES.md --- v1/docs/CHANGES.md 1970-01-01 01:00:00.000000000 +0100 +++ v2/docs/CHANGES.md 2026-10-20 14:30:00.000000000 +0200 diff -ruN v1/src/app.py v2/src/app.py --- v1/src/app.py 2026-09-01 10:00:00.000000000 +0200 +++ v2/src/app.py 2026-10-20 14:30:00.000000000 +0200 $ diff -rq -x '*.md' -x docs v1 v2 Files v1/config.ini and v2/config.ini differ Files v1/src/app.py and v2/src/app.py differ
Comparing command output
diff can compare the output of commands with process
substitution, and it reads standard input when a file name is
-.
diff <(ls v1) <(ls v2) echo "Hello" | diff -i - <(echo "hello") && echo "equal ignoring case"
The first command compares the directory listings. The same technique
compares sorted files, diff <(sort a.txt) <(sort b.txt),
or the output of a command on two servers, diff <(ssh web1 cat
/etc/nginx/nginx.conf) <(ssh web2 cat /etc/nginx/nginx.conf).
$ diff <(ls v1) <(ls v2) 1a2 > docs $ echo "Hello" | diff -i - <(echo "hello") && echo "equal ignoring case" equal ignoring case
Binary files
For binary files, diff only reports that they differ. The
cmp command finds the first differing byte.
diff /bin/ls /bin/cat cmp fruits1.txt fruits2.txt
cmp works on any file and reports the byte and line number of
the first difference; cmp -s only sets the exit status, which
makes it a fast identity check in scripts.
$ diff /bin/ls /bin/cat Binary files /bin/ls and /bin/cat differ $ cmp fruits1.txt fruits2.txt fruits1.txt fruits2.txt differ: byte 8, line 2
git diff for any files
Git's diff works outside of repositories with the --no-index
option and offers some extras, such as statistics.
git diff --no-index v1/config.ini v2/config.ini git diff --no-index --stat v1 v2
The output is a unified diff with a Git header; the a/ and
b/ prefixes are the reason why Git patches are applied with
patch -p1. In a terminal, Git colours the output and pipes it
through a pager. The --stat option summarizes the changes per
file, and --word-diff highlights changed words instead of whole
lines. GNU diff also has --color.
$ git diff --no-index v1/config.ini v2/config.ini
diff --git a/v1/config.ini b/v2/config.ini
index 3b76cb1..f653d59 100644
--- a/v1/config.ini
+++ b/v2/config.ini
@@ -1,4 +1,4 @@
[app]
name = greeter
-version = 1.0
-debug = true
+version = 1.1
+debug = false
$ git diff --no-index --stat v1 v2
{v1 => v2}/config.ini | 4 ++--
/dev/null => v2/docs/CHANGES.md | 5 +++++
{v1 => v2}/src/app.py | 8 ++++----
3 files changed, 11 insertions(+), 6 deletions(-)
Applying a patch to a file
A patch is simply saved diff -u output. The patch
command reads it from standard input and changes the file.
diff -u fruits1.txt fruits2.txt > fruits.patch cp fruits1.txt list.txt patch list.txt < fruits.patch diff list.txt fruits2.txt && echo "identical"
When a file name is given, patch applies the patch to that
file; otherwise, it takes the name from the patch header. After patching,
list.txt is identical to the new version. This is how a change
is sent to someone who has the old version: the patch is much smaller than
the whole file and shows exactly what has changed.
$ diff -u fruits1.txt fruits2.txt > fruits.patch $ cp fruits1.txt list.txt $ patch list.txt < fruits.patch patching file list.txt $ diff list.txt fruits2.txt && echo "identical" identical
Patching a directory tree
For a whole project, we create the patch with diff -ruN from
the parent directory and apply it inside the project directory.
diff -ruN v1 v2 > update.patch head -n 4 update.patch cp -r v1 project cd project patch -p1 --dry-run < ../update.patch patch -p1 < ../update.patch diff -r . ../v2 && echo "project matches v2"
The file names in the patch start with v1/ and
v2/, but the recipient's directory can have any name. The
-p1 option strips the first path component, so
v1/src/app.py becomes src/app.py, relative to the
current directory. The --dry-run option checks that the patch
applies without changing anything. After the real run, the project is
identical to v2, including the new file.
$ diff -ruN v1 v2 > update.patch $ head -n 4 update.patch diff -ruN v1/config.ini v2/config.ini --- v1/config.ini 2026-09-01 10:00:00.000000000 +0200 +++ v2/config.ini 2026-10-20 14:30:00.000000000 +0200 @@ -1,4 +1,4 @@ $ cp -r v1 project $ cd project $ patch -p1 --dry-run < ../update.patch checking file config.ini checking file docs/CHANGES.md checking file src/app.py $ patch -p1 < ../update.patch patching file config.ini patching file docs/CHANGES.md patching file src/app.py $ diff -r . ../v2 && echo "project matches v2" project matches v2
The wrong strip level
The most common problem when applying patches is a wrong -p
value.
patch -p0 -t --dry-run < ../update.patch
With -p0, the paths are used as they are, and there is no
v1/config.ini inside the project. Normally, patch
then asks interactively for the file name; the -t option
(batch mode) skips such files instead. The new file is the only one that
"works": it would be created in a wrong v1/docs directory. The
rule of thumb is: -p1 for patches made from the parent
directory and for Git patches, -p0 for patches made inside the
project directory.
$ patch -p0 -t --dry-run < ../update.patch can't find file to patch at input line 4 Perhaps you used the wrong -p or --strip option? The text leading up to this was: -------------------------- |diff -ruN v1/config.ini v2/config.ini |--- v1/config.ini 2026-09-01 10:00:00.000000000 +0200 |+++ v2/config.ini 2026-10-20 14:30:00.000000000 +0200 -------------------------- No file to patch. Skipping patch. 1 out of 1 hunk ignored checking file v1/docs/CHANGES.md can't find file to patch at input line 23 Perhaps you used the wrong -p or --strip option? The text leading up to this was: -------------------------- |diff -ruN v1/src/app.py v2/src/app.py |--- v1/src/app.py 2026-09-01 10:00:00.000000000 +0200 |+++ v2/src/app.py 2026-10-20 14:30:00.000000000 +0200 -------------------------- No file to patch. Skipping patch. 1 out of 1 hunk ignored
Applying a patch twice
patch detects when a patch has already been applied.
patch -p1 -N < ../update.patch; echo "exit: $?" ls src
Without options, patch asks in a terminal whether to reverse
the patch instead. The -N option (forward) skips patches that look
reversed or already applied. The skipped hunks are saved in
.rej files, and the exit status is 1. In scripts,
patch -N makes the step idempotent, but the rejects should be
removed afterwards.
$ patch -p1 -N < ../update.patch; echo "exit: $?" patching file config.ini Reversed (or previously applied) patch detected! Skipping patch. 1 out of 1 hunk ignored -- saving rejects to file config.ini.rej The next patch would create the file docs/CHANGES.md, which already exists! Skipping patch. 1 out of 1 hunk ignored patching file src/app.py Reversed (or previously applied) patch detected! Skipping patch. 1 out of 1 hunk ignored -- saving rejects to file src/app.py.rej exit: 1 $ ls src app.py app.py.rej
Reversing a patch
The -R option applies a patch in reverse, which undoes it.
patch -p1 -R < ../update.patch diff -r . ../v1 && echo "back to v1" ls docs
The project is back at version 1. The file that the patch created is removed, and so is its now empty directory. Keeping patches makes it easy to revert changes that turned out to be bad.
$ patch -p1 -R < ../update.patch patching file config.ini patching file docs/CHANGES.md patching file src/app.py $ diff -r . ../v1 && echo "back to v1" back to v1 $ ls docs ls: cannot access 'docs': No such file or directory
Making backups
The -b option saves the original of each changed file with the
.orig suffix.
patch -p1 -b < ../update.patch ls src config.ini*
The backups can be compared with the patched files or used to restore
them. The -z option sets a different suffix, and
-B a prefix, such as a backup directory.
$ patch -p1 -b < ../update.patch patching file config.ini patching file docs/CHANGES.md patching file src/app.py $ ls src config.ini* config.ini config.ini.orig src: app.py app.py.orig
Offsets
A patch can still apply when the target file has changed in other places.
We add two lines to the top of app.py before patching.
sed -i '1i #!/usr/bin/env python3\n"""A greeting program."""' src/app.py patch -p1 < ../update.patch
The hunk expected its text at line 1, but patch found the
context two lines lower and applied it there, reporting the offset. When
some context lines do not match either, patch can ignore up to
two context lines at the edges of a hunk; it then reports "with fuzz".
Offsets are harmless, but a fuzzy hunk should be checked.
$ sed -i '1i #!/usr/bin/env python3\n"""A greeting program."""' src/app.py $ patch -p1 < ../update.patch patching file config.ini patching file docs/CHANGES.md patching file src/app.py Hunk #1 succeeded at 3 (offset 2 lines).
Failed hunks
When the code that a hunk changes has been modified, the hunk cannot be applied.
sed -i 's/print("Hello " + name)/print("Hi " + name)/' src/app.py
diff -U 1 ../v1/src/app.py ../v2/src/app.py > ../app.patch
patch src/app.py < ../app.patch; echo "exit: $?"
cat src/app.py.rej
grep -n 'greet\|usage' src/app.py
We change a line of a fresh copy of version 1 that the patch also changes,
and create a patch with two hunks. The second hunk applies, the first one
fails: it is saved in app.py.rej, and patch exits
with status 1. The grep shows that the file is now half
patched. A failed hunk has to be applied by hand, using the reject file as
a guide; then the .rej file is deleted.
$ sed -i 's/print("Hello " + name)/print("Hi " + name)/' src/app.py
$ diff -U 1 ../v1/src/app.py ../v2/src/app.py > ../app.patch
$ patch src/app.py < ../app.patch; echo "exit: $?"
patching file src/app.py
Hunk #1 FAILED at 3.
1 out of 2 hunks FAILED -- saving rejects to file src/app.py.rej
exit: 1
$ cat src/app.py.rej
--- app.py 2026-09-01 10:00:00.000000000 +0200
+++ app.py 2026-10-20 14:30:00.000000000 +0200
@@ -3,4 +3,4 @@
-def greet(name):
- print("Hello " + name)
+def greet(name, greeting="Hello"):
+ print(f"{greeting}, {name}!")
$ grep -n 'greet\|usage' src/app.py
4:def greet(name):
10: print("usage: app.py NAME [GREETING]", file=sys.stderr)
12: greet(*sys.argv[1:3])
Best practices for diff and patch
- Use the unified format: Make patches with
diff -u, ordiff -ruNfor directories. - Diff from the parent directory: Then the patch is
applied with
patch -p1inside the project, like a Git patch. - Try first: Run
patch --dry-runbefore the real run. - Check the result: Look for
.rejfiles and fuzzy hunks after patching. - Mind exit status 1: In scripts, a difference is not an error; only status 2 means trouble.
Source
GNU diffutils manual, patch(1) manual page
In this article, we have compared files and directories with
diff in the normal, unified, context, and side-by-side
formats, ignored whitespace, and created and applied patches with
patch, including dry runs, strip levels, reversing, backups,
offsets, and failed hunks.
Author
List all Linux tutorials.