Terug naar hoofdinhoud

Linux commando: diff

31 augustus 2026

Twee bestanden die hetzelfde zouden moeten zijn, zijn dat niet. Een configuratie die gisteren werkte doet het vandaag niet, aan een teruggezette backup ontbreekt misschien iets, of een leverancier stuurde je een "kleine correctie" voor een bestand dat je zelf al aangepast had. Het commando dat alle drie de vragen beantwoordt is diff, en de uitvoer ervan is geen rapport om te lezen. Het is een reeks instructies die een ander programma kan uitvoeren.

1. De basis

diff vergelijkt twee bestanden regel voor regel en beschrijft wat je in het eerste zou moeten veranderen om er het tweede van te maken. Die richting doet ertoe: een diff is geen neutrale lijst verschillen, het is een recept met een van en een naar.

1.1 De eenvoudigst mogelijke toepassing

Twee versies van een klein configuratiebestand:

$ diff old.php new.php
3c3
<     'host'  => 'localhost',
---
>     'host'  => 'db.internal',
5c5
<     'debug' => false,
---
>     'debug' => true,

Dit is het oorspronkelijke uitvoerformaat uit 1974, en het leest als een reeks bewerkingen:

OnderdeelBetekent
3c3 Regel 3 van het eerste bestand verandert (change) in regel 3 van het tweede
< Een regel uit het eerste bestand
> Een regel uit het tweede bestand
--- De scheiding tussen beide kanten

Twee andere letters verschijnen in plaats van c: a voor regels die toegevoegd worden (add) en d voor regels die verdwijnen (delete). 7a8 betekent dus "voeg na regel 7 van het oude bestand toe wat regel 8 van het nieuwe wordt".

Je zult dit formaat zelden bewust gebruiken. Hoofdstuk 4 behandelt de twee die je echt wilt. Het is het herkennen waard, want dit krijg je als je vergeet om iets anders te vragen.

1.2 De afsluitcode is het punt

De uitvoer is voor mensen. De afsluitcode is wat diff bruikbaar maakt in een script, en die heeft drie waarden:

$ diff old.php old.php >/dev/null ; echo $?
0                                  # the files are identical

$ diff old.php new.php >/dev/null ; echo $?
1                                  # they differ

$ diff old.php nosuch.php >/dev/null 2>&1 ; echo $?
2                                  # something went wrong

Die 2 is degene die mensen vergeten. Een ontbrekend bestand, een onleesbare map of een verkeerde optie levert allemaal die code op, en een script dat "niet nul" leest als "de bestanden verschillen" meldt een verschil dat in werkelijkheid een typefout in een pad was.

if diff -q "$a" "$b" >/dev/null; then
    echo "identical"
elif [ $? -eq 1 ]; then
    echo "they differ"
else
    echo "diff itself failed"
fi

Het juiste mentale model: diff vertelt je niet dat twee bestanden verschillen. Hij schrijft de instructies op om van het eerste het tweede te maken, in een formaat dat precies genoeg is dat patch ze zonder jou kan uitvoeren.

Naar boven

2. Waar komt de naam vandaan?

diff is kort voor difference, verschil, en ongebruikelijk voor een Unix-commando is dat het hele verhaal. De interessante namen zijn die eromheen:

diff     the DIFFerence between two files
patch    applies a diff to a file; named for what it does
hunk     one contiguous group of changed lines, with its surrounding context
context  the unchanged lines printed around a change, so patch can find the place
fuzz     how much of that context patch is allowed to ignore (section 7.2)
.rej     a rejected hunk, written to a file for you to deal with by hand
.orig    the file as it was before patch touched it

Het woord hunk is degene om je eigen te maken, want elk gereedschap in deze familie rapporteert in hunks: diff produceert ze, patch past ze een voor een toe of weigert ze, en git laat je ze los klaarzetten.

Een hunk is een wijziging plus zijn omgeving. Die omgeving is de hele truc: door een paar ongewijzigde regels erboven en eronder mee te sturen, kan de patch nog steeds toegepast worden nadat het bestand verschoven is, want patch zoekt naar de context in plaats van de regelnummers te vertrouwen.

Naar boven

3. Een korte geschiedenis

diff kwam halverwege de jaren zeventig uit Bell Labs, en het probleem dat het oploste was geen versiebeheer. Het was dat computers traag waren en schijven klein, en dat het verschil tussen twee versies van een bestand bewaren veel goedkoper was dan allebei bewaren.

Het deel van het verhaal dat veranderde hoe software geschreven wordt kwam later. In 1985 bracht Larry Wall patch uit, en de combinatie van beide maakte van een diff een verzendmethode in plaats van een beschrijving. Software werd verspreid door diffs naar Usenet-nieuwsgroepen te sturen, waar iedereen ze op zijn eigen kopie van de broncode kon toepassen.

PeriodeMijlpaal
Halverwege jaren 70 diff verschijnt in Unix bij Bell Labs, met het algoritme gepubliceerd door Hunt en McIlroy
1985 Larry Wall brengt patch uit, en code gaat per post en nieuwsgroep reizen
Rond 1990 Het unified-formaat verschijnt, compact genoeg om te lezen en in een bericht te citeren
Vanaf 2005 Git en tijdgenoten bouwen op hetzelfde formaat voort in plaats van het te vervangen
Vandaag Elke code review die je ooit gezien hebt is een unified diff met een webpagina eromheen

De man-pagina van patch vermeldt de betrokkenen nog steeds, en een naam daarin verklaart het formaat dat je dagelijks leest:

$ man patch
AUTHORS
       Larry Wall wrote the original version of patch.  Paul Eggert removed
       patch's arbitrary limits; added support for binary files, setting
       file times, and deleting files; ...  Other contributors include
       Wayne Davison, who added unidiff support, and David MacKenzie, who
       added configuration and backup support.

"Unidiff support" is het unified-formaat. Het is het waarderen waard hoe volledig het gewonnen heeft: GitHub, GitLab, elk gereedschap voor code review en git diff zelf tonen allemaal het formaat dat aan een programma uit 1985 werd toegevoegd zodat patches in een e-mail zouden passen.

Leren een unified diff te lezen is niet een Unix-commando leren. Het is het formaat leren waarin elke code review ter wereld geschreven is.

Naar boven

4. Eenvoudige toepassingen

4.1 Het formaat dat je echt wilt: -u

De vlag -u (kort voor unified) toont een blok tekst met de verwijderingen en toevoegingen door elkaar, in plaats van twee losse lijsten:

$ diff -u old.php new.php
--- old.php     2026-08-23 17:15:36.275783415 +0200
+++ new.php     2026-08-23 17:15:36.276783411 +0200
@@ -1,7 +1,7 @@
 <?php
 $config = [
-    'host'  => 'localhost',
+    'host'  => 'db.internal',
     'user'  => 'joomla',
-    'debug' => false,
+    'debug' => true,
 ];
 echo "ready\n";

Lees het als een bestand met twee versies over elkaar heen:

