Voici un script Python pour connaître les modifications subies par un répertoire entre un instant A et un instant B. Comble du bonheur, ce n’est pas un programme en ligne de commande.
Ce programme comble (enfin je crois) un manque en la matière car des commandes existent qui font plus ou moins ça :
- diff : permet de comparer deux répertoires, mais il ne permet pas de comparer un répertoire avec lui-même…
- dnotify : permet de suivre les modifications en temps réel et d’exécuter un action quand une modification a lieu. S’il n y a pas de modification, dnotify ne rend pas la main…
Pré-requis
Le programme utilise zenity et yelp qui devraient être installés par défaut sur Ubuntu (Gnome).
Installation
Copier le script dans votre répertoire ~/bin. Il peut porter le nom que vous voulez, moi je l’ai appelé “Répertoire avant-après”, n’ayons pas peur des accents et des espaces !
Donnez-lui les droits en exécution :

Donner les droits en exécution au script
Il est ensuite possible de créer un lanceur sur le bureau. Sous Nautilus, faites apparaître le menu contextuel en cliquant avec le bouton de droite de la souris sur le bureau :

Menu contextuel de Nautilus
Cliquer sur Créer un lanceur…, la fenêtre suivante apparaît :

Créer un lanceur sur le bureau
Choisissez Application comme type de lanceur, donnez le nom que vous voulez et sélectionnez le script avec le bouton Parcours…. Il est également possible de changer l’icône en cliquant dessus.
Utilisation
Ce programme est très simple d’utilisation :
- vous sélectionnez un ou plusieurs répertoires, un cliché est automatiquement pris,
- vous exécutez les tâches dont vous voulez connaitre le comportement,
- vous cliquez sur le bouton Valider,
- vous lisez le résultat.
Le cliché inclut tous les sous-répertoires qu’il peut trouver.
Le programme est très rapide car les fichiers ne sont pas lus, seules les informations accessibles par la commande ls sont comparées.
Note : Il peut aussi être appelé depuis la ligne de commande en donnant les répertoires en paramètres, cela évite de devoir passer par la fenêtre de sélection. Le reste est identique.
Exemple
Dans cet exemple, je vais suivre les modifications de mon répertoire .mozilla lorsque je lance Firefox 3.
Avant de lancer Firefox, je lance le programme. La fenêtre de sélection de répertoire apparaît. Je sélectionne le répertoire et clique sur Valider.

Sélection du répertoire
Le premier cliché est réalisé et le message suivant apparaît :

Premier cliché réalisé
Je lance donc Firefox comme prévu. Une fois Firefox lancé, je clique sur Valider. La fenêtre des résultats s’affiche alors :

