jeudi 18 mars 2010

sed partie 2

Dans le billet précédent de nombreuses commandes pouvant être utilisées dans un script sed ont été présentées, à l'exception toutefois de la commande principale, la commande de substitution s.

En voici la syntaxe:

s/regexp/remplacement/[flags]

s remplace quand elle est vérifiée, l'expression régulière regexp par la chaîne remplacement.

Par défaut, seule la première occurrence de regexp est remplacée. Ce comportement peut-être modifié à l'aide des flags qui peuvent être ajoutés à la commande tels que:

g changer toutes les occurrences

2 changer l'occurrence 2

p impression s'il y a un remplacement.

Exemple d'instruction utilisant la commande s:

$ sed  '/oui/s/Tata/Titine/' toto

Je m'appelle Toto.

Je vais à l'école avec Tata

Nous allons à pied car ce n'est pas loin.

Elle m'aime et ça c'est cool!

Et oui Titine aime Toto et Toto aime Tata

La vie est belle car Tata habite près de Toto.

Ils se voient tous les jours!

La substitution s'effectue pour la première occurrence de « Tata » et pour les lignes où l'expression régulière constante « oui » est rencontrée (NB: le contenu du fichier toto sur lequel s'applique la commande est rappelé dans le billet précédent).

Choisissons de remplacer la deuxième occurrence:

$ sed -n '/oui/s/Tata/Titine/2p' toto

Et oui Tata aime Toto et Toto aime Titine

Cette fois, nous imprimons seulement la ligne où la substitution a eu lieu. Dans ce script où /oui/ est la condition restrictive et s la commande, p n'est pas à proprement parler une commande, mais un des flags de la commande s.

Et si on demande le remplacement de la 3ième occurrence?

$ sed -n  '/oui/s/Tata/Titine/3p' toto

Nous n'avons aucun output car il n'y a pas de substitution.

Comme ceci:

$ sed -n  '/oui/{s/Tata/Titine/3;p}' toto

Et oui Tata aime Toto et Toto aime Tata

les lignes où « oui » est rencontré sont imprimées qu'il y ait substitution ou pas. p n'est plus un flag de s, mais une commande à part entière. A la condition restrictive /oui/ correspondent deux commandes séparées par un point-virgule et entourées d'accolades.

Passons maintenant en revue quelques manipulations qui peuvent être effectuées en utilisant sed et la commande s.

Ajout à la fin d'une ligne

Il suffit de prendre $ pour l'expression régulière intervenant dans s, puisque $ désigne la fin de la ligne.

$ sed -n '/belle/s/$/ C\o47est super!/p' toto

La vie est belle car Tata habite près de Toto. C'est super!

Dans la chaîne de remplacement, \o47 est mis pour l'apostrophe (47 en octal)

Considérons maintenant un fichier grub.conf dans lequel on voudrait introduire l'option vga afin d'adapter la résolution des terminaux virtuels.

La formule à utiliser est celle-ci:

$ sed -i.old '/^kernel/s/$/ vga=795/' grub.conf

^ désigne le début de la ligne, donc toutes les lignes qui commencent par kernel se voient ajouter l'option adéquate.

Il n'y aura pas d'output suite à l'utilisation de l'option i. Le fichier initial est sauvegardé sous le nom grub.conf.old, et grub.conf est directement modifié.

Conversion des fins de ligne Unix au format DOS

$ sed -n '1p' toto | od -An -tx1z

4a 65 20 6d 27 61 70 70 65 6c 6c 65 20 54 6f 74  >Je m'appelle Tot<

6f 2e 0a                                         >o..<

Nous pouvons constater en affichant en hexadécimal via od la ligne 1 du fichier toto, que les lignes de notre fichier se termine par le caractère de contrôle nouvelle ligne (0A en hexadécimal)

$ sed -n '1s/$/\r/p' toto | od -An -tx1z

4a 65 20 6d 27 61 70 70 65 6c 6c 65 20 54 6f 74  >Je m'appelle Tot<

6f 2e 0d 0a                                      >o...<

Suite à la substitution, on a maintenant une fin de ligne au format DOS avec les caractères de contrôle retour chariot (0D en hexadécimal) et nouvelle ligne.

Rappelons que dans l'espace de travail, lorsque la substitution s'effectue la ligne traitée est dépourvue de son caractère nouvelle ligne. La substitution ajoute un retour chariot auquel s'ajoute lors de l'impression un caractère nouvelle ligne.

Pour convertir tout le fichier, il suffit d'enlever la condition restrictive:

$ sed -n 's/$/\r/p' toto > toto.dos

Nous avons dirigé l'output vers le fichier toto.dos (qui sera créé) de sorte qu'il n'y a pas d'output à l'écran.

Vérifions:

$ od -An -tx1z -N 64 toto.dos

4a 65 20 6d 27 61 70 70 65 6c 6c 65 20 54 6f 74  >Je m'appelle Tot<

6f 2e 0d 0a 4a 65 20 76 61 69 73 20 c3 a0 20 6c  >o...Je vais .. l<

27 c3 a9 63 6f 6c 65 20 61 76 65 63 20 54 61 74  >'..cole avec Tat<

61 0d 0a 4e 6f 75 73 20 61 6c 6c 6f 6e 73 20 c3  >a..Nous allons .<

Conversion des fin de ligne DOS au format Unix

$ sed -n '1s/\r$//p' toto.dos | od -An -tx1z

4a 65 20 6d 27 61 70 70 65 6c 6c 65 20 54 6f 74  >Je m'appelle Tot<

6f 2e 0a         

Nous remplaçons par rien (nous supprimons) le retour chariot \r qui se trouve en fin de ligne après importation de celle-ci par sed. Ensuite un caractère nouvelle ligne est ajouté via la fonction d'impression déclenchée par le flag p.

Concaténation de deux lignes

$ sed -n '/appelle/{N;p}' toto

Je m'appelle Toto.

Je vais à l'école avec Tata

Lorsque « appelle » est rencontré, N ajoute à l'espace de travail la ligne suivante. Les deux lignes sont séparées par un caractère nouvelle ligne \n (0A en hexadécimal). Ensuite p imprime le contenu de l'espace de travail.

$ sed -n '/appelle/{N;s/\n/ /;p}' toto

Je m'appelle Toto. Je vais à l'école avec Tata

Cette fois avant l'impression, nous remplaçons le \n inclus dans l'espace de travail par un espace, avec comme résultat une concaténation des deux lignes.

Si une ligne se termine par un « \ », la concaténer avec la suivante

Notre fichier de travail sera le fichier toto.bis:

$ cat toto.bis

Je m'appelle Toto.\

Je vais à l'école avec Tata.\

Nous allons à pied car ce n'est pas loin.

Elle m'aime et ça c'est cool!

Et oui Tata aime Toto et Toto aime Tata.\

La vie est belle car Tata habite près de Toto.

Ils se voient tous les jours!

Procédons:

$ sed ':a;/\\$/{N;s/\\\n/ /};ta' toto.bis

Je m'appelle Toto. Je vais à l'école avec Tata. Nous allons à pied car ce n'est pas loin.

Elle m'aime et ça c'est cool!

Et oui Tata aime Toto et Toto aime Tata. La vie est belle car Tata habite près de Toto.

Ils se voient tous les jours!

Un « \ » est un caractère d'échappement, mais il perd cette qualité, il devient un caractère ordinaire, s'il est lui-même précédé d'un caractère d'échappement. Dans l'expression précédente, les « \ » sont des caractères d'échappement, tandis que les « \ » sont des caractères ordinaires. La commande ta effectue un branchement vers le label a seulement en cas de substitution. Cette boucle via le label a est nécessaire pour gérer le cas où plusieurs lignes successives se terminent par un « \ » (backslash).

Suppression des espaces et des tabulations en fin de ligne

Soit le fichier hello contenant « Hello! »:

$ cat hello

Hello!

L'utilisation de od permet de visualiser l'existence d'espaces (x20) et de tabulations (x09):

$ od -An -tx1z hello
48 65 6c 6c 6f 21 09 09 20 20 0a                 >Hello!..  .<

La présence de tels caractères de contrôles dans des fichiers de configuration système, n'est pas toujours recommandées. Il convient donc de les enlever, ce qui se fait aisément avec sed:

sed 's/[ \t]*$//' hello | od -An -tx1z
48 65 6c 6c 6f 21 0a                             >Hello!.<

Dans l'expression régulière, le meta-caractère * signifie de 0 à n occurrences du caractère précédent qui ici peut être un espace ou une tabulation, le tout étant situé en fin de ligne. Donc lorsque l'expression régulière est satisfaite, lorsque de 0 à n espaces ou tabulations sont rencontrés en fin de ligne, ceux-ci sont remplacés par rien, c'est-à-dire supprimés.

Numérotation des lignes

La commande « = » envoie directement vers le flux de sortie, sans transiter par l'espace de travail le numéro de la dernière ligne lue:

$ sed -n '3,5{=;p}' toto

3

Nous allons à pied car ce n'est pas loin.

4

Elle m'aime et ça c'est cool!

5

Et oui Tata aime Toto et Toto aime Tata

Pour mettre le numéro sur la même ligne que le texte qui suit, il faut diriger le flux de sortie du premier sed vers un deuxième sed. On peut alors à l'aide de la commande N, concaténer les lignes deux à deux, comme ceci:

$ sed -n '3,5{=;p}' toto | sed -n 'N;s/\n/\t/;p'

3       Nous allons à pied car ce n'est pas loin.

4       Elle m'aime et ça c'est cool!

5       Et oui Tata aime Toto et Toto aime Tata

On a remplacé dans l'espace de travail le caractère \n qui sépare les deux lignes à concaténer, par un caractère de tabulation \t. Cependant, dès l'instant où le nombre de chiffre du numéro de ligne augmente comme ci-après:

$ sed -n '9,11{=;p}' toto toto | sed -n 'N;s/\n/\t/;p'

9       Je vais à l'école avec Tata

10      Nous allons à pied car ce n'est pas loin.

11      Elle m'aime et ça c'est cool!

il convient, pour un résultat plus esthétique, d'aligner les chiffres sur la droite.

Après importation des deux lignes à concaténer, insérons chaque fois 5 espaces au début de l'espace de travail avant de remplacer le caractère \n par un espace:

$ sed -n '9,11{=;p}' toto toto | sed -n 'N;s/^/     /;s/\n/ /;p'

     9 Je vais à l'école avec Tata

     10 Nous allons à pied car ce n'est pas loin.

     11 Elle m'aime et ça c'est cool!

Le résultat n'est pas encore satisfaisant. Voici comment procéder:

$ sed -n '9,11{=;p}' toto toto | sed -nr 'N;s/^/     /;s/ *(.{6})\n/\1  /;p'

     9  Je vais à l'école avec Tata

    10  Nous allons à pied car ce n'est pas loin.

    11  Elle m'aime et ça c'est cool!

L'option -r indique que l'expression régulière qui intervient dans la deuxième substitution est une expression régulière étendue, ce qui implique que les parenthèses et les accolades qui y figurent sont des meta-caractères.

* correspond à 0 à n occurrences du caractère précédent (ici un espace)

{6} correspond à exactement 6 occurrences du caractère précédent (ici un caractère quelconque représenté par le meta-caractère « . », c'est-à-dire en fait un espace ou un chiffre)

Les parenthèses dans l'expression régulière sont des parenthèses de mémorisation: leur contenu est représenté dans la chaîne de remplacement par \1.

La façon dont l'expression régulière est décodée dépend du nombre de chiffres du numéro de ligne:

0 espace  + (5 espaces + 1 chiffres) + \n

1 espace  + (4 espaces + 2 chiffres) + \n

2 espaces + (3 espaces + 3 chiffres) + \n

….

Le contenu des parenthèses est repris dans la chaîne de remplacement suivi de deux espaces.

Centrer un texte

$ sed -r ':a;s/^.{1,78}$/ & /;ta' toto

                               Je m'appelle Toto.

                          Je vais à l'école avec Tata

                   Nous allons à pied car ce n'est pas loin.

                         Elle m'aime et ça c'est cool!

                    Et oui Tata aime Toto et Toto aime Tata

                 La vie est belle car Tata habite près de Toto.

                         Ils se voient tous les jours!

Dans la chaîne de remplacement, & est mis pour l'entièreté de l'expression régulière rencontrée.

La commande ta effectue un branchement vers le label a seulement si l'espace de travail a été modifié par la commande s.

{1,78} signifie de 1 à 78 occurrences du caractère précédent, le meta-caractère « . », qui désigne n'importe quel caractère.

En clair, pour chaque ligne lue, une boucle est exécutée dans le script qui ajoute un espace avant et après cette ligne tant que la ligne obtenue ne dépasse pas 78 caractères.

Mettre des séparateurs de milliers dans un nombre.

$ echo '1234567 123456 12345 1234 123' | sed -r ':a;s/\B[0-9]{3}\b/.&/;ta'

1.234.567 123.456 12.345 1.234 123

[0-9] est mis pour n'importe quel chiffre (de0 à 9).

Chaque fois qu'un groupe de 3 chiffres est trouvé qui se trouve à la fin d'un mot (condition \b) et qui ne commence pas au début d'un mot (condition \B), ce groupe est remplacé par lui-même (&) précédé d'un point.

Le processus recommence après chaque remplacement effectué.

Regardons de plus près comment évolue la chaîne traitée, au cours des différentes boucles.

1234567 123456 12345 1234 123

1234.567 123456 12345 1234 123

1.234.567 123456 12345 1234 123

1.234.567 123.456 12345 1234 123

1.234.567 123.456 12.345 1234 123

1.234.567 123.456 12.345 1.234 123

Le groupe de 3 chiffres trouvé (et qui sera précédé d'un point à l'étape suivante) est marqué en rouge. Le groupe 567 qui est trouvé lors du premier passage ne l'est plus ensuite. En effet, le point étant un séparateur de mot, 567 est devenu lui-même un mot, 567 commence un mot, ce qui est une condition d'exclusion.

Normalisation d'une liste avec des prix

Nous disposons d'un fichier nommé prix et comprenant des prix, tel que celui-ci:

item 1  1.25,95

  item 2   12.50

voiture 18,4v55*,3 xxx

jeudi:

billets d'avion          1544,2

item 3      8,
carburant 42,653
item 4 ,95

et nous aimerions le normaliser: mettre les séparateurs de milliers à bon escient, introduire les centimes, aligner les prix ….

Le nombre d'instructions du script devenant conséquent, il devient intéressant de mettre ces instructions dans un fichier. Passons en revue les différentes instructions qui vont figurer dans ce fichier.

Commençons par supprimer toutes les tabulations, les espaces inutiles en début de ligne:

s/^[ \t]*//

Supprimons tout ce qui n'est pas chiffre à la fin:

s/[^0-9]*$//

Le symbole ^ n'est pas ici l'indicateur de début de ligne mais plutôt un symbole de négation:[^0-9] est la classe complémentaire de [0-9], donc une classe qui contient tout ce qui n'est pas chiffre (sauf \n). Tout ensemble de 0 à n non-chiffre ancré en fin de ligne, sera supprimé.

Si une ligne ne comprend aucun chiffre, elle sera vidée par l'instruction précédente. Supprimons les lignes vides:

/^$/d

(Une ligne vide est telle que l'indicateur de fin de ligne est collé à l'indicateur de début de ligne).

Remplaçons les chaînes d'au moins un espace ou une tabulation par un seul espace:

s/[ \t]+/ /g

Le meta-caractère + a un sens proche du meta-caractère *: il signifie de 1 à n occurrences du caractère précédent.

On supprime tout ce qui dans le prix n'est pas chiffre ou virgule:

:a;s/[^0-9, ]([0-9,]*)$/\1/;ta

La chaîne recherchée pour être remplacée ne commence ni par un chiffre ni une virgule, ni par un espace, ensuite elle comprend uniquement des chiffres et des virgules. La chaîne de remplacement est la chaîne trouvée amputée de son premier caractère. Analysons en détail ce qui se passe pour le le prix 18,4v55*,3 en marquant en rouge la chaîne trouvée à chaque passage pour être remplacée:

Passage 1              18,4v55*,3

Passage 2              18,4v55,3

Passage 3               18,455,3

Il n'y a plus de suppression possible et le branchement ta devient inopérant.

On ne conserve qu'une seule virgule(la dernière):

:b;s/,([0-9]*,)/\1/;tb

Explication: le motif recherché est constitué d'une virgule suivie de 0 à n chiffres et encore d'une virgule. Le motif de remplacement est la partie du motif recherché qui est entre parenthèses, ce qui revient à supprimer la première des 2 virgules. L'opération est éventuellement recommencée plusieurs fois.

Pour les lignes se terminant par une virgule suivie d'un seul chiffre, on ajoute 0 à la fin:

s/,[0-9]{1}$/&0/

(Rappelons que & représente la chaîne trouvée et destinée à être remplacée)

S'il y a plus de deux chiffres après la virgule, on les supprime:

s/(,[0-9]{2}).+$/\1/

Explication: le motif recherché est constitué d'une virgule suivie de 2 chiffres et encore d'au moins un caractère, le tout ancré en fin de ligne. On ne conserve que la virgule et les deux chiffres qui suivent.

Si la ligne ne se termine pas par une virgule suivie de 2 chiffres, on ajoute ,00 à la fin:

/,[0-9]{2}$/!s/$/,00/

Explication: ! Indique la négation. Donc la substitution s'effectue seulement si une virgule suivie de deux chiffres ne sont pas ancrés en fin de ligne.

Insérons un zéro avant la virgule si elle n'est pas précédée par un chiffre:

S/([^0-9])(,[0-9]{2}$)/\10\2/

Explication: le motif à remplacer est un non-chiffre suivi d'une virgule et de deux chiffres terminant la ligne. Il est accommodé de 2 paires de parenthèses mémorisantes et la chaîne remplaçante est construite en insérant un 0 entre les deux parties mémorisées de la chaîne à remplacer (\1 correspond au contenu de la première paire de parenthèses mémorisantes et \2 au contenu de la deuxième paire).

Il reste à ajouter  les séparateurs de milliers, aligner les prix à droite:

:c;s/\B[0-9]{3}\b/.&/;tc

Explication: voir plus haut

:f;/^.{1,39}$/s/[0-9\.\,]*$/ &/;tf

Explication: le fonctionnement est le même que pour centrer un texte sauf que & est précédé d'un espace au lieu d'être suivi et précédé par un espace.

Ajoutons encore le symbole de l'unité monétaire:

s/$/ €/

Plaçons toutes ces instructions dans un fichier appelé norme-prix . En voici le contenu:

#!/bin/sed -rf

 

# supprime les tabulations et espaces au début

s/^[ \t]*//

# supprime tous ce qui n'est pas chiffres à la fin

s/[^0-9]*$//

# supprime les lignes vides

/^$/d

# remplace les chaînes de au moins un espace ou une tabulation par un seul espace

s/[ \t:]+/ /g

# supprime les caractères autres que chiffres et virgule à l'intérieur des prix

:a;s/[^0-9, ]([0-9,]*)$/\1/;ta

# ne conserve qu'une virgule

:b;s/,([0-9]*,)/\1/;tb

# un seul chiffre après la virgule: ajoute 0

s/,[0-9]{1}$/&0/

# ne garde que les deux chiffres après la virgule

s/(,[0-9]{2}).+$/\1/

# si le prix ne se termine pas par une virgule et 2 chiffres: ajoute,00

/,[0-9]{2}$/!s/$/,00/

# si pas de chiffres devant la virgule met un 0

s/([^0-9])(,[0-9]{2}$)/\10\2/

# insère les séparateurs de milliers

:c;s/\B[0-9]{3}\b/.&/;tc

# aligne les prix à droite

:f;/^.{1,39}$/s/[0-9\.\,]*$/ &/;tf

# ajoute le symbole monétaire

s/$/ €/

Nous pouvons maintenant exécuter sed avec l'option -f qui indique que les instructions se trouvent dans le fichier dont le nom suit:

$ sed -rf norme-prix prix

item 1                            125,95 €

item 2                          1.250,00 €

voiture                        18.455,30 €

billets d'avion                 1.544,20 €

item 3                              8,00 €

carburant                          42,65 €

item 4                              0,95 €

La ligne shebang, pour autant que norme-prix ait été rendu exécutable, permet aussi de procéder comme suit:

$ ./norme-prix prix

item 1                            125,95 €

item 2                          1.250,00 €

voiture                        18.455,30 €

billets d'avion                 1.544,20 €

item 3                              8,00 €

carburant                          42,65 €

item 4                              0,95 €