Modifier des fichiers en masse

La problèmatique du jour est la suivante : j'utilise actuellement selenium pour les besoins du boulot. Je vous passe les détails, mais j'ai moults fichiers html qui me permettent de tester le contenu d'un site. Sauf que ce site évolue parfois, et du coup, il me faut modifier relativement fréquemment ces fichiers. Heureusement qu'il y a findus emacs.

1er cas : changer un lien

Dans certains tests (qui sont nommés trucVariable_toto.html), j'ai à tester la présence d'un lien <a href="toto">trucConstant</a>. Parfois, trucConstant change et devient autreChose. Du coup, pleins de fichiers dans lesquels je dois modifier moi aussi trucConstant par autreChose.

C'est un cas assez fréquent (correction d'une typo…), et assez simple à résoudre :

  • On ouvre le dossier où sont contenus les fichiers .html (avec C-x C-f) et on se trouve dans dired.
  • On marque les fichiers concerné %m TexteDansLeNomDesFichiers.
  • On appuie sur Q (pour Query replace regexp in marked files).
  • trucConstant (mais une regexp est possible).
  • autreChose.
  • Y (Yes, toutes les condordances pour tous les fichiers marqués (y,y,y au début pour voir que ça va bien si jamais on a un doute ;]).
  • Nos fichiers sont modifiés mais pas sauvés, et ouverts dans des buffers. Du coup M-x ibuffer (Parce que C-x C-b n'est parfois pas suffisant (h dans ibuffer pour l'aide)).
  • * u (pour marquer tous les non-sauvés).
  • S (pour sauver les buffers marqués).
  • D (pour supprimer les buffer marqués).
  • q (quitter ibuffer).
  • Profiter du temps gagné.

Idem mais un peu plus à changer

Parfois, c'est tout un tableau qui est à changer. Et ce tableau est identique pour tous les fichiers. Ça reste dans ce cas relativement simple. Pour ce cas, je copie le tableau, je le mets dans un buffer temporaire, je le prépare pour être « regexp ready », le stocke dans un registre, fais ma modification, stocke dans un second registre, et fais comme au-dessus, sauf qu'au lieu de taper trucConstant et autreChose, je restitue mes registres.

  • Ouvrir un des fichiers et aller au début de la partie à modifier.
  • C-SPC pour poser la marque.
  • Déplacer le curseur à la fin de la partie à modifier.
  • M-w pour copier.
  • Ouvrir un buffer temporaire C-x b auie (ou *scratch*).
  • Coller C-y la région précédemment sauvegardée.
  • Se placer au début du buffer M-<, ou sélectionner tout le buffer C-x h.
  • Changer les [ et ] par \[ et \] pour que Query replace regexp in marked files ne les prennent pas pour des caractères spéciaux : C-M-% changer \(\[\|\]\) par \\\1.
  • ! pour tout substituer.
  • Notre trucConstant un peu plus long est prêt. On sélectionne tout C-x h, et on stocke dans un registre C-x r s a par exemple. On peut faire notre deuxième registre.
  • On supprime avec C-w (comme tout le buffer est sélectionné).
  • On colle la région sauvegardée du début. Pour cela, on remonte dans l'historique du kill-ring avec C-y puis M-y.
  • On fait les modifications nécessaires.
  • On resélectionne tout C-x h, et on stocke notre deuxième registre avec C-x r s b par exemple.
  • On supprime notre buffer temporaire C-x k.
  • On se place dans notre dossier où on marque les fichiers contenant le texte qqChoseDansLeTableau avec % g puis qqChoseDansLeTableau.
  • Q puis on restitue notre premier registre avec C-x r i a.
  • La substitution est assurée par le registre b : C-x r i b.
  • Et comme au dessus, on appelle M-x ibuffer pour sauver/détruire les buffers modifiés.

Pas bien plus compliqué donc. Il faut juste faire attention au fait que Query replace regexp in marked files prend en première partie une regexp, ce qui peut poser quelques problèmes, mais aussi être utile puisqu'on peut se servir de bouts de la regexp pour les restituer avec \1, \2… D'ailleurs, c'est ce que l'on pourrait faire si le tableau n'est pas vraiment identique d'un fichier à l'autre. Mais comme ça risque d'être long et fastidieux, je préfère passer par des macros !

Supprimer une ligne dans un tableau, utilisation de macros

Pour ceux qui ne suivent que moyennement, je replace le contexte : plein de fichiers .html à changer, avec plus ou moins la même chose à l'intérieur, mais pas vraiment, et du coup, ça pourrait être chiant. Mais au final, pas tant que ça.

Afin de mieux expliquer, voici ce que contient une des pages du site que je dois tester :

.                       .                          .
.                       .                          .
|                       |                          | 
+-----------------------+--------------------------+
|    texte a :          |       valeur             |
+-----------------------+--------------------------+
|    texte b :          |       valeur             |
+-----------------------+--------------------------+
|    texte c :          |                          |
+-----------------------+--------------------------+
|      sstexte i        |       valeur             |
+-----------------------+--------------------------+
|      sstexte ii       |       valeur             |
+-----------------------+--------------------------+
|                       |                          |
.                       .                          .
.                       .                          .

Un tableau, avec des valeurs qui changent selon ce que l'on a fait avant, comment on a navigué, etc.

Mais maintenant, le site a changé, et la ligne avec texte b a disparu :