Fenêtre des résultats
Et voilà !
Fonctionnement
Sélection de répertoire
La sélection du ou des répertoires se fait avec zenity :
zenity --file-selection --directory --multiple
Explications :
- –file-selection : fait apparaître la fenêtre de sélection de fichiers,
- –directory : fonctionne en mode sélection de répertoire
- –multiple : permet de sélectionner plusieurs répertoires
En mode sélection multiple, zenity retourne une ligne contenant les répertoires séparés par un |.
Prise de cliché
Le programme utilise la commande ls pour récupérer les répertoires, les fichiers et leur état à un instant donné :
ls -AlRZ --time-style=long-iso repertoire | grep -v -e "^total" -e "^$"
Description :
- ls :
- -A : affiche tous les fichiers, même cachés à l’exception de . et ..,
- -l : affichage complet,
- -R : analyse récursive,
- -Z : récupère le contexte SELinux.
- repertoire : répertoire(s) à analyser
- grep :
- -v : inverse le filtrage de grep,
- -e “^total” : élimine les lignes de totaux,
- -e “^$” : élimine les lignes vides.
Toutes ces informations sont enregistrées à chaque prise de cliché pour ensuite être analysées.
La toute première lettre des droits fournis par ls -l permet de connaître le type de fichier :
- d : répertoire,
- - : fichier standard,
- b : fichier bloc spécial (device),
- c : fichier caractère spécial (device),
- l : lien symbolique,
- p : tube nommé,
- s : socket.
Analyse des différences
Les clichés sont indexés avec le chemin absolu de chaque fichier et répertoire.
L’algorithme d’analyse est plutôt simple :
- ajout : on regarde les entrées présentes dans le second cliché mais pas dans le premier,
- suppression : on regarde les entrées présentes dans le premier cliché mais pas dans le second,
- modification : on regarde si les informations de l’entrée ont changé entre les 2 clichés (type d’entrée, propriétaire, groupe, contexte SELinux, taille, jour et heure de modification).
Affichage des résultats
Pour afficher les résultats, on génère un bête fichier HTML que yelp se chargera d’afficher. Yelp est normalement l’afficheur d’aide en ligne de Gnome mais il peut afficher n’importe quel fichier HTML (sans trop pousser non plus…) que vous lui fournissez.
Idéal pour notre cas.
Code source
Oui, le code source est lourd et mal commenté (quick and dirty)…
#!/usr/bin/env python
# coding=utf8
import sys
from subprocess import call,Popen,PIPE
from os.path import abspath,join,dirname,basename
from tempfile import NamedTemporaryFile
def decoupe_entree(ligne):
elements=ligne.split(None,8)
entree={
"droits" :elements[0],
"proprietaire":elements[2],
"groupe" :elements[3],
"contexte" :elements[4],
"taille" :elements[5],
"jour" :elements[6],
"heure" :elements[7],
"nom" :elements[8]
}
lettre_type=elements[0][0]
entree["type"]="fichier de type inconnu"
if lettre_type=="d": entree["type"]="répertoire"
if lettre_type=="-": entree["type"]="fichier"
if lettre_type=="b": entree["type"]="fichier bloc spécial"
if lettre_type=="c": entree["type"]="fichier caractère spécial"
if lettre_type=="l": entree["type"]="lien symbolique"
if lettre_type=="p": entree["type"]="tube nommé"
if lettre_type=="s": entree["type"]="socket"
return entree
def exec_analyse(repertoires):
cmd1=['ls','-AlRZ','--time-style=long-iso']+repertoires
cmd2=['grep','-v','-e','^total','-e','^$']
p1=Popen(cmd1,stdout=PIPE,stderr=PIPE)
p2=Popen(cmd2,stdin=p1.stdout,stdout=PIPE,stderr=None)
lignes=p2.communicate()[0].splitlines()
repcourant=""
entrees ={}
for ligne in lignes:
if ligne.startswith("/"):
repcourant=ligne[0:-1]
else:
entree=decoupe_entree(ligne)
entrees[join(repcourant,entree['nom'])]=entree
return entrees
def compare_entree(avant,apres):
differences=[]
comparaisons={
"type" : "le type a changé",
"proprietaire": "le propriétaire a changé",
"groupe" : "le groupe a changé",
"contexte" : "le contexte SELinux a changé",
"taille" : "la taille a changé",
"jour" : "la date de modification a changé",
"heure" : "l’heure de modification a changé"
}
for composant in comparaisons:
if avant[composant]!=apres[composant]:
differences.append(comparaisons[composant]+" (%s → %s)"%(avant[composant],apres[composant]))
return differences
def compare_entrees(entrees_avant,entrees_apres):
ajouts =[]
suppressions =[]
modifications=[]
# Recherche les suppressions et les modifications
for entree in entrees_avant:
if entree not in entrees_apres:
# Entrée supprimée
suppressions.append(entree)
continue
if entrees_avant[entree]!=entrees_apres[entree]:
# Entrées différentes
modifications.append(entree)
# Recherche les ajouts:
for entree in entrees_apres:
if entree not in entrees_avant:
# Nouvelle entrée
ajouts.append(entree)
return [ajouts,suppressions,modifications]
def prepare_nom(chemin):
chemin=chemin.replace("&","&").replace(">",">").replace("<","&tt;")
return dirname(chemin)+"/<strong>"+basename(chemin)+"</strong>"
def affiche_differences(entrees_avant,entrees_apres,ajouts,suppressions,modifications):
html ='<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Strict//EN" "http://www.w3.org/TR/html4/strict.dtd">\n'
html+='<html lang="fr">\n'
html+='<head>\n'
html+='<title>Suivi des modifications</title>\n'
html+='<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n'
html+='</head>\n'
html+='<body>\n'
html+='<h1>Suivi des modifications</h1>'
html+='<h2>Ajouts</h2>\n'
if len(ajouts)==0:
html+='<p>Aucun ajout</p>\n'
else:
html+='<ul>\n'
for ajout in ajouts:
html+='<li>'+prepare_nom(ajout)+' (<i>'+entrees_apres[ajout]["type"]+'</i>)</li>\n'
html+='</ul>\n'
html+='<h2>Suppressions</h2>\n'
if len(suppressions)==0:
html+='<p>Aucune suppression</p>\n'
else:
html+='<ul>\n'
for suppression in suppressions:
html+='<li>'+prepare_nom(suppression)+' (<i>'+entrees_avant[suppression]["type"]+'</i>)</li>\n'
html+='</ul>\n'
html+='<h2>Modifications</h2>\n'
if len(modifications)==0:
html+='<p>Aucune modification</p>\n'
else:
html+='<ul>\n'
for modification in modifications:
html+='<li>'+prepare_nom(modification)
diffs=compare_entree(entrees_avant[modification],entrees_apres[modification])
html+='<ul>\n'
for diff in diffs:
html+='<li>'+diff+'</li>\n'
html+="</ul>\n"
html+='</li>\n'
html+='</ul>\n'
html+='</body>\n'
html+='</html>'
return html
if len(sys.argv)>=2:
rep_donnes=sys.argv[1:]
else:
rep_donnes=Popen(['zenity','--file-selection','--directory','--multiple'],stdout=PIPE).communicate()[0].splitlines()[0]
if rep_donnes=="": exit(0)
rep_donnes=rep_donnes.split('|')
repertoires=[]
for rep in rep_donnes:
rep=rep.strip()
if rep=="": continue
rep=abspath(rep)
if rep in repertoires: continue
repertoires.append(abspath(rep))
if len(repertoires)==0: exit(0)
entrees_avant=exec_analyse(repertoires)
call(["zenity","--info","--title=Premier cliché réalisé","--text=Un premier cliché du répertoire a été réalisé.\nVeuillez cliquer sur le bouton pour lancer le second et afficher les différences"])
entrees_apres=exec_analyse(repertoires)
(ajouts,suppressions,modifications)=compare_entrees(entrees_avant,entrees_apres)
html=affiche_differences(entrees_avant,entrees_apres,ajouts,suppressions,modifications)
sortie=NamedTemporaryFile(suffix=".html")
sortie.write(html)
sortie.flush()
call(["yelp",sortie.name])


