Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site https://www.yesik.it !

Diff.png

diff — L'utilitaire diff permet de déterminer les différences entre deux fichiers. C'est à dire l'ensemble des transformations permettant de passer de l'un à l'autre.

L'utilitaire diff est un outil standard des systèmes Unix-like. Son rôle est de comparer deux fichiers textes pour en faire apparaitre les différences. Plus formellement, diff produit un script permettant de transformer un des deux fichiers originaux pour obtenir le second.

Cette possibilité est exploitée par certains systèmes de gestion de versions comme SCCS pour ne stocker que les différences entre les versions successives d'un même fichier. De la même manière, plutôt que de transmettre à un collègue un fichier modifié, il est possible de juste lui envoyer un patch contenant les modifications que vous avez apportées.

Usage de base

Pour illustrer l'utilisation de base de diff, voici un exemple. J'ai dans mon dossier courant deux versions différentes d'un fichier makefile. L'original, appelé makefile.v1 et une version modifiée appelée makefile.v2:

# L'original
sh$ cat makefile.v1
#
# funky/src/Makefile
#
 
all: funky
 
funky: main.cc
	gcc -g -o $@ main.cc
 
clean:
	-rm funky
 
.PHONY: all funky
# Version modifiée
sh$ cat makefile.v2
#
# funky/Makefile
#
 
package = funky
version = 1.0
tarname = $(package)
distdir = $(tarname)-$(version)
 
all clean funky:
	cd src && $(MAKE) $@
 
dist: $(distdir).tar.gz
 
$(distdir).tar.gz: $(distdir)
	tar chof - $(distdir) | gzip -9 -c > $@
	rm -rf $(distdir)
 
$(distdir):
	mkdir $(distdir)
	mkdir $(distdir)/src
	cp Makefile $(distdir)
	cp src/Makefile $(distdir)
	cp src/main.cc $(distdir)
 
.PHONY: all clean dist

En appliquant diff pour déterminer les différences entre ces fichiers, j'obtiens le résultat suivant:

sh$ diff makefile.v1 makefile.v2
2c2
< # funky/src/Makefile
---
> # funky/Makefile
5c5,8
< all: funky
---
> package = funky
> version = 1.0
> tarname = $(package)
> distdir = $(tarname)-$(version)
7,8c10,11
< funky: main.cc
< 	gcc -g -o $@ main.cc
---
> all clean funky:
> 	cd src && $(MAKE) $@
10,11c13
< clean:
< 	-rm funky
---
> dist: $(distdir).tar.gz
13c15,26
< .PHONY: all funky
---
> $(distdir).tar.gz: $(distdir)
> 	tar chof - $(distdir) | gzip -9 -c > $@
> 	rm -rf $(distdir)
>  
> $(distdir):
> 	mkdir $(distdir)
> 	mkdir $(distdir)/src
> 	cp Makefile $(distdir)
> 	cp src/Makefile $(distdir)
> 	cp src/main.cc $(distdir)
>  
> .PHONY: all clean dist
 7,8c10,11
 < funky: main.cc
 < 	gcc -g -o $@ main.cc
 ---
 > all clean funky:
 > 	cd src && $(MAKE) $@

Le fragment ci-dessus indique que les lignes 7 et 8 du premier fichier ont été modifiées et correspondent aux lignes 10 et 11 du second fichier (7.8c10.11). En dessous, on peut voir le contenu de ces lignes 7 et 8 (identifiées par <). Ainsi que le texte de remplacement (identifié par >).

Au premier regard, le résultat semble confus. Mais avec un peu d'habitude, on peut le comprendre sans trop de problème en se souvenant de quelques règles simples:

Vous vous doutez bien que si diff utilise un tel format de sortie, c'est dans le but de pouvoir facilement exploiter ce résultat avec un autre logiciel. L'outil qui va traditionnellement de paire est patch:

sh$ diff makefile.v1 makefile.v2 > makefile.diff
sh$ patch makefile.v1 makefile.diff -o makefile.patched 
patching file makefile.v1

Cette dernière commande prend le contenu de makefile.v1, lui applique les transformations décrites dans makefile.diff et enregistre le résultat dans makefile.patched. De fait, cela rend makefile.patched identique à makefile.v2. Ce que l'on peut vérifier de visu ou à l'aide de diff lui-même:

sh$ diff -q makefile.v2 makefile.patched && echo Identiques!
Identiques!

Utilisation créative

Après ce rappel sur la commande diff, voyons maintenant le cœur de cet article. À savoir comment utiliser de manière créative diff. En l'occurrence pour pouvoir générer un document HTML mettant en évidence les différences entre deux versions d'un fichier.

Piège:

Ici, je vais utiliser des options de la commandes diff spécifiques à la version GNU de cet utilitaire. Si vous utilisez une autre version de diff, il se peut que ces options soient un peu différentes – ou malheureusement totalement absentes.

Pour réaliser cette tâche, je vais utiliser les options --old-line-format, --new-line-format et --unchanged-line-format de GNU diff. Celles-ci permet de spécifier comment diff va reporter les lignes différentes ou identiques entre les fichiers qu'il compare.

Comme souvent avec les commandes Unix, le format est spécifié avec des combinaisons de caractères qui seront reproduits tel quel et de métacaractères commençant par % qui seront remplacés dynamiquement par des valeurs lors de l'exécution du programme. La combinaison que j'utilise ici est %L qui sera remplacée par le contenu de la ligne courante (y compris le retour à la ligne).

Par exemple:

--old-line-format=O%L
Affiche les lignes présentes dans le premier (old) fichier mais absentes dans le second précédées du caractère O.
--new-line-format=N%L
Affiche les lignes présentes dans le second (new) fichier mais absentes dans le premier précédées du caractère N.
--unchanged-line-format=U%L
Affiche les lignes communes aux deux fichiers précédées du caractère U.