.                       .                          .
.                       .                          .
|                       |                          | 
+-----------------------+--------------------------+
|    texte a :          |       valeur             |
+-----------------------+--------------------------+
|    texte c :          |                          |
+-----------------------+--------------------------+
|      sstexte i        |       valeur             |
+-----------------------+--------------------------+
|      sstexte ii       |       valeur             |
+-----------------------+--------------------------+
|                       |                          |
.                       .                          .
.                       .                          .

Dans mes tests selenium, j'utilise xpath pour retrouver les éléments. Les éléments du tableau ont pour chemin quelque chose comme :

.                       .                          .
.                       .                          .
|                       |                          | 
+-----------------------+--------------------------+
|  //path/to/elem[x]    |     //path/to/avec[x]    |
+-----------------------+--------------------------+
|  //path/to/elem[x+2]  |                          |
+-----------------------+--------------------------+
|  //path/to/elem[x+4]  |     //path/to/avec[x+4]  |
+-----------------------+--------------------------+
|  //path/to/elem[x+5]  |     //path/to/avec[x+5]  |
+-----------------------+--------------------------+
|                       |                          |
.                       .                          .
.                       .                          .

Et là, on voit bien le problème : le texte c n'est plus à l'indice x+2 à présent, mais x+1. À partir de x, il va falloir décrémenter tous les chiffres.

Ça ferait un bon exercice, mais ne parlant pas encore le (e)lisp couramment, https://duckduckgo.com/?q=!+emacs+increment+number+at+point me donne la réponse plus rapidement…

(defun decrement-number-at-point ()
      (interactive)
      (skip-chars-backward "0123456789")
      (or (looking-at "[0123456789]+")
          (error "No number at point"))
      (replace-match (number-to-string (1- (string-to-number (match-string 0))))))

On va procéder maintenant en deux étapes : enregistrer une macro pour faire le changement dans un fichier, et appeler cette macro pour faire le changement dans tous les fichiers.

Enregistrement de la première macro

On a de la chance, dans le fichier .html (le test selenium) c'est tout bien aligné :

...
<tr>
        <td>verifyText</td>
        <td>//div[@id='unId']/table/tbody/tr[3]/th/span</td>
        <td>Texte a :</td>
</tr>
<tr>
        <td>verifyText</td>
        <td>//div[@id='unId']/table/tbody/tr[3]/td/span</td>
        <td>valeur</td>
</tr>
<tr>
        <td>verifyText</td>
        <td>//div[@id='unId']/table/tbody/tr[4]/th/span</td>
        <td>Texte b :</td>
</tr>
<tr>
        <td>verifyText</td>
        <td>//div[@id='unId']/table/tbody/tr[4]/td/span</td>
        <td>valeur</td>
</tr>
<tr>
        <td>verifyText</td>
        <td>//div[@id='unId']/table/tbody/tr[5]/th</td>
        <td>Texte c :</td>
</tr>
<tr>
        <td>verifyText</td>
        <td>//div[@id='unId']/table/tbody/tr[6]/th/span</td>
        <td>regexp:- sstexte i \* :</td>
</tr>
...
  • On se place sur le premier chiffre à changer.
  • F3 pour commencer l'enregistrement de la macro.
  • M-x decrement-number-at-point.
  • M-5 C-n (on descend de 5 lignes pour se placer sur le prochain chiffre).
  • F4 on termine la macro.

Si on appuie maintenant à nouveau sur F4, on va incrémenter le nombre sous le curseur et se déplacer vers le second chiffre.

Si on fait M-0 F4, on va appliquer la macro tant que c'est possible, et du coup, changer tous les chiffres qui vont bien.

Notre première macro est finie, on peut la garder pour la suite. Pour cela on la nomme pour pouvoir l'appeler comme une fonction : C-x C-k n auie.

Appliquer la première macro dans tous les fichiers

Enfin, tous les fichiers qui contiennent texte a

  • dans dired : %g texte a
  • puis voilà comment je fais en principe :
F3 Début de la macro
M-} Se positionner sur le fichier marqué suivant
Enter Ouvrir le fichier
M-< Aller au début du buffer (on pourrait l'avoir déjà d'ouvert)
C-M-S verifyText.*C-qC-j.*unId.*tr.4.*C-qC-j.*Texte b Texte b apparaissant à un autre endroit, je prends pas de risque
C-u C-p 4 lignes vers le haut
M-16 M-z > Supprimer jusqu'au 16e « > »
C-s 5 Chercher 5
Enter Arrêter la recherche
C-b Positionner le curseur sur le 5
M-0 M-x auie Exécuter tant que possible l'incrément/déplacement vers le bas
C-x C-s Sauver
C-x C-k Tuer le buffer
F4 Arrêt de la macro

Je sauve et tue le buffer, mais on pourrait faire comme précédemment, utiliser ibuffer. En plus, si jamais nos macros sont mal définies, on peut faire des M-x revert-buffer si ça c'est mal passé. Avec ma méthode, ce n'est pas le cas. (Mais bon, mes tests sont versionnés, je prends pas de risque non plus.)

Voilà les trois façons que j'utilise pour modifier tout un tas de fichiers. Ce qui est pratique, c'est que même si mes fichiers sont dans des dossiers rep/ssrep1 et rep/ssrep2, j'ouvre rep dans un buffer, puis i sur ssrep1 et ssrep2 me permet de voir tous les fichiers d'un coup :]

Si vous connaissez plus simple, n'hésitez pas à me dire comment !

Autres billets

Date: <2013-11-05>

Generated by Emacs 24.3.1 (Org mode 8.2.4) - Show Org source (htmlized)

CSS inspired by Tontof, colors by Chaotic Soul

Validate