[...] This post was Twitted by zigazou – Real-url.org [...]
Salut à toi Zigazou et merci pour ces scripts.
Je viens de tester diffrep il fonctionne bien mais j’ai un petit bug, une fois que la comparaison faite elle ne s’affiche pas dans Aide.
Visiblement j’ai un problème de droit sur le fichier situé dans tmp, après vérification seul le propriétaire a des droits et je suis le propriétaire.
Voici le messade: Le fichier « /tmp/tmpnpeufB.html » ne peut être lu. Ce fichier peut être manquant, ou vous n’avez peut-être pas les permissions de le lire.
Par contre si j’ouvre tmpnpeufB.html avec firefox pas de soucis.
Salut M.i.B !
diffrep utilise yelp pour l’affichage du fichier html. As-tu le même message si tu fais manuellement yelp /tmp/tmpnpeufB.html ?
Sinon, il est toujours possible de remplacer yelp par xdg-open (toute dernière ligne du script).
S’il continue à afficher ce message, ça veut dire que ça vient de ma gestion du fichier temporaire.
Merci d’essayer !
@+
je viens de lancer la commande yelp /tmp/tmpTSB5Ni.html même résultat.
Après la modif du script avec xdg-open, Firefox ne peut trouver le fichier à l’adresse /tmp/tmpTSB5Ni.html
Je vais donc vérifier mon tmp pas de tmpTSB5Ni.html présent
Si ça peux t’aider.
Au fait je suis sous intrepid.
À mon avis, ne te tracasse pas trop pour ton répertoire tmp, je pense que ça vient de mon script.
En faisant des tests, j’obtiens les mêmes symptômes que toi en ajoutant un sortie.close() juste avant la dernière ligne
Si je fais un ls -l, j’obtiens ça pour l’utilisateur jojo :
-rw——- 1 jojo jojo 426 2009-05-30 18:21 /tmp/tmphOimj1.html
Peux-tu essayer en remplaçant les dernières lignes par celles-ci ?
sortie=NamedTemporaryFile(suffix=”.html”,delete=False)
sortie.write(html)
sortie.flush()
sortie.close()
call(["yelp",sortie.name])
Merci
en modifiant le script comme demandé, il ne se lance pas et dans le terminal ça donne ceci
mib@MYPC1:$ ./diffrep
File “./diffrep”, line 175
sortie=NamedTemporaryFile(suffix=”.html”,delete=False)
^
SyntaxError: invalid syntax
C’est dû à WordPress qui met des guillemets typographiques et non pas informatiques, quand tu fais un copier-coller, remplacer les guillemets par celles de ton clavier (sur la touche 3 ” #)
Visiblement j’ai encore caractères non reconnu
mib@MYPC1:$ ./diffrep
Traceback (most recent call last):
File “./diffrep”, line 175, in
sortie=NamedTemporaryFile(suffix=”.html”,delete=False)
TypeError: NamedTemporaryFile() got an unexpected keyword argument ‘delete’
Argh !
Intrepid doit être en Python 2.4.
Jaunty est en Python 2.6…
C’est pour cela qu’il ne prend pas le paramètre delete…
Va falloir que je planche plus pour la compatibilité avec la 2.4 (c’est peut-être même de là que vient le problème général
)
Ok et merci pour toutes ces tests et explications, en espérant que tu trouves une solution pour intrepid