Aller au contenu

sed : modifiez votre texte sans ouvrir vos fichiers

Les clones d’Unix sont peu utilisés pour gérer la documentation technique. Ceci est étrange si l’on songe à la pléthore d’outils disponibles sous ces plateformes pour manipuler du texte dans tous les sens.

Prenons l’exemple du dialogue entre M. Jourdain et son maître de philosophie, dans le Bourgeois gentilhomme de Molière :

MONSIEUR JOURDAIN :

[…] Je voudrais donc lui mettre dans un billet : « Belle marquise, vos beaux yeux me font mourir d’amour » ; mais je voudrais que cela fût mis d’une manière galante, que cela fût tourné gentiment.

[…]

MAÎTRE DE PHILOSOPHIE :

On les peut mettre premièrement comme vous avez dit : Belle marquise, vos beaux yeux me font mourir d’amour. Ou bien : D’amour mourir me font, belle marquise, vos beaux yeux. Ou bien : Vos yeux beaux d’amour me font, belle marquise, mourir. Ou bien : Mourir vos beaux yeux, belle marquise, d’amour me font. Ou bien : Me font vos yeux beaux mourir, belle marquise, d’amour.

Commençons par afficher la phrase d’origine dans un terminal :

echo "Belle marquise, vos beaux yeux me font mourir d'amour."
Cliquez sur Exécuter.

Il s’agit maintenant d’intervertir les mots de la phrase, pour en créer une nouvelle. Pour une simple transposition, on pourrait juger plus facile d’utiliser awk. awk ne gère en effet pas des lignes, mais des champs d’un enregistrement (d’une ligne), délimités par défaut par des espaces. Autrement dit, awk traite le texte comme une base de données. Il peut facilement afficher toute la ligne, ou seulement un ou plusieurs champs, dans l’ordre souhaité. Les champs sont indiqués sous la forme $n, où n indique la position du champ dans la ligne, à partir de la gauche. Ainsi $1 indique le premier champ, $2 le deuxième, etc. $0 correspond à toute la ligne.

Nous allons donc donner la déclaration d’amour de M. Jourdain en entrée d’un programme awk d’une ligne, grâce au symbole de redirection pipeline (|).

awk '{print $9" "$8" "$6" "$7" "$1" "$2" "$3" "$4" "$5}'
Cliquez sur Exécuter.

La sortie de la commande echo n’est pas affichée. Ce qui est affiché, c’est la sortie du programme awk, dont la sortie de la commande echo, soit la déclaration d’amour de M. Jourdain, était l’entrée.

La sortie finale ne correspond cependant pas à ce que l’on souhaitait. Les champs ne correspondent pas trait pour trait à des mots. Il faudrait donc raffiner la commande awk.

Il est plus simple de se tourner vers sed. sed sélectionne dans des lignes des ensembles de caractères cités littéralement, ou via des méta-caractères dans des expressions rationnelles (ou expressions régulières). Un méta-caractère bien connu des expressions rationnelles est *, qui signifie « zéro ou plusieurs occurrences de l’élément précédent » : a* correspond à zéro ou plusieurs a, et .* - n’importe quel caractère, répété - correspond à une suite quelconque de caractères.

sed gère également des références arrières, qui affichent à l’endroit où on le souhaite la valeur correspondant à une expression littérale ou rationnelle trouvée auparavant. Heureusement pour nous, la déclaration d’amour de M. Jourdain contient exactement neuf mots, ce qui correspond au nombre maximal de références arrières possibles.

sed "s/\(.*\) \(.*\), \(.*\) \(.*\) \(.*\) \(.*\) \(.*\) \(.*\)\(d'.*\)/\9 \8 \6 \7, \1 \2, \3 \4 \5/"
Cliquez sur Exécuter.

Nous butons sur le même problème : l’expression régulière .* ne correspond pas à un mot, mais à une suite de caractères, ponctuation comprise. Il faut alors utiliser la forme \<.*\>, qui correspond à un mot tel que ceux dont M. Jourdain se sert pour faire de la prose. Nous allons utiliser les caractères d’échappement (barre oblique inverse \) pour que les signes < et > ne soient pas interprétés littéralement sous certaines consoles, mais comme des méta-caractères ayant une fonction spéciale :

export p="\(\<.*\>\) \(\<.*\>\), \(\<.*\>\) \(\<.*\>\) \(\<.*\>\) \(\<.*\>\) \(\<.*\>\) \(\<.*\>\) \(d'\<.*\>\)"
sed "s/$p/\9 \8 \6 \7, \1 \2, \3 \4 \5/"
Cliquez sur Exécuter.

Nous pourrions également utiliser la forme [[:alpha:]]* qui fait gagner en lisibilité, mais perdre en concision :

export a="[[:alpha:]]"
export n="\($a*\) \($a*\), \($a*\) \($a*\) \($a*\) \($a*\) \($a*\) \($a*\) \(d'$a*\)"
sed "s/$n/\9 \8 \6 \7, \1 \2, \3 \4 \5/"
Cliquez sur Exécuter.

C’est mieux, mais nous avons un problème de capitalisation. Nous allons donc utiliser les opérateurs \u et \l placés judicieusement. Auparavant, nous allons exporter des variables pour rendre le script plus concis et plus lisible :

