mardi 1 juin 2010

Stockage CHS

Décodage stockage CHS

Dans le billet précédent, nous avons avec la commande od affiché une table de partitions et nous en avons expliqué le contenu, mis à part certaines zones qui ont été ignorées. Il s'agit des zones où  sont enregistrées les données CHS (Cylindre,Tête, Secteur) relatives au début et à la fin des partitions.
Nous allons expliquer comment décoder les données CHS que l'on peut trouver dans une table de partitions.
Affichons avec od la table de partitions du disque utilisé comme exemple:
# od -Ad -tx1 -j 446 -N 66 /dev/sda
0000446 00 01 01 00 0b ef bf 5e 3f 00 00 00 b1 0a 8c 00 
0000462 80 01 81 5f 07 ef ff ff 2f 0b 8c 00 d1 47 1c 04 
0000478 00 ef ff ff 83 ef ff ff 00 53 a8 04 10 3b 00 00 
0000494 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 
0000510 55 aa 
La colonne 1 de la table nous indique que la partition sda2 est marquée comme étant amorçable.
La colonne 5 nous apprend que:
sda 1 est une partition FAT 32
sda2 est une partition NTFS
sda3 est une partition linux
(Voir billet précédent).
Le secteur qui contient cette table de partition est le secteur 0 du disque: il se trouve sur le cylindre 0 et est lu par la tête 0. Cependant dans le système d'adressage CHS, les secteurs sont comptés à partir de 1.
Son adresse CHS est donc
CHS=(0,0,1)
Pour les autres adresses CHS, nous allons maintenant considérer les colonnes sur fond orange (données CHS du début de la partition) et sur fond gris (données CHS de fin de la partition).
Le premier octet se rapporte à la tête, les deux suivant à l'ensemble secteur cylindre.
Ainsi pour le début de la partition 1, il semble évident que:
CHS=(0,1,1)
Ce n'est pas faux, mais en fait, c'est un peu plus compliqué que cela.
En effet si le premier octet donne le numéro de la tête,  dans le deuxième octet, seuls les bits 0 à 5 correspondent au  numéro du secteur. Les bits 6 et 7  constituent les bits supérieurs (8 et 9) du numéro de cylindre. Le numéro de cylindre est donc un nombre à 10 bits dont le maximum est 1023.
Le tableau suivant montre ce qu'il en est pour le stockage des données relatives à la fin de la partition sda1:

Tête
Secteur Cylindre
E
F
B
F
5
E
1
1
1
0
1
1
1
1
1
0
1
1
1
1
1
1
0
1
0
1
1
1
1
0
7
6
5
4
3
2
1
0
7
6
5
4
3
2
1
0
7
6
5
4
3
2
1
0
La ligne 2 du tableau reprend les données en hexadécimal provenant de la table de partitions, la ligne 3 reprend les mêmes données en binaire. En ligne 4, on trouve la numérotation des bits.
Les bits sur fond jaune se rapport au numéro de cylindre, les bits sur fond vert au numéro de secteur.
Réorganisons le tableau de manière à mettre ensemble les bits qui constituent le numéro de cylindre:

Tête
Secteur
Cylindre
E
F
3
F
2
5
E
1
1
1
0
1
1
1
1
1
1
1
1
1
1
1
0
0
1
0
1
1
1

