Pourquoi j'aime UNIX

[2006-03-30 01:20:55]

Après de malheureux tests de différentes options pour synchroniser mes dossiers de courriels, je me suis retrouvé avec un petit problème: des centaines de courriels dupliqués... J'en ignorais le nombre exact au départ mais la perspective de tous les effacer un à un, avec les risques d'erreurs que cela entraîne m'a fait rebrousser chemin. Mais comment faire alors pour m'en sortir?

Mes dossiers sont au format Maildir, qui sauvegarde un fichier par courriel. Il me suffit donc de trouver les mails dupliqués parmi ceux-ci. J'aurais pu utiliser le Message-Id pour identifier les messages identiques mais je n'étais pas certains que ces derniers étaient bons pour tous mes emails (mes archives datent de 1999). Non, je voulais un md5sum du email mais mes emails différaient par un champ: X-OfflineIMAP-XXXX (un tag ajouté par le logiciel que j'utilise actuellement... le seul capable de synchroniser mes dossiers correctement). Il me fallait donc le retirer.

Voici donc la recette que j'ai utilisé:

grep -rl '^X-OfflineIMAP-' ~/Mail | tee progress | xargs -n 20 sed -i '/^X-OfflineIMAP-/d' &
tail -f progress | xargs -n1 dirname | uniq -u
find ~/Mail -type f ! -name .uidvalidity | xargs -n 20 md5sum | tee mails.md5sum | cut -d\  -f 3 | xargs -n1 dirname | uniq -u
sort -o mails.md5sum mails.md5sum
uniq -D -t\  -W 1 < mails.md5sum > mails.alldups
uniq -d -t\  -W 1 < mails.alldups > mails.udups
cat mails.alldups mails.udups | sort | uniq -u > mails.duplicates
cat mail.duplicates | xargs -n20 rm

Qu'est-ce que ça fait tout ça? On va y aller ligne par ligne:

grep -rl '^X-OfflineIMAP-' ~/Mail | tee progress | xargs -n 20 sed -i '/^X-OfflineIMAP-/d' &

La commande grep ici cherche pour les fichiers contenant une ligne commençant (ce à quoi sert le ^) par X-OfflineIMAP- dans mon répertoire ~/Mail. L'option -r descend dans tous les sous-répertoires et l'option -l ne fait que lister les noms des fichiers (plutôt que la ligne elle-même). La commande tee permet de sauvegarder dans un fichier en même temps que de l'envoyer vers l'entrée suivante. Je m'en sers pour visualiser les progrès (la ligne suivante). xargs permet d'envoyer l'entrée en argument à la commande suivante. Comme la quantité de fichiers est énorme, je limite le nombre d'argument à 20 (avec -n 20). La commande en question est sed avec l'option -i qui permet les changements en place. La commande de sed est simple: trouve la ligne commençant par X-OfflineIMAP et efface-là (d).

Cette commande à elle seule prend beaucoup de temps à s'exécuter mais comme j'avais un fichier de progression, je pouvais savoir où j'étais rendu:

tail -f progress | xargs -n1 dirname | uniq -u

La commande tail, avec l'option -f, envoie en continue la fin d'un fichier passé en paramètre. C'est mon fichier de progression créé en background par le processus précédent. J'utilise encore xargs pour filtrer les noms de fichiers avec dirname. C'est pas très efficace (sed aurait mieux fait) mais je n'avais pas le goût de me casser la tête. De toute façon, j'avais nice mon shell pour être gentil avec les autres usagers du système. Je me retrouve donc avec une liste de répertoire m'indiquant où j'en suis rendu. Comme je veux juste savoir quel répertoire est en cours de traitement, j'ai rajouté un uniq -u qui n'affiche que la première ligne unique de son entrée. Je peux donc voir quel est le dernier répertoire dans lequel grep a trouvé un nouveau fichier.

find ~/Mail -type f ! -name .uidvalidity | xargs -n 20 md5sum | tee mails.md5sum | cut -d\ -f 3 | xargs -n1 dirname | uniq -u

Cette ligne est longue mais plus simple qu'il n'y paraît. La partie active est find ~/Mail -type f ! -name .uidvalidity, qui trouve tous les fichiers réguliers dans le répertoire (correspondant aux courriels), à l'exception des fichiers .uidvalidity (qui ne sont pas des courriels). La commande suivante passe les noms des fichiers à md5sum par paquet de 20. Je sauvegarde cette liste dans mails.md5sum et utilise la sortie pour créer une sortie de progression similaire à la précédente. Le cut ici ne fait que retirer la première partie du fichier, qui contient la somme md5.

sort -o mails.md5sum mails.md5sum
uniq -D -t\  -W 1 < mails.md5sum > mails.alldups
uniq -d -t\  -W 1 < mails.alldups > mails.udups
cat mails.alldups mails.udups | sort | uniq -u > mails.duplicates

Voilà ici le corps du travail pour trouver les duplicats. En premier, je mets en ordre le fichier mails.md5sums. C'est nécessaire pour uniq qui ne fonctionne que sur des fichiers ordonnés. Ensuite, je crée deux nouveaux fichiers: mails.alldups, qui contient tous les duplicats et mails.udups qui ne contient que le premier duplicat (celui qui sera conservé, arbitrairement). L'option -W 1 permet de n'utiliser que le premier champ (délimité par le caractère d'espacement, donné par -t\ ) pour trouver les uniques, soit la somme md5. Ensuite, je concatène les deux fichiers, que je mets en ordre (pour uniq) et demande à uniq de ne sortir que les lignes uniques. Ceci retira donc toutes les lignes dans mails.alldups qui ne sont pas dans mails.udups. C'est bien entendu sauvegarder dans mails.duplicates.

Un rapide wc -l mails.dup* me permet de constater que j'ai bien fait de ne pas faire ce travail à la main:

 4131 mails.dup
 4823 mails.duplicates
 8954 mails.dups
17908 total

4823 duplicats de 4131 fichiers différents, pour un total de 8954 fichiers! Ouf! Il ne me reste plus qu'à les effacer:

cat mail.duplicates | xargs -n20 rm

Voilà... c'est fait... Essayer de faire la même chose avec une machine sans shell script; Quoi, le menu Edit->Effacer courriels en double n'existe pas? Dommage...