Regel begint metBetekent
--- Het oorspronkelijke bestand, met zijn tijdstempel
+++ Het nieuwe bestand
@@ Het begin van een hunk (uitgelegd in 4.2)
Een spatie Context: ongewijzigd, in beide aanwezig
- Weggehaald uit het origineel
+ Toegevoegd in de nieuwe versie

Een gewijzigde regel verschijnt altijd twee keer, een keer als - en een keer als +. Er is geen markering voor "gewijzigd", want op dit niveau bestaat een regel wijzigen niet: je verwijdert de oude en voegt een nieuwe toe.

Er bestaat een ouder formaat -c (context) dat de twee versies in aparte blokken toont met ! voor wijzigingen. Je komt het tegen in oude documentatie. Unified zegt hetzelfde in ongeveer de helft van de ruimte, en daarom nam het het over.

4.2 De @@-regel lezen

Dit is het deel dat iedereen overslaat, en het kost dertig seconden om te leren. Hier is een bestand met drie losse wijzigingen:

$ diff -u big-old.txt big-new.txt
@@ -1,6 +1,6 @@
 line 1
 line 2
-line 3
+line 3 CHANGED
 line 4
 line 5
 line 6
@@ -8,6 +8,7 @@
 line 8
 line 9
 line 10
+line 10.5 INSERTED
 line 11
 line 12
 line 13
@@ -22,7 +23,7 @@
 line 22
 line 23
 line 24
-line 25
+line 25 CHANGED
 line 26

Elke @@-regel zegt waar de hunk in beide bestanden zit:

@@ -22,7 +23,7 @@
    |  |  |  |
    |  |  |  └─ ... and covers 7 lines there
    |  |  └─── in the NEW file it starts at line 23 ...
    |  └───── ... and covers 7 lines
    └─────── in the OLD file this hunk starts at line 22 ...

Kijk nu naar de drie hunks samen. De eerste begint in beide op regel 1. De tweede begint in beide op regel 8, maar beslaat 6 regels in het oude bestand en 7 in het nieuwe, omdat er een regel is ingevoegd. Bij de derde hunk zijn de bestanden uit elkaar gelopen: hij begint op regel 22 in het oude bestand en op regel 23 in het nieuwe. Dat ene getal is het lopende saldo van alles wat erboven is toegevoegd en weggehaald.

Je kunt instellen hoeveel context er meekomt, en dat verandert hoe de hunks gegroepeerd worden:

$ diff -U1 big-old.txt big-new.txt | grep -c '^@@'
3
$ diff -U5 big-old.txt big-new.txt | grep -c '^@@'
2

Met vijf regels context liggen twee van de wijzigingen dicht genoeg bij elkaar dat hun omgevingen overlappen, dus voegt diff ze samen tot een hunk. De standaard is drie, en dat is een goed compromis en de reden dat vrijwel elke patch die je ziet precies drie ongewijzigde regels rond elke wijziging heeft.

4.3 Naast elkaar

Om te lezen in plaats van om te patchen zet -y de twee bestanden in kolommen:

$ diff -y --width=68 old.php new.php
<?php                           <?php
$config = [                     $config = [
    'host'  => 'localhost',   | 	    'host'  => 'db.internal',
    'user'  => 'joomla',            'user'  => 'joomla',
    'debug' => false,         | 	    'debug' => true,
];                              ];
echo "ready\n";                 echo "ready\n";

De markering in de middelste kolom vertelt je wat er gebeurde: | gewijzigd, < alleen in het linkerbestand, > alleen in het rechter. Voeg bij een lang bestand --suppress-common-lines toe zodat je alleen de wijzigingen ziet:

$ diff -y --suppress-common-lines --width=60 old.php new.php
    'host'  => 'localhost',  | 	    'host'  => 'db.internal'
    'debug' => false,        | 	    'debug' => true,

4.4 Kleur

GNU diff kan zijn eigen uitvoer kleuren, en dat maakt een unified diff veel makkelijker te scannen:

$ diff --color=auto -u old.php new.php

auto is de waarde die je wilt: kleur als de uitvoer een terminal is, en platte tekst als hij doorgesluisd of omgeleid wordt. Dat doet ertoe, want een patchbestand met kleurcodes erin is geen patchbestand meer. --color=always bestaat om naar een pager te sturen, en is overal elders de verkeerde keuze.

Een alias maakt het blijvend:

alias diff='diff --color=auto -u'
Naar boven

5. Gemiddelde toepassingen

5.1 Twee mappen vergelijken

Hier houdt diff op gereedschap voor programmeurs te zijn en wordt het gereedschap voor beheerders. -r (kort voor recursive) loopt twee bomen af:

$ diff -r v1 v2
Only in v2: added.txt
diff -r v1/changed.txt v2/changed.txt
1c1
< old
---
> new
diff -r v1/inc/deep.txt v2/inc/deep.txt
1c1
< x
---
> y
Only in v1: removed.txt

Op twee echte websites is die uitvoer duizenden regels lang. Wat je vrijwel altijd wilt is er -q bij (kort voor brief), dat meldt welke bestanden verschillen zonder de inhoud te tonen:

$ diff -rq v1 v2
Only in v2: added.txt
Files v1/changed.txt and v2/changed.txt differ
Files v1/inc/deep.txt and v2/inc/deep.txt differ
Only in v1: removed.txt

diff -rq is het nuttigste commando uit dit artikel voor iedereen die servers beheert. Het beantwoordt "wat is er verschillend tussen deze twee kopieën van de site?" in een enkele regel, en het is het eerste om te draaien als een uitrol misgegaan is, als je vermoedt dat een kernbestand aangepast is, of als je wilt weten wat een update werkelijk veranderd heeft.

Een optie verandert de betekenis. -N behandelt een ontbrekend bestand als een leeg bestand, dus "alleen in" wordt "verschilt":

$ diff -rqN v1 v2
Files v1/added.txt and v2/added.txt differ
Files v1/changed.txt and v2/changed.txt differ
Files v1/inc/deep.txt and v2/inc/deep.txt differ
Files v1/removed.txt and v2/removed.txt differ

Gebruik -N als je een patch maakt, want een patch moet bestanden kunnen aanmaken en verwijderen. Laat hem weg als je de uitvoer zelf leest, want "alleen in" is informatiever dan "verschilt".

Voordat je hier iets van vertrouwt: lees paragraaf 7.1. diff -rq vergelijkt de inhoud van bestanden en verder niets, en wat het weglaat doet er meer toe dan de meeste mensen verwachten.

5.2 Bestanden en mappen uitsluiten

Op een echte site verzuipt het vorige commando in ruis. Caches, logs en gegenereerde miniaturen verschillen voortdurend en zeggen je niets:

$ diff -rq site-a site-b
Files site-a/cache/x.tmp and site-b/cache/x.tmp differ
Files site-a/index.php and site-b/index.php differ
Files site-a/logs/error.log and site-b/logs/error.log differ

Alleen de middelste regel doet ertoe. -x (kort voor exclude) neemt een patroon in shell-stijl en mag herhaald worden:

$ diff -rq -x cache -x '*.log' site-a site-b
Files site-a/index.php and site-b/index.php differ

Zodra de lijst groeit, zet je hem in een bestand en geef je dat mee met -X. Dat bestand hoort in versiebeheer, naast het script dat de vergelijking draait:

$ cat skip.txt
cache
logs
tmp
*.log

$ diff -rq -X skip.txt site-a site-b
Files site-a/index.php and site-b/index.php differ

Een regel pakt iedereen een keer: het patroon wordt vergeleken met de bestandsnaam, niet met het pad. Een pad met een schuine streep erin komt stilzwijgend met niets overeen:

$ diff -rq -x 'logs/*' site-a site-b
Files site-a/cache/x.tmp and site-b/cache/x.tmp differ
Files site-a/index.php and site-b/index.php differ
Files site-a/logs/error.log and site-b/logs/error.log differ    <-- not excluded

Sluit logs uit, niet logs/* en niet site-a/logs. De kale naam is waar diff elke regel mee vergelijkt terwijl hij de boom afloopt, en een map op naam uitsluiten slaat alles erin over.

5.3 Negeren wat je niet interesseert

Standaard telt elke byte mee, ook witruimte:

$ diff ws1.txt ws2.txt
1,2c1,2
< hello world
< second line
---
> hello  world
> second line   

De ene regel kreeg er een spatie in het midden bij, de andere spaties aan het eind, en allebei worden ze gemeld. Vier vlaggen zetten dat uit, van mildst naar breedst:

VlagNegeert
-b Verschillen in de hoeveelheid witruimte
-w Alle witruimte, overal
-B Regels die volledig leeg zijn
-i Hoofdletters en kleine letters

Er is ook -I, dat een reguliere expressie neemt en elke wijziging negeert op een regel die eraan voldoet. Zo vergelijk je twee gegenereerde rapporten zonder dat het tijdstempel bovenaan elk bestand anders laat lijken:

$ diff r1.txt r2.txt
1c1
< Generated: 2026-08-23
---
> Generated: 2026-08-24
$ echo $?
1

$ diff -I '^Generated:' r1.txt r2.txt
$ echo $?
0

Gebruik deze om te onderzoeken, niet om te beslissen. Een witruimtewijziging is onzichtbaar voor -w en zeer zichtbaar voor Python, YAML, en alles wat een here-document leest.

5.4 De valkuil van regeleindes

Af en toe toont een bestand dat "niemand aangeraakt heeft" elke regel als gewijzigd. Dit zijn vrijwel altijd de regeleindes, en het is het meest voorkomende loze alarm in webwerk, want een bestand dat op Windows bewerkt en via FTP geüpload is komt terug met een carriage return op elke regel.

$ diff crlf.txt lf.txt | cat -A
1,2c1,2$
< line one^M$
< line two^M$
---$
> line one$
> line two$

De ^M aan het eind van elke regel is de carriage return, zichtbaar gemaakt door cat -A. Zonder die truc zien beide kanten er identiek uit en lijkt de diff onzinnig. Twee commando's bevestigen het en een vlag negeert het:

$ file crlf.txt lf.txt
crlf.txt: ASCII text, with CRLF line terminators
lf.txt:   ASCII text

$ diff --strip-trailing-cr crlf.txt lf.txt ; echo $?
0

Het bestand echt repareren is beter dan het verschil voor altijd negeren. dos2unix doet dat, en sed -i 's/\r$//' bestand ook op een machine die dat programma niet heeft.

5.5 Als het geen tekst is

diff geeft het op bij binaire bestanden, en zegt dat ook:

$ diff bin1.dat bin2.dat
Binary files bin1.dat and bin2.dat differ

Dat is meestal alles wat je nodig hebt, en de afsluitcode werkt gewoon. Wil je details, dan is cmp het juiste gereedschap: dat vergelijkt byte voor byte en vertelt je waar het eerste verschil zit.

$ cmp bin1.dat bin2.dat
bin1.dat bin2.dat differ: byte 25, line 1

$ cmp -l bin1.dat bin2.dat | head -3
 25  60 300
 26 155  72
 41  50  30

cmp -l somt elke afwijkende byte op met zijn positie en de twee waarden in octaal. Voor een kale ja-of-nee op grote bestanden zijn cmp -s (stil) en diff -q even snel, want beide stoppen bij het eerste verschil; op twee identieke bestanden van 200 MB kostten ze hier 0,059s en 0,062s. Kies cmp als je wilt weten waar het verschil zit, en diff als je wilt weten wat het zegt.

Als je echt naar de bytes moet kijken, maak er dan eerst tekst van en vergelijk die. xxd maakt een hexdump met een regel per zestien bytes, en dat is precies de vorm die een regelgebaseerd gereedschap wil:

$ diff -u <(xxd h1.bin) <(xxd h2.bin)
@@ -1,4 +1,4 @@
 00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............
-00000010: 0300 3e00 0100 0000 306d 0000 0000 0000  ..>.....0m......
-00000020: 4000 0000 0000 0000 2824 0200 0000 0000  @.......($......
+00000010: 0300 3e00 0100 0000 c03a 0000 0000 0000  ..>......:......
+00000020: 4000 0000 0000 0000 1892 0000 0000 0000  @...............

Dit maakt binaire formaten niet leesbaar, maar het maakt van "deze twee bestanden verschillen ergens" iets als "deze twee bestanden verschillen op positie 0x10 en 0x20", en dat is vaak genoeg om een versienummer, een tijdstempel of een headerveld te herkennen.

5.6 Dingen vergelijken die geen bestanden zijn

Bash laat je de uitvoer van een commando aanbieden aan alles dat een bestandsnaam verwacht, met <(...). Dat haalt de tijdelijke bestanden weg uit een hele categorie controles:

$ diff <(printf 'apache2\nmysql\nphp\n') <(printf 'apache2\nnginx\nphp\n')
2c2
< mysql
---
> nginx

Hetzelfde idee beantwoordt echte vragen over twee servers:

$ diff <(ssh web1 'dpkg -l | sort') <(ssh web2 'dpkg -l | sort')
$ diff <(ssh web1 'php -m | sort') <(ssh web2 'php -m | sort')
$ diff <(cd /var/www/live && find . | sort) <(cd /var/www/staging && find . | sort)

Sorteer beide kanten. diff vergelijkt op volgorde, dus twee identieke pakketlijsten in een andere volgorde leveren pagina's ruis op. Die ene gewoonte maakt deze techniek bruikbaar.

Er is een ruw randje. Omdat de "bestanden" anonieme pipes zijn, noemt de header ze naar bestandsdescriptors, en dat zegt je niets:

$ diff -u <(echo a) <(echo b) | head -2
--- /dev/fd/63	2026-08-23 18:33:16.048005790 +0200
+++ /dev/fd/62	2026-08-23 18:33:16.048005790 +0200

--label vervangt ze, een keer per kant, op volgorde:

$ diff -u --label production --label staging <(echo a) <(echo b) | head -2
--- production
+++ staging

Gebruik het zodra de uitvoer door iemand anders gelezen wordt of in een ticket geplakt. Een diff waarvan de twee kanten production en staging heten heeft geen uitleg nodig; een waarvan de kanten /dev/fd/63 en /dev/fd/62 heten heeft een alinea nodig.

5.7 Normaliseer voordat je vergelijkt

De vorige paragraaf eindigde op een gewoonte die het waard is om als regel op te schrijven: de beste vergelijking is meestal geen vergelijking van de ruwe invoer. Twee kanten kunnen dezelfde informatie in een andere volgorde, een andere opmaak of een andere codering bevatten, en een regelgebaseerd gereedschap meldt dat allemaal als wijziging.

Het patroon is altijd hetzelfde:

raw input  -->  normalise  -->  compare

Sorteer, en zet de taalinstelling vast. Beide kanten sorteren is de voor de hand liggende helft. De helft die vrijwel niemand noemt is dat de sorteervolgorde zelf van de taalinstelling afhangt, dus dezelfde gegevens gesorteerd op twee machines met verschillende taalinstellingen komen niet overeen:

$ cat loc.txt
apple
Banana
cherry

$ LC_ALL=en_US.UTF-8 sort loc.txt
apple Banana cherry

$ LC_ALL=C sort loc.txt
Banana apple cherry           <-- capitals first, by byte value

Vergelijk die twee volgordes met elkaar en je krijgt verschillen op elke regel, uit identieke invoer. Als de twee kanten van twee verschillende servers komen, is dat precies wat er gebeurt. Zet de taalinstelling aan beide kanten vast en het probleem verdwijnt:

$ diff <(LC_ALL=C sort list1.txt) <(LC_ALL=C sort list2.txt)
$ diff <(ssh web1 'LC_ALL=C dpkg-query -W | sort') \
       <(ssh web2 'LC_ALL=C dpkg-query -W | sort')

LC_ALL=C geeft een kale bytevolgorde die overal hetzelfde is. Het is niet de volgorde die een mens zou kiezen, en dat maakt niet uit, want niemand leest het: de twee kanten hoeven het alleen met elkaar eens te zijn.

Gebruik gereedschap dat het formaat kent, als dat bestaat. Opnieuw opgemaakte JSON is voor diff een volledige herschrijving, ook als er niets veranderd is. jq -S drukt het opnieuw af met gesorteerde sleutels, zodat alleen echte verschillen overblijven:

$ diff -u --label old --label new <(jq -S . old.json) <(jq -S . new.json)

Hetzelfde idee dekt de meeste klachten van het type "waarom is alles anders?": maak de XML netjes op, sorteer de databasedump, haal de tijdstempelregel weg met -I, maak de regeleindes gelijk. Doe het normaliseren een keer, in het commando, en de diff wordt leesbaar.

Naar boven

6. Gevorderde toepassingen

6.1 Een patch maken

Een patch is niet meer dan een unified diff die in een bestand is opgeslagen. Twee regels maken het verschil tussen een patch die toegepast kan worden en een die dat niet kan.

Gebruik -u, en neem de mapnaam mee. Draai de vergelijking een niveau boven de boom, zodat de paden in de patch een voorvoegsel hebben dat weggehaald kan worden:

$ diff -u site-a/components/helper.php site-b/components/helper.php > fix.patch
$ cat fix.patch
--- site-a/components/helper.php   2026-08-23 17:16:58.021471057 +0200
+++ site-b/components/helper.php   2026-08-23 17:16:58.022471053 +0200
@@ -1,5 +1,5 @@
 <?php
 function getLimit()
 {
-    return 20;
+    return 50;
 }

Voor een hele boom voeg je -r en -N toe, zodat nieuwe en verwijderde bestanden meekomen:

$ diff -ruN site-a site-b > release.patch

6.2 Toepassen: de -p-niveaus

Hier lopen de meeste pogingen stuk, en de reden is eenvoudig zodra je hem ziet. De patch bevat een pad. patch moet dat pad omzetten naar een bestand op jouw schijf, en -p vertelt hem hoeveel voorste mapniveaus hij moet weggooien.

Path in the patch:   site-a/components/helper.php

-p0  looks for      site-a/components/helper.php
-p1  looks for      components/helper.php
-p2  looks for      helper.php

De juiste waarde hangt dus volledig af van waar jij staat. Vanuit een kopie van de site staat het voorvoegsel site-a/ niet op schijf en moet het weggehaald worden:

$ cd /var/www/site
$ patch -p0 < ../fix.patch
can't find file to patch at input line 3
Perhaps you used the wrong -p or --strip option?

$ patch -p1 < ../fix.patch
patching file components/helper.php

-p1 is meestal het antwoord, want patches worden per afspraak een niveau boven de boom gemaakt, en het is wat git maakt en verwacht. Bij twijfel: lees de ----regel van de patch en tel.

6.3 Proefdraaien, backups en ongedaan maken

Drie gewoontes maken van patch iets dat je in de hand hebt in plaats van iets waarvan je hoopt dat het werkt.

Probeer het eerst. --dry-run doet alles behalve schrijven:

$ patch -p1 --dry-run < ../fix.patch
checking file components/helper.php
$ echo $?
0

Let op de formulering: "checking file" bij een proefrun, "patching file" als het echt is. Zo zie je in een log welke van de twee gebeurd is.

Bewaar het origineel. Standaard bewaart patch niets. -b zet de vorige versie naast het bestand:

$ patch -p1 -b < ../fix.patch
$ ls -A components/
helper.php  helper.php.orig

Weet hoe je het terugdraait. Een patch is omkeerbaar, want hij beschrijft beide kanten van elke wijziging. -R draait hem achterstevoren:

$ patch -p1 -R < ../fix.patch
patching file components/helper.php

Dat is het echte antwoord op "de correctie maakte het erger". Je hebt het oude bestand niet nodig; je hebt de patch nodig die het nieuwe opleverde.

6.4 Als het niet schoon toegepast wordt

Echte bestanden schuiven op. patch is daarvoor gebouwd, en hij vertelt je precies hoe hard hij heeft moeten werken.

Offset betekent dat hij de context ergens anders in het bestand vond. Hier waren er twee commentaarregels bovenaan toegevoegd:

$ patch -p1 < ../fix.patch
patching file components/helper.php
Hunk #1 succeeded at 3 with fuzz 1 (offset 2 lines).
$ echo $?
0

Een offset is normaal en ongevaarlijk. patch zocht naar buiten toe vanaf het regelnummer in de hunk-header, vond de omringende regels twee regels lager, en paste de wijziging daar toe. Precies hiervoor bestaan de contextregels.

Weigering betekent dat hij het opgaf. Dezelfde patch twee keer toepassen is de gebruikelijke oorzaak:

$ patch -p1 < ../fix.patch
patching file components/helper.php
Reversed (or previously applied) patch detected!  Assume -R? [n]
Apply anyway? [n]
Skipping patch.
1 out of 1 hunk ignored -- saving rejects to file components/helper.php.rej
$ echo $?
1

Twee dingen om op te merken. patch herkende dat het bestand het resultaat al bevatte en bood aan het om te keren, en dat is een oprecht knap stuk techniek uit 1985. En hij schreef een .rej-bestand met de hunks die hij niet kon plaatsen:

$ cat components/helper.php.rej
--- components/helper.php
+++ components/helper.php
@@ -1,5 +1,5 @@
 <?php
 function getLimit()
 {
-    return 20;
+    return 50;
 }

Een .rej-bestand is geen foutmelding. Het is het werk dat voor jou overblijft. Zoek na elke patch die weigeringen meldt de boom af voordat je de klus afgerond noemt:

$ find . -name '*.rej' -o -name '*.orig'

Tussen "schoon toegepast" en "geweigerd" zit een derde uitkomst die een eigen paragraaf verdient, want hij slaagt en hij zou je zorgen moeten baren. Paragraaf 7.2.

6.5 Driewegs, en waar de conflictmarkeringen van git vandaan komen

diff3 vergelijkt drie bestanden: dat van jou, dat van hen, en de gemeenschappelijke voorouder waar beide van uitgingen. Dat is de vorm van elke samenvoeging.

$ diff3 -m mine.txt base.txt theirs.txt
a
<<<<<<< mine.txt
MINE
||||||| base.txt
BASE
=======
THEIRS
>>>>>>> theirs.txt
c

Iedereen die git gebruikt heeft herkent die uitvoer meteen. Die markeringen zijn geen uitvinding van git: git gebruikt dezelfde driewegslogica en drukt hetzelfde formaat af. Het middelste deel, tussen ||||||| en =======, is hoe de regel eruitzag voordat een van beide kanten hem aanraakte, en dat is het stuk waarvan mensen het vaakst wensen dat ze het hadden bij het oplossen van een conflict.

De volgorde van de argumenten is het onthouden waard, want hij is niet alfabetisch en niet vanzelfsprekend: mine, base, theirs. De gemeenschappelijke voorouder staat in het midden.

6.6 Een patch toepassen die je niet zelf schreef

patch schrijft bestanden, dus een patch van een onbekende bron is onvertrouwde invoer. De traditionele zorg is dat een patch een pad zou kunnen bevatten dat buiten je boom wijst. Op GNU patch 2.7.6 is dat afgevangen, en het is de moeite waard om de weigeringen te zien in plaats van het aan te nemen.

Een patch die een absoluut pad noemt wordt met naam en toenaam geweigerd:

$ patch -p0 < ../abs.patch
Ignoring potentially dangerous file name /home/peter/sec/outside.txt
can't find file to patch at input line 3

$ cat ../outside.txt
ORIGINAL                      <-- untouched

Een patch die met ../ naar buiten klimt wordt ook geweigerd, zowel bij -p0 als bij -p1:

$ patch -p1 < ../evil.patch
can't find file to patch at input line 3
Perhaps you used the wrong -p or --strip option?

$ cat ../outside.txt
ORIGINAL                      <-- still untouched

De ontsnappingsroutes zijn dus dicht. Wat niet dicht is, is alles wat de patch binnen je boom gewoon mag doen, en dat is het deel dat er werkelijk toe doet:

  • Lees hem eerst. Een patch is platte tekst. less fix.patch toont je elke regel die hij gaat wijzigen, en dat is meer dan je van de meeste dingen die je installeert kunt zeggen.
  • Kijk welke bestanden hij aanraakt voordat je naar de inhoud kijkt: grep '^+++' fix.patch.
  • Eerst proefdraaien, dan toepassen. patch -p1 --dry-run kost niets.
  • Werk op een kopie als de boom productie is, en vergelijk de kopie daarna terug.

Die laatste is de algemene regel waar dit hele artikel steeds op uitkomt: het gereedschap dat een wijziging toepast en het gereedschap dat hem controleert zijn hetzelfde gereedschap, in tegengestelde richting gebruikt.

Naar boven

7. Iets wat de meeste gebruikers niet weten

7.1 diff -rq vergelijkt inhoud en verder niets

Paragraaf 5.1 noemde diff -rq het nuttigste commando uit dit artikel. Dat is het ook, en het zal je ook vertellen dat twee mappen overeenkomen terwijl ze wezenlijk verschillen, want het vergelijkt de inhoud van bestanden en verder niets.

Hier zijn twee mappen. Een bestand heeft in beide dezelfde inhoud, maar de permissies en het tijdstempel verschillen:

$ ls -l p1/f.txt p2/f.txt
-rw-r--r-- Jan  1  2020  p1/f.txt
-rwxrwxrwx Aug 23 17:16  p2/f.txt

$ diff -rq p1 p2
$ echo $?
0                                  <-- "identical"

Een bestand ging van 644 naar 777, schrijfbaar voor iedereen, en diff meldde helemaal niets. Op een website is dat het verschil tussen een dichtgezet bestand en een dat iedereen kan herschrijven, en een restore die zo gecontroleerd is komt met de verkeerde permissies door de test.

Bij symbolische links is het erger, want diff volgt ze. Hier is s1/ptr een symlink en s2/ptr een echt bestand met dezelfde inhoud:

$ ls -l s1/ptr s2/ptr
lrwxrwxrwx  s1/ptr -> real.txt
-rw-rw-r--  s2/ptr

$ diff -rq s1 s2
$ echo $?
0                                  <-- still "identical"

De structuur was vernield en de controle kwam erdoor. Voor elke site met de standaardindeling current -> releases/2026-08 bij het uitrollen is dat precies de storing die ertoe zou doen, en precies degene die dit commando verbergt.

Moderne GNU diff heeft er een vlag voor:

$ diff -rq --no-dereference s1 s2
File s1/ptr is a symbolic link while file s2/ptr is a regular file
$ echo $?
1

De eerlijke samenvatting van wat diff -rq controleert is dus kort:

EigenschapGecontroleerd door diff -rq?
Inhoud van bestanden Ja. Dat is de hele taak.
Bestanden aanwezig of afwezig Ja, gemeld als "Only in"
Permissies Nee
Eigenaar en groep Nee
Tijdstempels Nee
Symlink tegenover echt bestand Nee, tenzij je --no-dereference toevoegt
Lege mappen Gemeld als "Only in", maar nooit als een verschil in inhoud

Paragraaf 7.5 geeft het commando dat de rest afdekt.

7.2 Fuzz past je patch toe op code die verder is gegaan

Paragraaf 6.4 liet een patch luid falen zien. Dit is de storing die dat niet doet.

patch vindt een hunk door zijn contextregels te matchen. Als hij ze niet allemaal kan matchen, stopt hij niet: hij probeert het opnieuw terwijl hij de buitenste contextregels negeert. Het naslagwerk noemt de grens onomwonden:

$ man patch
       First patch looks for a place where all lines of the context match.
       If no such place is found, and it's a context diff, and the maximum
       fuzz factor is set to 1 or more, then another scan takes place
       ignoring the first and last line of context.  If that fails, and the
       maximum fuzz factor is set to 2 or more, the first two and last two
       lines of context are ignored, and another scan is made.
       (The default maximum fuzz factor is 2.)

Kijk wat dat toelaat. Hier is de functie waartegen de patch geschreven was door iemand anders hernoemd, dus de context komt niet meer overeen:

$ patch -p1 < ../fix.patch
patching file components/helper.php
Hunk #1 succeeded at 1 with fuzz 2.
$ echo $?
0

Afsluitcode 0. Een script ziet succes. Maar patch heeft zojuist een wijziging geschreven in een bestand waarvan de omringende code niet de code is waar de auteur van de patch naar keek, want hij gooide aan beide kanten twee regels context weg om een match te vinden.

Soms is dat precies goed en bespaart het je een middag. Soms zet het een correctie in een functie die niet langer de bedoelde functie is. Het punt is dat het je niet gevraagd werd.

patch die slaagt is niet hetzelfde als patch die gelijk heeft. Elke regel waarin fuzz voorkomt betekent dat het bestand opgeschoven was en dat patch gegokt heeft; lees wat hij gedaan heeft voordat je verdergaat.

Zet het gokken uit voor alles wat onbeheerd draait. -F0 zet de maximale fuzz op nul, zodat een hunk of precies past of faalt:

$ patch -p1 -F0 < ../fix.patch
patching file components/helper.php
Hunk #1 FAILED at 1.
1 out of 1 hunk FAILED -- saving rejects to file components/helper.php.rej
$ echo $?
1

Dat is het gedrag dat je in een uitrolscript wilt: een duidelijke storing waar je op kunt handelen, in plaats van een stil succes dat je later moet ontdekken.

7.3 diff ziet geen verplaatst blok

diff vergelijkt niet regel 1 met regel 1 en regel 2 met regel 2. Als dat zo was, zou een regel bovenaan invoegen elke regel eronder als gewijzigd melden. In plaats daarvan zoekt hij de langste reeks regels die de twee bestanden gemeen hebben en behandelt hij de rest als toegevoegd of verwijderd, en daarom meldt het invoegen van een regel precies een toegevoegde regel.

Die uitlijning maakt diff bruikbaar, en ze heeft een blinde vlek: het begrip verplaatsing bestaat niet. Neem een bestand en verwissel de volgorde van twee functies, zonder er iets in te veranderen:

$ diff -u mv1.txt mv2.txt
@@ -1,6 +1,6 @@
 header
-function A
-body A
 function B
 body B
+function A
+body A
 footer

Er is niets toegevoegd en niets weggehaald, maar de diff toont twee verwijderingen en twee toevoegingen. Voor het gereedschap is er geen verschil tussen "dit blok is verplaatst" en "dit blok is hier weggehaald en daar is een identiek blok geschreven".

Dat doet ertoe bij review. Een patch die een groot bestand herordent ziet er enorm uit en leest alsof alles herschreven is, en de echt nieuwe regels liggen begraven tussen de verplaatste. Drie uitwegen, op volgorde van hoeveel je moet installeren:

  • Meng verplaatsingen niet met bewerkingen. Ga je een bestand herordenen, doe dat dan in een wijziging die alleen herordent, zodat de volgende diff leesbaar is.
  • Vraag om een andere uitlijning. -d (--minimal) vraagt diff om extra moeite te doen om een kleinere set wijzigingen te vinden. Het is trager, en op eenvoudige bestanden geeft het meestal hetzelfde antwoord, maar bij een slecht uitgelijnde diff is het een poging waard.
  • Gebruik gereedschap dat verplaatsingen volgt bij het reviewen, zoals git diff --color-moved, dat verplaatste blokken in een andere kleur zet in plaats van te doen alsof ze nieuw zijn.

Het bredere punt is dat een diff een geldige beschrijving is van hoe je van A naar B komt, niet de enige en niet per se degene die een mens geschreven zou hebben. Twee gereedschappen kunnen voor hetzelfde paar bestanden verschillend ogende uitvoer geven en allebei gelijk hebben.

7.4 Een diff is een programma, geen rapport

Al het andere in dit artikel volgt uit een idee dat makkelijk te missen is: de uitvoer van diff is met opzet machineleesbaar, en de machine die hem leest is patch.

Daarom is het formaat zo pietluttig. De spatie vooraan een contextregel is geen versiering, het is de markering voor "ongewijzigd", en hem weghalen breekt de patch. De regeltellingen in de @@-header moeten overeenkomen met het aantal regels dat volgt. Een editor die spaties aan het eind weghaalt kan een patchbestand ongeldig maken. Een mailprogramma dat lange regels afbreekt ook, en daarom werden patches van oudsher als bijlage verstuurd en heeft elk project een pagina over hoe je ze niet verminkt.

Het verklaart ook de twee regels die mensen verrassen:

  • Bewerk nooit een patchbestand om het te "repareren", tenzij je bereid bent de regeltellingen in elke hunk-header die je aanraakt mee te corrigeren.
  • Laat nooit kleur in een patch komen. --color=always > fix.patch levert een bestand op dat er perfect uitziet en dat patch niet kan lezen, want elke regel begint nu met een escape-reeks in plaats van met een spatie, een plus of een min.

7.5 Een teruggezette backup goed controleren

Zet 7.1 naast de rest en je krijgt de controle die na een restore of een verhuizing werkelijk de moeite waard is. Hij bestaat uit twee helften, want geen enkel commando dekt allebei.

Eerst de inhoud:

$ diff -rq --no-dereference /var/www/live /mnt/restore/live

Daarna alles wat diff negeert, door de metagegevens in tekst om te zetten en die te vergelijken:

$ diff \
    <(cd /var/www/live   && find . -printf '%p %m %u:%g\n' | sort) \
    <(cd /mnt/restore/live && find . -printf '%p %m %u:%g\n' | sort)
3c3
< ./f.txt 644 www-data:www-data
---
> ./f.txt 777 www-data:www-data

De opmaakreeks van find -printf doet het werk: %p het pad, %m de permissiebits in octaal, %u:%g de eigenaar en de groep. Beide kanten sorteren maakt de vergelijking stabiel. Het resultaat is een regel per bestand waarvan de modus of het eigendom veranderde, en dat is precies wat de inhoudsvergelijking niet kan zien.

Ditzelfde paar commando's beantwoordt een tweede vraag die vaker opkomt dan iemand zou willen: is er iets op deze site gewijzigd dat niet gewijzigd had mogen worden? Download een schone kopie van dezelfde versie en vergelijk die met de draaiende site. Elk bestand dat verschilt is of je eigen aanpassing, of iets waar je heel goed naar moet kijken.

Trek het nog een stap breder en je hebt de nuttigste gewoonte uit dit artikel. Leg de toestand als tekst vast voordat je iets verandert, leg hem daarna opnieuw vast, en vergelijk de twee. Het kost twee commando's en het beantwoordt precies "wat heeft dat nu eigenlijk gedaan?":

$ before=$(mktemp) after=$(mktemp)
$ trap 'rm -f "$before" "$after"' EXIT

$ dpkg-query -W | LC_ALL=C sort > "$before"
$ sudo apt upgrade
$ dpkg-query -W | LC_ALL=C sort > "$after"

$ diff -u --label before --label after "$before" "$after"

De trap-regel maakt het veilig om in een script te zetten: de tijdelijke bestanden worden ook opgeruimd als het vroegtijdig stopt. Alles wat tekst afdrukt werkt als momentopname, en op Linux is dat vrijwel alles:

find /etc -type f | LC_ALL=C sort        which configuration files exist
systemctl list-unit-files --state=enabled   which services will start
ss -tulpn                               which ports are listening
php -m | LC_ALL=C sort                  which PHP extensions are loaded
crontab -l ; ls /etc/cron.d             what is scheduled

Draai een van die commando's voor een verhuizing en daarna nog eens, en de verschillen tussen de twee servers zijn geen kwestie van geheugen meer.

7.6 Weten waar diff ophoudt

Een deel van vakmanschap is weten wanneer een gereedschap het verkeerde is.

NodigGebruikWaarom
Gestructureerde gegevens vergelijken jq -S en dan diff, of gereedschap dat het formaat kent Opnieuw opgemaakte JSON is voor een regelgebaseerd gereedschap een volledige herschrijving
Woord voor woord vergelijken git diff --word-diff, wdiff diff kent geen eenheid kleiner dan een regel
Wijzigingen door de tijd heen volgen git diff vergelijkt twee toestanden; hij onthoudt niets
Een database vergelijken Een dump, gesorteerd, en dan diff De volgorde van rijen ligt niet vast, dus zonder sorteren is de diff betekenisloos
Synchroniseren in plaats van rapporteren rsync -n Een proefrun van rsync toont wat er zou veranderen, en kan het daarna doen
Twee gesorteerde lijsten vergelijken comm Geeft je drie kolommen: alleen-in-A, alleen-in-B, en beide

Die laatste rij wordt te weinig gebruikt. Heb je twee lijsten met namen, dan beantwoorden comm -13 en comm -23 de vraag "wat staat alleen aan deze kant?" directer dan een diff lezen.

Naar boven

8. Best practices

  • Neem diff -u als standaard. Het is compact, het is wat elk gereedschap voor code review je toont, en het is het enige formaat dat patch en git allebei zonder discussie aannemen. Maak er desnoods een alias van.
  • Leer de @@-regel een keer. Oude begin en lengte, nieuwe begin en lengte. Dertig seconden lezen maakt van elke patch en elke pull request een zin in plaats van een muur symbolen.
  • Gebruik diff -rq als eerste zet bij elke "wat is er veranderd?"-vraag, en voeg --no-dereference toe zodat symlinks als symlinks vergeleken worden.
  • Sluit de ruis uit met -x of -X, en onthoud dat het patroon met namen vergeleken wordt en niet met paden.
  • Controleer de metagegevens apart. Inhoud is maar de helft van een restore. De vergelijking met find -printf uit paragraaf 7.5 vangt de wijzigingen in permissies en eigendom op waar diff blind voor is.
  • Sorteer beide kanten, en zet de taalinstelling vast. Pakketlijsten en mapoverzichten komen in willekeurige volgorde terug, en de volgorde zelf hangt af van LANG. LC_ALL=C sort aan beide kanten.
  • Test op afsluitcode 2, niet alleen op "niet nul". 0 is identiek, 1 is verschillend, 2 is dat diff zelf faalde. Een typefout in een pad als "de bestanden verschillen" behandelen levert zelfverzekerde onzin op.
  • Laat een patch altijd eerst proefdraaien. patch -p1 --dry-run kost een seconde en vertelt je of het -p-niveau klopt voordat er iets geschreven wordt.
  • Gebruik -b als je met de hand patcht, zodat de vorige versie naast het bestand ligt als je hem nodig hebt.
  • Lees elke regel waarin fuzz voorkomt. Een offset is prima. Fuzz betekent dat het bestand opgeschoven was en dat patch gegokt heeft, en hij eindigt nog steeds met 0.
  • Zet -F0 in alles wat geautomatiseerd is. Een gefaalde hunk die je ziet is beter dan een vage hunk die je niet ziet.
  • Zoek naar .rej en .orig voordat je de overwinning uitroept. find . -name '*.rej' is de laatste stap van elke patch die niet perfect schoon liep.
  • Gebruik het als test. jouw-commando > actual.txt gevolgd door diff -u expected.txt actual.txt is een complete regressietest: afsluitcode 0 is geslaagd, 1 is gezakt, en de uitvoer bij het zakken is al een leesbare uitleg van wat er veranderde. De meeste frameworks voor snapshottesten zijn dit met extra stappen.
  • Leid gekleurde uitvoer nooit naar een patchbestand. Gebruik --color=auto en niets anders.
$ man 1 diff              # every option, grouped by what it ignores
$ man 1 patch             # the -p levels, fuzz, rejects and backups
$ info diff               # the full GNU manual, much deeper than the man page
$ man 1 cmp               # byte-level comparison
$ man 1 diff3             # three-way, and the origin of conflict markers
Naar boven

9. Veelgemaakte fouten

9.1 Mythe versus werkelijkheid

MytheWerkelijkheid
"diff -rq bewees dat de restore klopt." Het bewees dat de inhoud van de bestanden overeenkomt. Permissies, eigendom, tijdstempels en symlinkstructuur zijn allemaal niet gecontroleerd.
"patch eindigde met 0, dus de patch is goed toegepast." Hij is toegepast. Met fuzz kan hij toegepast zijn op code die niet meer overeenkomt met de context van de patch.
"Een afsluitcode ongelijk aan nul betekent dat de bestanden verschillen." 1 betekent dat ze verschillen. 2 betekent dat diff faalde, meestal een verkeerd pad.
"Het hele bestand is veranderd, dus iemand heeft het herschreven." Veel vaker zijn het de regeleindes. Controleer met file en cat -A voordat je iemand beschuldigt.
"-p1 is een magisch getal." Het is het aantal voorste padonderdelen dat weggehaald moet worden. Lees de ----regel en bepaal waar je staat.
"Een .rej-bestand betekent dat de patch mislukt is." Het betekent dat die hunks mislukt zijn. De rest is toegepast, dus de boom is nu half gepatcht.
"diff toont wat er veranderd is." Het toont een manier om van A naar B te komen. Verplaats een blok code en diff meldt het als een verwijdering plus een ongerelateerde toevoeging (paragraaf 7.3).
"Ik heb beide kanten gesorteerd, dus de vergelijking is eerlijk." De sorteervolgorde hangt af van de taalinstelling. Dezelfde lijst sorteert anders onder en_US.UTF-8 dan onder C, dus twee servers kunnen het oneens zijn over identieke gegevens.
"-x 'cache/*' sluit de cachemap uit." Uitsluitpatronen komen overeen met de bestandsnaam, niet met het pad. Schrijf -x cache; alles met een schuine streep erin komt met niets overeen.
"Unified diff is iets van git." Het werd rond 1990 aan Larry Walls patch toegevoegd, vijftien jaar voordat git bestond.

9.2 Andere valkuilen om te vermijden

  • Ongesorteerde uitvoer vergelijken. diff <(ssh a 'dpkg -l') <(ssh b 'dpkg -l') zonder sort aan beide kanten levert een diff van de volgorde op, niet van de inhoud.
  • De volgorde van de argumenten omdraaien. diff nieuw oud is een geldig commando dat een patch oplevert die jouw werk ongedaan maakt. Het eerste bestand is altijd het "van".
  • -N vergeten bij een patch voor een hele boom. Zonder die vlag worden bestanden die maar aan een kant bestaan als "Only in" genoemd en niet meegenomen, dus de patch kan ze stilzwijgend niet aanmaken.
  • Een patch vanuit de verkeerde map toepassen. Het -p-niveau en je werkmap zijn twee helften van dezelfde instelling. Krijg er een verkeerd en je krijgt "can't find file to patch", of erger: je patcht een bestand met dezelfde naam ergens anders.
  • -w gebruiken om een verschil te laten verdwijnen. Witruimte is betekenisvol in Python, YAML, Makefiles en here-documents. -w is om te onderzoeken, niet om te concluderen.
  • Een patchbestand met de hand bewerken. De regeltellingen in de @@-headers moeten kloppen met de regels die volgen, en niets waarschuwt je als dat niet zo is.
  • Een diff van een gegenereerd bestand vertrouwen. Geminificeerde assets, gecompileerde templates en cachebestanden verschillen voortdurend om redenen die niemand hoeft te zien. Sluit ze uit, of vergelijk de bronnen waar ze uit komen.
  • Een uitsluiting als pad schrijven. -x 'logs/*' en -x 'site/cache' komen allebei met niets overeen, stilzwijgend. Het patroon wordt getest tegen de kale naam van elke regel.
  • Twee servers vergelijken zonder de taalinstelling vast te zetten. Verschilt LANG ertussen, dan levert sort aan elke kant een andere volgorde op en is de diff louter ruis. LC_ALL=C sort aan beide kanten.
  • .orig-bestanden in een webroot laten staan. helper.php.orig wordt niet door PHP uitgevoerd, wat betekent dat de webserver hem als platte tekst kan uitleveren, inclusief alles wat erin staat. Ruim ze op.
Naar boven

10. Samenvatting

diff is vijftig jaar oud, zijn uitvoerformaat is de taal waarin elke code review geschreven is, en zijn waardevolste gebruik op een server heeft niets met programmeren te maken.

  • diff beschrijft hoe je van het eerste bestand het tweede maakt. De volgorde doet ertoe, en de uitvoer is net zo goed bedoeld voor patch als voor jou.
  • De afsluitcodes dragen de betekenis: 0 identiek, 1 verschillend, 2 er ging iets mis.
  • Gebruik -u. De header @@ -22,7 +23,7 @@ is oude begin en aantal, nieuwe begin en aantal, en het verschil tussen die twee getallen is alles wat er erboven is toegevoegd of weggehaald.
  • diff -rq beantwoordt "wat is er verschillend tussen deze twee kopieën van de site?" in een enkele regel, en --no-dereference maakt het eerlijk over symlinks.
  • Sluit de ruis uit met -x of -X, en onthoud dat het patroon namen matcht en geen paden.
  • Normaliseer voordat je vergelijkt: sorteer beide kanten, zet de taalinstelling vast met LC_ALL=C, en gebruik jq -S of iets vergelijkbaars voor gestructureerde gegevens.
  • diff kent geen verplaatst blok. Een bestand herordenen leest als een volledige herschrijving, en dat is het weten waard voordat je er een reviewt.
  • Witruimte en regeleindes zijn de twee meest voorkomende loze alarmen. file en cat -A herkennen ze; --strip-trailing-cr en -b omzeilen ze; het bestand repareren is beter.
  • Een patch is een unified diff in een bestand. -p vertelt patch hoeveel voorste mappen hij moet weghalen, en dat hangt af van waar jij staat.
  • Eerst --dry-run, -b om het origineel te bewaren, -R om terug te draaien. Een patch is per ontwerp omkeerbaar.
  • Een offset is normaal. Fuzz is een waarschuwing: het bestand was opgeschoven, patch negeerde wat context om het passend te maken, en hij eindigt nog steeds met 0. Gebruik -F0 waar niemand kijkt.
  • De conflictmarkeringen van git komen van diff3, en het middelste deel is de gemeenschappelijke voorouder.
  • Moderne patch weigert absolute paden en ontsnappingen met ../, dus het echte risico is wat een patch binnen je boom gewoon mag doen. Lees hem; het is platte tekst.
  • diff -rq controleert alleen de inhoud. Permissies, eigendom en symlinkstructuur hebben de vergelijking met find -printf nodig.

Dit is het spiekbriefje dat je wilt bewaren:

diff -u a b                    unified: the format everything else speaks
diff -y a b                    side by side, add --suppress-common-lines
diff --color=auto -u a b       colour on screen, plain when redirected
diff -rq DIR1 DIR2             WHICH files differ (the one to remember)
diff -rq --no-dereference ...  ... and treat symlinks as symlinks
diff -rqN DIR1 DIR2            count missing files as differences too
diff -rq -x cache -x '*.log'   exclude by NAME (never a path)
diff -rq -X skip.txt DIR1 DIR2 exclusion patterns from a file

-b ignore whitespace amount    -w ignore all whitespace
-B ignore blank lines          -i ignore case
-I REGEX ignore matching lines (timestamps, version stamps)
--strip-trailing-cr            ignore Windows line endings
-U N                           N lines of context (default 3)

exit 0 identical | 1 different | 2 diff itself failed

diff -u old new > fix.patch          make a patch
diff -ruN old/ new/ > release.patch  make a whole-tree patch
patch -p1 --dry-run < fix.patch      try it, change nothing
patch -p1 -b < fix.patch             apply, keeping .orig
patch -p1 -R < fix.patch             undo it
patch -p1 -F0 < fix.patch            no fuzz: exact match or fail
grep '^+++' fix.patch                which files does it touch?
find . -name '*.rej' -o -name '*.orig'   ALWAYS, after patching

cmp a b                        first differing byte
comm -23 a b                   lines only in the first (sorted input)
diff3 -m mine base theirs      three-way merge with conflict markers
diff <(cmd1 | sort) <(cmd2 | sort)    compare output, not files
diff -u --label old --label new ...   name the sides (pipes are /dev/fd/63)
diff <(LC_ALL=C sort a) <(LC_ALL=C sort b)   same order on every machine
diff -u <(jq -S . a.json) <(jq -S . b.json)  ignore key order
diff -u <(xxd a.bin) <(xxd b.bin)     byte differences, readably

# before and after: what did that change actually do?
cmd | LC_ALL=C sort > before ; ...do the thing... ; cmd | LC_ALL=C sort > after
diff -u --label before --label after before after

# verify a restore: contents, then everything diff ignores
diff -rq --no-dereference /var/www/live /mnt/restore/live
diff <(cd /var/www/live && find . -printf '%p %m %u:%g\n' | sort) \
     <(cd /mnt/restore/live && find . -printf '%p %m %u:%g\n' | sort)

Het gat tussen "de backup is gedraaid" en "de backup zet terug" is waar de meeste onaangename verrassingen wonen, en vergelijken wat er terugkwam met wat er stond is de goedkoopste manier om dat gat te dichten.

Naar boven
Linux commando: diff
Peter Martin
Peter Martin
Joomla Specialist

Peter is Joomla specialist en Linux admin voor snelle, veilige en schaalbare websites.

Gerelateerde artikelen