0
7
6
5
4
3
2
1
0
5
4
3
2
1
0
9
8
7
6
5
4
3
2
1
0
239
63
606
(On a ajouté au tableau une 5ième ligne qui donne l'équivalent en décimal des nombres hexadécimaux de même couleur).
L'adresse CHS de la fin de la partition est donc:
CHS=(606,239,63)
Dessinons des tableaux du même genre pour rechercher l'adresse CHS du début de sda2:

Tête
Secteur Cylindre
0
1
8
1
5
F
0
0
0
0
0
0
0
1
1
0
0
0
0
0
0
1
0
1
0
1
1
1
1
1
7
6
5
4
3
2
1
0
7
6
5
4
3
2
1
0
7
6
5
4
3
2
1
0

Tête
Secteur
Cylindre
0
1
0
1
2
5
F
0
0
0
0
0
0
0
1
0
0
0
0
0
1
1
0
0
1
0
1
1
1
1
1
7
6
5
4
3
2
1
0
5
4
3
2
1
0
9
8
7
6
5
4
3
2
1
0
1
1
607
On trouve:
CHS=(607,1,1)
Les autres données CHS qui figurent dans la table de partition étudiée sont toutes égales à EF FF FF FF ce qui donne des numéros de cylindre valant tous 3FF (1023). Ces données ne sont donc d'aucune utilité pour établir des adresses CHS.
Cependant, il existe des formules permettant de calculer l'adresse CHS à partir du système d'adressage LBA (Logical Block Adress) dans lequel les N blocs logiques d'un disque sont numérotés de 0 à N1. Les blocs logiques sont ici identiques aux secteurs et l'adresse LBA du début des partitions est celle qui figure dans la table de partitions occupant les colonnes 9 à 12. Pour l'adresse de fin, il suffit d'ajouter la taille (en secteurs) moins 1. Si A désigne l'adresse LBA, NS le nombre de secteurs par piste et NT le nombre de têtes, les formules sont les suivantes:
S = ( A % NS ) + 1
H = ( A - S + 1 ) / NS % NT
C = ( A - S + 1 ) / NS / NT
(% est l'opérateur modulo)
Pour notre disque exemple NS=63 et NT=240
Calculons l'adresse CHS de la fin de la partition sda1, pour vérifier si on retrouve bien les valeurs directement lues dans la table des partitions.
Rappelons que les adresses en secteurs (en blocs logiques) des partitions se présentent dans la table de partitions sous forme d'un nombre hexadécimal de 32 bits au format little-endian. Idem pour les tailles qui elles se trouvent aux colonnes 13 à 16. On pourrait convertir ces adresses et ces tailles en nombre décimal, ou même les afficher directement sous ce format à l'aide de od (voir billet précédent), mais la puissance extraordinaire de la ligne de commande linux fait qu'il n'est même pas nécessaire d'effectuer ces conversions:
$ A=$((0x3f+0x8c0ab1-1))
$ S=$(((A%NS)+1))
$ H=$(((A-S+1)/NS%NT))
$ C=$(((A-S+1)/NS/NT))
$ echo $C,$H,$S
606,239,63
Procédons de même pour la fin de la partition sda2:
A=$((0x8c0b2f+0x41c47d1-1))
$ S=$(((A%NS)+1))
$ H=$(((A-S+1)/NS%NT))
$ C=$(((A-S+1)/NS/NT))
$ echo $C,$H,$S
5167,239,63
Ces dernières valeurs ne peuvent pas être stockées dans la tables de partitions à l'emplacement prévu, puisque le numéro du cylindre est supérieur à 1023.

mercredi 19 mai 2010

od et table de partitions

Nous désirons afficher le contenu de la table de partitions qui se trouve à l'adresse 446 du secteur 0 (MBR) d'un disque
Pour ce faire, utilisons la commande od avec les options
- j 446 pour sauter (jump) 446 octets
- N 64 pour limiter l'ouput à 64 octets
puisque nous savons que cette table contient 4 lignes de 16 octets chacune:
 # od -j 446 -N 64 /dev/sdb
0000676 000400 000001 177007 177777 000077 000000 007503 002152
0000716 177000 177777 177005 177777 111016 002154 023762 002436
0000736 177200 177777 177203 177777 007602 002152 032311 000002
0000756 177000 177777 177203 177777 042113 002154 037301 000000
0000776
L'output n'est pas celui souhaité. Les lignes de la table de partitions se présentent sous forme de 8 chiffres codés sur 2 octets, chaque ligne étant précédée de son adresse. On voit que les adresses varient par incrément de 20. Or 20 en octal vaut 16 en décimal. Les adresses seraient elles en octal?
Vérifions:
# echo $((6*64+7*8+6))
446
  
Bingo! Et bien oui: od signifie octal display. Mais pas de panique, si l'affichage s'effectue en octal par défaut, on peut changer cela grâce à de multiples options:
-Ad pour avoir les adresses en décimal
-tx1 pour afficher le contenu sous forme de nombre hexadécimal codé sur un octet.
# od -j 446 -N 66 -Ad -tx1 /dev/sdb
0000446 00 01 01 00 07 fe ff ff 3f 00 00 00 43 0f 6a 04
0000462 00 fe ff ff 05 fe ff ff 0e 92 6c 04 f2 27 1e 05
0000478 80 fe ff ff 83 fe ff ff 82 0f 6a 04 c9 34 02 00
0000494 00 fe ff ff 83 fe ff ff 4b 44 6c 04 c1 3e 00 00
0000510 55 aa
0000512

Nous avons cette fois demandé la lecture de 66 octets afin que s'affiche le nombre magique 55aa (à l'adresse 510), sorte de signature qui indique la fin du secteur d'amorçage.
La colonne  1 de la table (en vert) contient uniquement des 00 sauf si la partition est marquée comme
étant amorçable: dans ce cas elle contient 80
La colonne 5 (en jaune) donne le type de partition:

0b = FAT 32
07 = NTFS
05 = Étendue
83 = Linux
Les 4 colonnes en turquoise correspondent à l'adresse (en secteurs) de la partition, les 4 colonnes en saumon sa taille (en secteurs toujours).
Comme la première partition commence normalement au secteur 63 (3f en hexadécimal), on constate que les adresses des partitions se présentent sous forme d'un nombre hexadécimal codé sur 4 octets certes,
mais au format little-endian, c'est-à-dire avec l'octet de poids le plus faible à gauche. Il en est logiquement de même pour les tailles.
Calculons la taille de la première partition, c'est-à-dire le nombre décimal correspondant à 43 0f 6a 04:
$ echo $(((4*16+3)+(0*16+15)*256+(6*16+10)*65536+(0*16+4)*16777216))
74059587
L'opération est assez fastidieuse, mais il suffit de réécrire le nombre hexadécimal avec en premier l'octet de poids le plus fort pour arriver aisément au résultat:
$ echo $((0x046a0f43))
74059587
Cependant la table de partition comprend encore 6 nombres de ce type pouvant être calculés.
Pourquoi ne pas demander à od d'effectuer cela pour nous!
Il suffit de remplacer l'option -tx1 par l'option -tu4 qui affiche le contenu sous forme d'un nombre décimal non s
igné, codé sur 4 octets:
# od -j 446 -N 64 -Ad -tu4 /dev/sdb
0000446      65792 4294966791       63 74059587
0000462 4294966784 4294966789 74224142 85862386
0000478 4294966912 4294966915 74059650   144585
0000494 4294966784 4294966915 74204235    16065
Oui, od est vraiment une petite merveille.

Par curiosité regardons ce qui se trouve à l'adresse 446 du VBR (Volume Boot Record), secteur 0 de la partition 1 et ajoutons z au type x1 de manière à afficher à la suite de chaque ligne générée les caractères imprimables qui y figurent:
# od -j 446 -N 66 -Ad -tx1z /dev/sdb1
0000446 0d 0a 45 6e 74 72 65 7a 20 43 74 72 6c 2b 41 6c >..Entrez Ctrl+Al<
0000462 74 2b 53 75 70 70 72 20 70 6f 75 72 20 72 65 64 >t+Suppr pour red<
0000478 82 6d 61 72 72 65 72 0d 0a 00 0d 0a 00 00 00 00 >.marrer.........<
0000494 00 00 00 00 00 00 00 00 00 00 83 99 a8 be 00 00 >................<
0000510 55 aa >U.<
0000512
Il n'y a rien à cette adresse qui ressemble de près ou de loin à une table de partitions.
Remarquons que l'on pouvait afficher le contenu de cette région du disque en utilisant son adresse absolue, puisque la table de partitions nous donne l'adresse en secteur de la partition 1, c'est-à-dire 63.
# od -j $((63*512+446)) -N 66 -Ad -tx1z /dev/sdb
0032702 0d 0a 45 6e 74 72 65 7a 20 43 74 72 6c 2b 41 6c >..Entrez Ctrl+Al<
0032718 74 2b 53 75 70 70 72 20 70 6f 75 72 20 72 65 64 >t+Suppr pour red<
0032734 82 6d 61 72 72 65 72 0d 0a 00 0d 0a 00 00 00 00 >.marrer.........<
0032750 00 00 00 00 00 00 00 00 00 00 83 99 a8 be 00 00 >................<
0032766 55 aa >U.<
0032768
Donc les VBR ne contiennent pas de table de partitions.
Essayons ceci:
# od -j 446 -N 66 -Ad -tx1 /dev/sdb2
0000446 00 fe ff ff 83 fe ff ff 01 00 00 00 f1 fe 82 02
0000462 00 fe ff ff 05 fe ff ff f2 fe 82 02 40 9c 8a 02
0000478 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
0000510 55 aa 0000512
(Le * représente une ligne identique à la précédente)
 Oops, nous venons d'écrire que les VBR ne contiennent pas de table de partitions et pourtant nous voyons ici ce qui ressemble fort à  une table de partitions avec deux lignes non triviales!
Oui mais sdb2 n'est pas une partition primaire, ma
is une partition étendue et son secteur 0 n'est pas un VBR mais un EBR (Extended Boot Record).
La première ligne de cette pseudo-table de partitions correspond à une partition de type linux  (83) qui n'apparaît pas dans la table de partitions du MBR: il s'agit d'une partition logique notée sdb5 (les partitions décrites dans le MBR sont numérotées de 1 à 4). Son adresse est relative non pas au disque mais à la partition étendue sdb2. Nous voyons, puisque l'adresse est 1, que son VBR (et oui les partitions logiques ont aussi un VBR) vient juste après le EBR de sdb2. Notons que ceci n'est pas un standard, mais correspond à la réalité du disque examiné.
Quant à la deuxième ligne, elle correspond à une pseudo-partition étendue (type 05) qui est le container de sdb6, la partition logique suivante. Nous voyons que ce container commence juste après sdb5. Le secteur 0 de ce container est aussi un EBR. Nous voudrions afficher la pseudo-table de partition qui y figure.
L'adresse relative (donc par rapport à sdb2) du container de sdb6 est:
$ echo $((0x0282fef2))
42139378
Le nombre à communiquer à od via l'option -j, est celui-là  multiplié par 512.
Alors od est peut-être une petite merveille, mais il ne fonctionnera pas pour une valeur de j aussi élevée.
C'est ici qu'on a
ppelle dd à la rescousse:
# dd skip=$((0x046c920e+0x0282fef2)) count=1 if=/dev/sdb | od -Ad -tx1 -j 446
1+0 enregistrements lus
1+0 enregistrements écrits
512 octets (512 B) copiés, 5,7541e-05 s, 8,9 MB/s
000446 00 fe ff ff 83 fe ff ff 3f 00 00 00 01 9c 8a 02
000462 00 fe ff ff 05 fe ff ff 32 9b 0d 05 c0 8c 10 00
000478 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
000510 55 aa
000512
On part de sdb et non pas de sdb2: il faut travailler avec les adresses absolues. De ce fait, dd doit sauter un nombre de blocs (taille par défaut 512 octets) donné par l'adresse de la partition étendue sdb2 à laquelle s'ajoute l'adresse relative du container de sdb6. Il lit ensuite un seul bloc (count=1). C'est ce bloc unique qui est transmis à od. L'option -N y devient donc inutile.
La première ligne de la pseudo-table de partitions du container de sdb6, décrit la partition logique sdb6 elle-même. Elle est de nouveau de type linux et son adresse par rapport à son propre container (qui est aussi l'adresse de son VBR) est de 63 secteurs. La deuxième ligne donne l'adresse relative à sdb2 (et la taille) d'un nouveau container , celui qui contient sdb7. Pour afficher la pseudo-table suivante, il suffit de remplacer dans la commande précédente l'adresse relative du container sdb6 par celle du container sdb7.
# dd skip=$((0x046c920e+0x050d9b32)) count=1 if=/dev/sdb | od -Ad -tx1 -j 446
1+0 enregistrements lus
1+0 enregistrements écrits
512 octets (512 B) copiés, 0,000309911 s, 1,7 MB/s
0000446 00 fe ff ff 82 fe ff ff 3f 00 00 00 81 8c 10 00
0000462 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
*
0000510 55 aa
0000512
La partition décrite en ligne 1 (sdb7) est une partition de type swap (type 82), qui se trouve à l'adresse 63 de son container. Il n'y a plus de nouveau container: donc cette partition logique est la dernière de la partition étendue sdb2.
En résumé, le secteur 0 de chaque container de partition logique est un EBR qui contient une pseudo-table de partitions.
La première ligne de cette pseudo-table décrit la partition logique contenue: type, adresse par rapport à l'origine du container (le plus souvent 63) et taille. Le secteur 0 de cette partition
logique est un VBR.
La deuxième ligne si elle existe décrit le container suivant: adresse par rapport à l'origine de la partition étendue et taille.
Toutes ces données doivent être cohérentes: ainsi la taille du container vaut l'adresse relative de la part
ition logique contenue + la taille de cette partition.
Vérifions le pour sdb7:

$ echo $((0x108cc0-(0x3f+0x108c81)))
0
Les différents containers sdb5, sdb6, sdb7 ne constituent pas un système de poupées russes: il sont situés côte à côte.
Ainsi l'adresse du container de sdb7 s'obt
ient à partir de l'adresse + la taille du container de sdb6:
# echo $((0x050d9b32-(0x0282fef2+0x028a9c40)))
0
La somme de la taille de ces containers donnent la taille du super container, la partition étendue sdb2:

$ echo $((0x0282fef2+0x028a9c40+0x108cc0-0x051e27f2))
0
Il ne faut pas confondre les containers sdb5, sdb6, sdb7 et les partitions logiques proprement dites dont les tailles additionnées ne redonnent d'ailleurs pas la taille de la partition étendue.

$ echo $((0x0282fef1+0x028a9c01+0x108c81-0x051e27f2))
-127
127 correspond aux 3 EBR et à l'espace vide entre ces EBR et les VBR.
On peut représenter schématiquement la situation comme ceci (proportions non respectées):

(Les VBR font partie des partitions logiques: ils en occupent le secteur 0)




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 €