Si je reprends les fichiers de la première partie de cet article, voici ce que cela produira:

sh$ diff --unchanged-line-format=U%L \
         --new-line-format=N%L \
         --old-line-format=O%L \
         makefile.v1 makefile.v2
U#
O# funky/src/Makefile
N# funky/Makefile
U#
U 
Oall: funky
Npackage = funky
Nversion = 1.0
Ntarname = $(package)
Ndistdir = $(tarname)-$(version)
U 
Ofunky: main.cc
O	gcc -g -o $@ main.cc
Nall clean funky:
N	cd src && $(MAKE) $@
U 
Oclean:
O	-rm funky
Ndist: $(distdir).tar.gz
U 
O.PHONY: all funky
N$(distdir).tar.gz: $(distdir)
N	tar chof - $(distdir) | gzip -9 -c > $@
N	rm -rf $(distdir)
N 
N$(distdir):
N	mkdir $(distdir)
N	mkdir $(distdir)/src
N	cp Makefile $(distdir)
N	cp src/Makefile $(distdir)
N	cp src/main.cc $(distdir)
N 
N.PHONY: all clean dist

Même si pour l'instant le résultat reste toujours assez peu lisible, je pense que vous comprenez le potentiel des options que j'ai utilisées. C'est ce que je vais développer maintenant pour formater ce résultat en HTML.

Produire du HTML avec diff

En utilisant les options introduites précédemment, il est possible de produire un résultat en HTML:

sh$ diff --unchanged-line-format='<p class="unchanged">%L</p>' \
         --new-line-format='<p class="new">%L</p>' \
         --old-line-format='<p class="old">%L</p>' \
         makefile.v1 makefile.v2
<p class="unchanged">#
</p><p class="old"># funky/src/Makefile
</p><p class="new"># funky/Makefile
</p><p class="unchanged">#
</p><p class="unchanged"> 
</p><p class="old">all: funky
</p><p class="new">package = funky
</p><p class="new">version = 1.0
</p><p class="new">tarname = $(package)
</p><p class="new">distdir = $(tarname)-$(version)
</p><p class="unchanged"> 
</p><p class="old">funky: main.cc
</p><p class="old">	gcc -g -o $@ main.cc
</p><p class="new">all clean funky:
</p><p class="new">	cd src && $(MAKE) $@
</p><p class="unchanged"> 
</p><p class="old">clean:
</p><p class="old">	-rm funky
</p><p class="new">dist: $(distdir).tar.gz
</p><p class="unchanged"> 
</p><p class="old">.PHONY: all funky
</p><p class="new">$(distdir).tar.gz: $(distdir)
</p><p class="new">	tar chof - $(distdir) | gzip -9 -c > $@
</p><p class="new">	rm -rf $(distdir)
</p><p class="new"> 
</p><p class="new">$(distdir):
</p><p class="new">	mkdir $(distdir)
</p><p class="new">	mkdir $(distdir)/src
</p><p class="new">	cp Makefile $(distdir)
</p><p class="new">	cp src/Makefile $(distdir)
</p><p class="new">	cp src/main.cc $(distdir)
</p><p class="new"> 
</p><p class="new">.PHONY: all clean dist
</p>

Piège:

En l'état, la commande ne gère pas la présence de caractères HTML réservés (< et &) dans les lignes affichées!

Bien sûr, reste encore à encapsuler cela dans un document HTML et à fournir la feuille de style adéquate. Mais il est possible d'écrire un petit script pour s'occuper de la plomberie nécessaire et obtenir un résultat directement exploitable dans un navigateur:

Diffhtml.png
Le script ci-contre utilise la commande diff pour présenter sous forme d'un document HTML les différences entre deux fichiers:
sh$ ./diffhtml makefile.v1 makefile.v2 > diff.html
sh$ firefox diff.html
Comme vous pouvez le constater, le résultat est des plus lisibles. Et peut facilement être adapté à votre besoin.
#!/bin/sh
 
#
# diffhtml: use diff to produce a html document showing differences
#           between two files
#
 
tmpdir=`mktemp -d`  || exit
trap "rm -rf -- '${tmpdir}'" EXIT
mkfifo -- ${tmpdir}/1  || exit
mkfifo -- ${tmpdir}/2  || exit
 
cat $1 | sed -e 's/&/&amp;/g' -e 's/</&lt;/g' > ${tmpdir}/1 & 
cat $2 | sed -e 's/&/&amp;/g' -e 's/</&lt;/g' > ${tmpdir}/2 & 
 
cat << EOF
<html>
    <head>
	<title>DiffHtml</title>
	<style type='text/css'>
	pre p { background-color: #F8F8F8; padding: 0; margin: 0; }
	pre .old { color: #555; text-decoration: line-through; }
	pre .new { background-color: #FDD; }
	pre .unchanged { }
	</style>
    </head>
    <body>
	<pre>
EOF
 
diff --unchanged-line-format='<p class="unchanged">%L</p>' \
     --new-line-format='<p class="new">%L</p>' \
     --old-line-format='<p class="old">%L</p>' \
     ${tmpdir}/1 ${tmpdir}/2
 
cat << EOF
	</pre>
    </body>
</html>
EOF

Même si le script semble au premier regard assez long, en l'examinant de plus près, vous constaterez que l'essentiel est composé par le code HTML brut. Le cœur du script restant l'appel à la commande diff. Seule complexité, la première partie qui utilise des pipes nommés créés avec mkfifo lors de la substitution des caractères < et & par leurs entités HTML correspondantes.