Recoller certaines lignes avec sed
J'avais un fichier TSV, dont certaines lignes étaient coupées par un retour chariot indésirable. Chaque ligne qui commençait par une tabulation devait donc être recollée à la ligne précédente. J'ai voulu le faire avec la commande sed, mais je me suis rendu compte que j'étais un peu rouillé pour faire plus qu'un simple s/foo/bar/
. Je le note donc ici pour ne plus perdre de temps, un jour, à retrouver comment sed fonctionne.
J'avais donc des lignes de ce style :
Il était une fois trois petits cochons qui vivaient avec leur maman dans une petite maison
Que je voulais remettre d'équerre comme ça :
Il était une fois trois petits cochons qui vivaient avec leur maman dans une petite maison
1) Ma méthode inutilement compliquée 🙄
Il fallait donc que je demande à sed de regarder si la ligne courante (la ligne lue dans l'espace de travail, "pattern space" dans la doc en anglais) commence par une tabulation, et si c'est le cas, recoller avec la ligne précédente. Donc il fallait que je garde en permanence la ligne précédente dans le "hold space" (l'espace de rétention). Ce qui s'écrit en pseudo-algorithme :
- Si la ligne dans l'espace de travail ne commence pas par une tabulation :
-
Échange cette ligne avec celle qui est dans l'espace de rétention (commande
x
)
c'est-à-dire "ressort la ligne précédente" sans perdre la ligne courante - et affiche-la (commande
p
) - Si la ligne dans l'espace de travail commence par une tabulation :
-
Échange cette ligne avec celle qui est dans l'espace de rétention (commande
x
)
idem cas précédent, mais la suite est différente -
puis colle l'espace de rétention avec la ligne dans l'espace de travail (commande
G
en majuscule)
(à ne pas confondre avec g en minuscule qui écrase l'espace de travail !) -
puis substitue le retour chariot par rien (
s/\n//
) -
puis affiche le résultat (
p
) -
puis lis la ligne suivante (
n
) -
et mets cette ligne dans l'espace de rétention (commande
h
en minuscule)
(à ne pas confondre avec H en majuscule qui ajoute à l'espace de rétention)
en clair, on écrase l'espace de rétention précédent, pour réamorcer le processus - Cas particulier si on est sur la première ligne, qui est repérée par le chiffre 1 (pas par 0) :
-
Mets cette ligne dans l'espace de rétention (commande
h
) -
Puis efface l'espace de travail et commence un nouveau cycle (commande
d
)
(lis une ligne et exécute le script) - Et si la dernière ligne, qui est repérée par
$
, ne commence pas par une tabulation (pas le cas avec ce fichier) : -
Affiche cette ligne (
p
)
Ce qui s'écrit en one-liner avec un peu d'espacement pour y voir clair :
sed -n '1{h;d}; /^\t/!{x;p}; /^\t/{x;G;s/\n//;p;n;h}; $p'
Je rappelle le contenu du fichier de départ, je numérote les lignes, et je décortique le fonctionnement ligne à ligne, un peu à la manière de sed --debug
:
1 Il était une fois 2 trois petits 3 cochons 4 qui vivaient avec leur maman 5 dans une 6 petite maison
ligne lue |
exécute |
pattern space |
hold space |
1 |
1{h;d} |
|
Il était une fois |
2 |
/^\t/!{x;p} | Il était une fois | trois petits |
3 |
/^\t/{x;G;s/\n//;p;n;h} |
trois petits cochons juste avant la commande n |
cochons juste avant la commande h |
4 déjà lue par la commande n |
|
qui vivaient avec leur maman après la commande n, je n'affiche pas |
qui vivaient avec leur maman après la commande h |
5 |
/^\t/!{x;p} |
qui vivaient avec leur maman cette fois j'affiche |
dans une |
6 |
/^\t/{x;G;s/\n//;p;n;h} |
dans une petite maison juste avant la commande n |
petite maison juste avant la commande h |
2) La bonne méthode, celle du manuel 🤦
J'ai pourtant appris, dès que j'ai débuté, qu'il faut toujours RTFM… mais j'ai quand même passé quelques heures à mettre au point ma méthode avant de découvrir celle-ci qui est beaucoup plus simple, dans le manuel.
sed ':a ; $!N ; s/\n\t/\t/ ; ta ; P ; D'
Avec un peu de couleur pour faciliter la lecture :
sed ':a ; $!N ; s/\n\t/\t/ ; ta ; P ; D'
- L'idée, c'est de coller une ligne avec la suivante (commande
N
) sauf si c'est la dernière ligne ($!
) - et s'il y a une tabulation juste après le retour chariot qui colle les deux lignes lues, il suffit de supprimer ce retour chariot et de laisser la tabulation avec la substitution
s/\n\t/\t/
- La commande
t
permet de se brancher sur l'étiquette "a" seulement s'il y a eu une substitution (c'est différent de la commandeb
). - La commande
P
affiche l'espace de travail jusqu'au premier retour chariot, c'est-à-dire la ligne "précédente". - L'action de la commande
D
dépend de si l'espace de travail contient des retours chariot ou pas. - S'il en contient, elle efface tout jusqu'au premier retour chariot, c'est-à-dire qu'elle efface la ligne "précédente", puis elle relance le script sans lire de nouvelle ligne dans le fichier. Elle va donc permettre de traiter la ligne "suivante".
- S'il n'en contient pas, c'est-à-dire qu'on a une seule ligne dans l'espace de travail, elle efface l'espace de travail et relance le script avec une nouvelle ligne du fichier.
Bon, d'accord, sed c'est un peu barjot et j'aurais eu plus vite fait de le faire en Perl ou en PHP… Mais sed fait partie de la "culture Unix" donc c'est toujours un plaisir de l'utiliser 😉