ZetCode

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

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Linux tutorials.