export w="\(\<.*\>\)"
export m="$w $w, $w $w $w $w $w $w"
sed "s/$m \(d'\<.*\>\)/\u\9 \8 \6 \7, \l\1 \2, \3 \4 \5/"
Cliquez sur Exécuter.

Nous pouvons maintenant facilement redistribuer les références arrières pour obtenir toutes les variations du maître de philosophie :

sed "s/$m \(d'\<.*\>\)/\u\3 \5 \4 \9 \6 \7, \l\1 \2, \8/"
Cliquez sur Exécuter.
sed "s/$m \(d'\<.*\>\)/\u\8 \3 \4 \5, \l\1 \2, \9 \6 \7/"
Cliquez sur Exécuter.
sed "s/$m \(d'\<.*\>\)/\u\6 \7 \3 \5 \4 \8, \l\1 \2, \9/"
Cliquez sur Exécuter.

Réécrivons le dialogue de M. Jourdain et de son maître de philosophie en style geek :

MONSIEUR JOURDAIN :

Je voudrais donc lui afficher sur la sortie standard :

Fenêtre de terminal
Belle marquise, vos beaux yeux me font mourir d'amour.

Mais je voudrais que cela fût mis d’une manière galante, que cela fût tourné gentiment.

MAÎTRE DE PHILOSOPHIE :

On les peut mettre premièrement comme vous avez dit :

Cliquez sur Exécuter.

Ou bien :

Cliquez sur Exécuter.

Ou bien :

export w="\(\<.*\>\)"
export m="$w $w, $w $w $w $w $w $w"
sed "s/$m \(d'\<.*\>\)/\u\9 \8 \6 \7, \l\1 \2, \3 \4 \5/"
Cliquez sur Exécuter.

Ou bien :

sed "s/$m \(d'\<.*\>\)/\u\3 \5 \4 \9 \6 \7, \l\1 \2, \8/"
Cliquez sur Exécuter.

Ou bien :

sed "s/$m \(d'\<.*\>\)/\u\8 \3 \4 \5, \l\1 \2, \9 \6 \7/"
Cliquez sur Exécuter.

Ou bien :

sed "s/$m \(d'\<.*\>\)/\u\6 \7 \3 \5 \4 \8, \l\1 \2, \9/"
Cliquez sur Exécuter.

Chaque commande sed ci-dessus exécutait GNU sed. Voici ce binaire même - compilé depuis son code source C en WebAssembly - qui exécute le vrai GNU sed, en direct dans cette page. Modifiez les phrases et reformulez-les vous-même :

Chargement du binaire sed…
Cliquez sur le bouton pour exécuter.

Le binaire (~250 Ko) est téléchargé une fois puis mis en cache. Chaque reformulation est une substitution sed distincte sur vos lignes - la commande même du didacticiel, exécutée pour de vrai, côté client.

La même idée de permutation de mots n’a pas à rester dans le shell : on la retrouve avec les expressions régulières en Python. La voici en vrai Python, exécuté en direct dans cette page :

Cliquez sur le bouton pour exécuter.

Toute ligne qui ne compte pas neuf mots est signalée au lieu d'être reformulée - tout s'exécute côté client, sans serveur.

Certes, beaucoup d’efforts pour pas grand-chose, me direz-vous. Mais imaginons un fichier qui contient 1000 phrases de la même structure.

Ceci est en l’occurrence peu probable, mais il est en revanche monnaie courante de trouver dans la documentation technique des phrases de même structure, pour des raisons d’homogénéité stylistique.

Pour effectuer nos tests sur un échantillon, plaçons les trois phrases précédentes dans un fichier :

echo "Cher docteur, ces grands malheurs vous font pleurer d'amertume." > variations.txt
echo "Petit garçon, cette bonne glace te fait saliver d'envie." >> variations.txt
echo "Vaste océan, la forte houle te fait tanguer d'ivresse." >> variations.txt
Cliquez sur Exécuter.

Plaçons les différentes commandes sed dans un script différent chacune :

export p="$m \(d'\<.*\>\)"
echo "s/$p/\u\9 \8 \6 \7, \l\1 \2, \3 \4 \5/" > moliere1.sed
echo "s/$p/\u\3 \5 \4 \9 \6 \7, \l\1 \2, \8/" > moliere2.sed
echo "s/$p/\u\8 \3 \4 \5, \l\1 \2, \9 \6 \7/" > moliere3.sed
echo "s/$p/\u\6 \7 \3 \5 \4 \8, \l\1 \2, \9/" > moliere4.sed
Cliquez sur Exécuter.

Exécutons maintenant en boucle tous les scripts sed sur toutes les lignes du fichier :

for (( i=1; i<5; i++ )); do
   while read s;
    do echo "$s" |
     sed -f moliere$i.sed ;
    done < variations.txt
   done
Cliquez sur Exécuter.

Et voilà. En quelques instants, sans jamais ouvrir un seul fichier, nous appliquons une suite d’opérations complexes sur un nombre indéfini de phrases de même structure. Ce qui n’est pas possible sous un traitement de texte ou autre outil muni d’une interface graphique, ou sur des fichiers binaires.