Si vous maîtrisez vi, alors vous savez utiliser sed sans même le savoir ! En effet, les commandes de sed reprennent essentiellement celles de ed, vous savez, l'éditeur de texte orienté ligne dont vi est une extension visuelle pleine page !
Là, le vocabulaire risque d'être compliqué ! Il ne faut pas confondre la ligne de commande du programme sed avec les commandes internes à sed. Ce paragraphe est dédié aux options de la ligne de commande du programme.
La syntaxe générale d'appel de sed est la suivante :
sed [-e commande] [-f scriptsed] [-n] [fichier]
Nous attaquons désormais la section consacrée à
l'étude des commandes internes de sed,
Lorsque vous tappez un script sed, il vous est possible d'indenter les commandes à votre guise en les faisant précéder d'espaces et/ou de tabulations. Toutefois, il ne faut surtout pas laisser trainer d'espaces à la suite de vos commandes (trailing blanks en Anglais) sous peine de générer des erreurs de sed.
La syntaxe d'une ligne de commande sed est la suivante :
portée commande options
Comme dans vi, nous retrouvons ici la notion de portée d'application d'une commande, c'est à dire l'étendue des lignes à laquelle la commande doit s'appliquer. Toutefois, la signification de la portée dans les 2 cas est inversée ! En effet, dans vi, si vous ne spécifiez pas de portée, la commande est exécutée uniquement sur la ligne courante alors que sed applique par défaut chaque commande à l'intégralité des lignes du fichier en entrée.
Pour résumer, en une phrase, dans sed la portée est restrictive alors que dans vi elle est extensive.
La portée peut recouvrir plusieurs formes :
debut texte non traite texte non traite BEGIN texte traite texte traite texte traite texte traite END texte non traite texte non traite BEGIN texte traite texte traite texte traite texte traite BEGIN texte traite texte traite texte traite END texte non traite texte non traite texte non traite texte non traite BEGIN texte traite texte traite texte traite texte traite END texte non traite texte non traite texte non traite END texte non traite texte non traite texte non traite fin
1 2 3 4 5 5 5 6 7 8 9
1 2 3 4 5 5 5 6 7 8 9
Il est important de noter que vous pouvez spécifier l'inverse d'une portée, c'est à dire, les lignes ne vérifiant pas la condition, en faisant suivre la spécification de portée par le caractère "!". Il est possible d'inverser tout type de portée, simple ou double.
Toutes les commandes peuvent accepter une portée simple, certaines accepteront en plus une portée double. Ceci sera précisé dans l'étude individuelle de chaque commande.
Une ligne de commentaire est de la forme :
J'espère que vous avez noté la présence d'un espace après le signe #. Si celui ci n'est pas obligatoire, il est fortement recommandé car certaines commandes « cachées » de sed sont associées à des commentaires spéciaux. En outre, il faut impérativement commencer les commentaires en première colonne.
Par exemple, et là, il s'agit d'une commande documentée, il est possible de remplacer l'option -n de la ligne de commande par le commentaire #n.
C'est la commande la plus simple de sed. Son but est d'afficher les lignes concernées par sa portée (qui peut être simple ou double) spécialement si l'option de ligne de commande -n est présente.
Par exemple, le tableau suivant présente, dans la colonne de gauche, le texte que nous allons traiter avec sed ; dans la colonne centrale, la commande sed que nous allons appliquer ; et pour finir, dans la dernière colonne, le résultat de l'application de sed -n commande texte qui nous montre bien que seules les lignes contenant l'expression régulière de portée sont affichées.
Texte initial | Script | Résultat |
Un deux trois Un deux deux trois Quatre trois Cinq Quatre |
/trois/p | Un deux trois deux trois Quatre trois Cinq Quatre |
La commande sed -n -e "/re/p" fichier est équivalente à grep "re" fichier.
La commande l (un L minuscule) est une prochaine cousine de p à une ENORME différence près : les caractères non affichables sont représentés par la chaîne "\0code_ascii_octal", ce qui permet de les repérer facilement !
Assurément, c'est la commande la plus utilisée de sed ! En effet, elle permet de remplacer du texte par un autre avec des options particulièrement évoluées, en particulier, des expressions régulières dans le motif de recherche et le motif de remplacement. Son format générique est le suivant :
[portee]s sep motif_recherche sep motif_remplacement sep options
Commentons cette commande qui peut paraître compliquée au premier abord en décomposant chacun de ces éléments :
L'une des grandes forces des éditeurs de texte d'Unix (et sed ne déroge pas à la règle) réside dans leur capacité à traiter des expressions régulières dans les motifs de remplacement.
Les caractères significatifs sont les suivants :
Supposez que vous disposiez d'une structure de données avec trois champs nommés obc, tch et aze auxquels vous accèdez par notation pointée. Vous décidez alors que votre structure pourra ajouter dynamiquement des champs et que ceux-ci pourront être accédés par adressage associatif sur leur nom à l'aide de l'opérateur crochet. Typiquement l'expression variable.champ deviendra variable["champ"]. La commande de remplacement peut s'écrire facilement avec sed sous la forme :
"s/\.\(aze\|obc\|tch\)/[\"\1\"]/g"
Examinons chacune des composantes de cette commande :
Utilisons dorénavant notre expression de remplacement sur le fichier suivant (que nous supposerons appelé essai.txt)
printf("%d",tlkp.aze); lop[1].obc = var2.obc ; Z = var3.tch + var3.tkp; var1.aze = aze;
La commande sed sera la suivante :
sed -e "s/\.\(aze\|obc\|tch\)/[\"\1\"]/g" essai.txt
et son résultat :
printf("%d",tlkp["aze"]); lop[1]["obc"] = var2["obc"] ; Z = var3["tch"] + var3.tkp; var1["aze"] = aze;
Vous noterez que nous avons eu le résultat attendu ! En particulier :
Autre cas aussi intéressant, nous avons un fichier présenté sous forme d'une liste de couples de la forme (a,b) un couple par ligne et nous souhaitons l'écrire (a,b) devient (b,a). Comment pouvez vous le faire ?
Afin d'écrire l'expression de remplacement, décomposons le problème : Il s'agit d'identifier deux expressions séparées par une virgule et entourées par des parenthèses puis de les échanger. Le fait que l'on manipule les deux expressions impose de les traiter comme des groupes. On obtient alors :
s/(\(.*\),\(.*\))/(\1,\2) devient (\2,\1)/
Les expressions recherchées sont tout simplement des chaînes quelconques identifiées comme la répétition du caractère joker autant de fois que nécessaire. L La virgule centrale sert à délimiter la droite de l'expression de gauche et la gauche de l'expression de droite.a grande difficulté lors de la construction de cette expression régulière concerne les parenthèses. En effet, certaines font partie du motif à rechercher, les autres (celles précédées par un \) sont des caractères de contrôles permettant d'identifier les groupes.
Bien que la commande précédente soit juste, il aurait été plus simple de l'écrire :
s/(\(.*\),\(.*\))/& devient (\2,\1)/
où la commande & remplace toute la chaîne reconnue par le motif de gauche. Ceci permet d'éviter d'utiliser à nouveau une construction avec le caractère \, lequel a tendance à nuire à la lisibilité de l'ensemble.
Un application sympa de sed permet de modifier l'extension d'un grand nombre de fichiers en utilisant également find et xargs. La construction d'une telle commande est fournie en exercice corrigé.
Maintenant que vous êtes familiers avec 2 commandes simples, il est temps de s'attaquer à la notion d'espace de travail tant celle ci est centrale pour la compréhension de sed.
Soit un script sed contenant plusieurs commandes, comment sont elles évaluées, et dans quel ordre ? Dans le cas le plus simple, sed lit une ligne, la place dans son espace de travail et applique successivement sur cet espace de travail toutes les commandes dont la portée est compatible avec l'espace de travail. Une fois toutes les opérations passées en revues, le contenu du tampon de travail est affiché (à moins que l'option -n n'ait été présente sur la ligne de commande en l'absence de toute commande d'affichage).
Considérons par exemple, le script et le texte suivant :
Texte initial | Script sed |
Premiere ligne Seconde ligne Troisieme ligne vide |
/Seconde/s/ligne/& vide/ /vide/p |
Si nous étudions le script, la première ligne remplace la chaîne "ligne" par "ligne vide" sur toutes les lignes contenant le mot "Seconde" alors que la seconde affiche les lignes contenant le mot vide.
Etudions l'action du script pas à pas :
Tampon de travail avant la commande | Commande | Résultat | Tampon après la commande | Etat courant de l'affichage du script | |
Lecture de la première ligne | Première ligne | ||||
Première ligne | /Seconde/s/ligne/& vide/ | Non exécutée car ne vérifie pas la condition de portée : tampon non modifié | Première ligne | ||
Première ligne | /vide/p | Non exécutée car ne vérifie pas la condition de portée : tampon non modifié et aucun affichage | Première ligne | ||
Première ligne | Toutes les commandes du script ont été considérée : on passe à la ligne de fichier suivante. Si la ligne de commande n'avait pas contenu l'option -n, le tampon de travail aurait été affiché préalablement. | Seconde ligne | |||
Seconde ligne | /Seconde/s/ligne/& vide/ | La condition de portée est vérifiée : il y a exécution de la commande de remplacement et modification du tampon | Seconde ligne vide | ||
Seconde ligne vide | /vide/p | La condition de portée est vérifiée : il y a exécution de la commande d'affichage | Seconde ligne vide | Seconde ligne vide | |
Seconde ligne vide | Toutes les commandes du script ont été considérée : on passe à la ligne de fichier suivante. Si la ligne de commande n'avait pas contenu l'option -n, le tampon de travail aurait été affichée préalablement, ce qui, dans ce cas, aurait fait double effet avec la commande p | Troisième ligne vide | Seconde ligne vide | ||
Troisième ligne vide | /Seconde/s/ligne/& vide/ | Non exécutée car ne vérifie pas la condition de portée : tampon non modifié | Troisième ligne vide | ||
Troisième ligne vide | /vide/p | La condition de portée est vérifiée : il y a exécution de la commande d'affichage | Troisième ligne vide | Seconde ligne vide Troisième ligne vide |
|
Troisième ligne vide | Toutes les commandes ont été passées en revue, et il n'y a plus de ligne à lire : le script est terminé | Seconde ligne vide Troisième ligne vide |
Vous noterez les points suivants :
Ces commandes permettent d'ajouter un texte statique avant (insertion) ou après (ajout) une ligne spécifiée dans l'adresse. A ce propos, l'adresse doit être simple : numéro de ligne unique ou expression régulière. Il n'est pas possible de passer une adresse de type plage qui n'aurait d'ailleurs pas de sens. La syntaxe générale est la suivante :
Vous remarquez tout de suite que ces commandes sont sur plusieurs lignes, ce qui est plutôt rare pour sed. En fait la difficulté résidait dans la spécification du texte à ajouter. Celui-ci doit commencer sur la ligne suivant la commande, seule la dernière ligne ne devant pas se terminer par un backslash \. Si le texte à ajouter doit contenir un caractère backslash, celui-ci doit être doublé afin d'être banalisé.
Afin de clarifier notre propos, étudions l'effet de ces commandes. Pour ce faire, nous allons traiter le petit exemple suivant. Soit le texte initial :
Avant le debut DEBUT apres le 1er debut avant le second debut DEBUT apres le second DEBUT .. mais avant la 1ère FIN est ce vraiment la FIN ? cette fois c'est la FIN
La syntaxe est la suivante :
Par exemple, la commande :
/^DEBUT/i\ /* Commentaire à placer avant le debut\ du bloc*/
... sur le fichier précédent (notez que nous ne traitons que les lignes ou le mot DEBUT est en début de ligne, donnera le résultat :
Avant le debut /* Commentaire a placer avant le debut du bloc*/ DEBUT apres le 1er debut avant le second debut /* Commentaire a placer avant le debut du bloc*/ DEBUT apres le second DEBUT .. mais avant la 1ere FIN est ce vraiment la FIN ? cette fois c'est la FIN
La syntaxe est la suivante :
Par exemple, la commande :
/^DEBUT/i\ /* Commentaire à placer après la fin\ du bloc*/
appliquée au fichier précédent (notez que nous ne traitons que les lignes ou le mot FIN est situé au bout de la ligne) donnera le résultat :
Avant le debut DEBUT apres le 1er debut avant le second debut DEBUT apres le second DEBUT .. mais avant la 1ere FIN /* Commentaire a placer apres la fin du bloc*/ est ce vraiment la FIN ? cette fois c'est la FIN/* Commentaire a placer apres la fin du bloc*/
Cumulons dorénavant ces deux commandes dans un seul script :
/^DEBUT/i\ /* Commentaire a placer avant le debut\ du bloc*/ /FIN$/a\ /* Commentaire a placer apres la fin\ du bloc*/
Le résultat devient :
/* Commentaire a placer avant le debut du bloc*/ DEBUT apres le 1er debut avant le second debut /* Commentaire a placer avant le debut du bloc*/ DEBUT apres le second DEBUT .. mais avant la 1ere FIN /* Commentaire a placer apres la fin du bloc*/ est ce vraiment la FIN ? cette fois c'est la FIN/* Commentaire a placer apres la fin du bloc*/
Si une ligne contient les 2 patterns de recherche, cela ne pose pas de problème spécial. Par exemple :
texte DEBUT texte FIN texte
devient :
texte /* Commentaire a placer avant le debut du bloc*/ DEBUT texte FIN /* Commentaire a placer apres la fin du bloc*/ texte
La commande c de changement de texte ne doit surtout pas etre confondue avec la commande s de substitution. En effet, la commande s remplace une expression régulière par une autre, dans les lignes de sa portée, alors que c remplace son buffer de travail, c'est à dire en fait, sa portée, par son argument ! lequel est spécifié selon le même format que les commandes i ou a.
L'intérêt de cette commande réside dans le fait qu'une portée peut s'étendre sur plusieurs lignes, ce qui est dur à réaliser avec les substitutions. Du coup, la commande c est utilisée, dans la plupart des cas, avec des adresses doubles.
Considérons un exemple simple ou un fichier est constitué d'un entête quelconque se terminant par une ligne vide. Vous désirez remplacer tout cet entête par le texte blah pipo suivi d'une ligne vide, vous pourriez écrire la commande ainsi :
1,/^$/c\ blah pipo\
Exemple, l'application de la commande précédente au texte :
Ceci est un exemple debile d'utilisation de la commande de changement de texte L'entete est termine le texte commence !
Nous fournit le résultat :
blah pipo L'entete est termine le texte commence !
Notez également que la commande de changement de texte influe tellement sur le tampon de travail que les commandes suivantes ne sont pas appliquées.
La syntaxe générale de cette commande est la suivante :
Cette commande détruit purement et simplement les lignes décrites dans sa portée. Dans le cas d'un script, il y a retour immédiat à la première instruction.
Le gros problème consiste à choisir le bon délimiteur pour les expressions régulières. Or, lorsque vous écrivez un script, vous ne pouvez pas savoir si tel ou tel caractère ne risque pas de se retrouver dans les motifs de recherche ou de remplacement. Aussi, le meilleur moyen consiste à utiliser un caractère « très régulièrement absent », par exemple, le caractère de code ASCII 1, où «ctrl-A».
Ici ce pose un nouveau problème : comment saisir sans danger ce caractère ? Nous contournons la difficulté par l'astuce suivante :
CTRL_A=`echo | tr '\012' '\001'`
Si vous n'avez pas compris de votre propre chef, je vous propose les explications suivantes :
On comprend donc que la commande précédente place dans la variable CTRL_A le résultat du remplacement d'un saut de ligne par le caractère 1 ! On peut alors utiliser le résultat précédent avec sed, par exemple :
sed -e "s${CTRL_A}motif_recherche${CTRL_A}motif_remplacement${CTRL_A}options
Il est possible de grouper des commandes en utilisant des accolades. La forme générale est la suivante :
[portee du groupe]{ premiere commande seconde commande }
Le format de présentation est assez rigide, l'accolade ouvrante doit suivre immédiatement la portée (facultative) et l'accolade fermante doit être isolée sur une ligne.
Etudions les intérêts de regrouper ainsi plusieurs commandes :
Il y a une différence énorme entre :
portee1 commande1 portee1 commande2
et :
portee1{ commande1 commande2 }
Pouvez voir laquelle ?
Dans le premier cas, la portée est évaluée pour chaque commande. Si, par malheur, commande1 modifie suffisament certaines lignes pour qu'elles ne soient plus reconnues par portee1, alors la commande2 ne sera pas appliquée dessus et réciproquement.
En revanche, dans le second cas, la portée est évaluée avant l'entrée dans le bloc de commandes, garantissant que toutes les commandes seront appliquées sur les lignes reconnues par portee1 avant l'entrée dans les bloc.
Avec le groupement, il est possible de définir des imbrications de portées rafinant progressivement la plage des lignes d'application des commandes. La syntaxe générale est la suivante :
portee1{
portee1A commande1A
portee1B commande1B
...
portee2{
portee2A commande2A
...
}
}
Exemple : dans un fichier HTML, on peut insérer du texte préformaté en l'entourant des balises <PRE> et </PRE>. Supposons que l'on souhaite détruire les lignes blanches dans de tels blocs. Il est possible d'utiliser les groupes pour faire cela :
/<PRE>/,/<\/PRE>{ /^$/d }
et le tour est joué !
Nous allons petit à petit raffiner un exercice pour montrer comment utiliser une commande simple de sed à l'intérieur d'une script shell.
On demande de construire un script shell nommé echange dont la ligne de commande est la suivante :
echange motif_initial motif_remplacement fichier_a_modifier
et appliquant le remplacement de motif_initial par motif_remplacement dans fichier_a_modifier. A la fin de l'opération, l'opération devra être effective dans fichier_a_modifier
Un résultat possible :
#!/bin/ksh # test sur les parametres ! if (( $# != 3 )) then # bon, on a affaire a un gland ... echo "Probleme de parametres, usage : $0 motif_initial motif_remplacement fichier_a_modifier else # mise en place de quelques variables ! initial=$1 final=$2 fichier=$3 # toujours utiliser cette astuce CTRL_A=`echo | tr '\012' '\001'` # les instructions suivantes visent a creer un nom de fichier inexistant # en rajoutant autant de fois qu'il le faut le numero de pid du script ($$) # au nom de fichier initial resultat=${fichier}$$ while [[ -e ${resultat} ]] do resultat=${resultat}$$ done # application de la commande sed -e "s${CTRL_A}${initial}${CTRL_A}${final}${CTRL_A}g ${fichier} > ${resultat} mv -f ${resultat} ${fichier} # et le tour est joue ! fi
Tout ceci n'est guère difficile, il suffisait de penser à :
Modifiez le script précédent pour qu'il puisse attaquer plusieurs fichiers.
Comme nous avons pris la précaution d'associer des variables aux arguments, ceci ne devrait pas poser de problème particulier : il suffit de rajouter une boucle sur les noms de fichiers qui sont nécessairement les derniers paramètres. Dans le script qui suit, les lignes en italique sont celles qui ont subi des modifications ou ont été rajoutées.
#!/bin/ksh # test sur les parametres ! if (( $# < 3 )) then # bon, on a affaire a un gland ... echo "Probleme de parametres, usage : $0 motif_initial motif_remplacement fichier1_a_modifier [fichier2_a_modifier ...] else # mise en place de quelques variables ! initial=$1 shift final=$1 shift # toujours utiliser cette astuce CTRL_A=`echo | tr '\012' '\001'` for fichier in $* do # les instructions suivantes visent a creer un nom de fichier inexistant # en rajoutant autant de fois qu'il le faut le numero de pid du script ($$) # au nom de fichier initial resultat=${fichier}$$ while [[ -e ${resultat} ]] do resultat=${resultat}$$ done # application de la commande sed -e "s${CTRL_A}${initial}${CTRL_A}${final}${CTRL_A}g ${fichier} > ${resultat} mv -f ${resultat} ${fichier} # et le tour est joue ! done fi
Vous comprenez mieux maintenant l'intérêt de toujours travailler avec des noms de variables plutôt qu'avec les paramètres positionnels ? la modification a été évidente !
La principale différence entre les deux formats de fichier texte réside dans la marque de saut de ligne : ^J sous Unix et ^M^J sous DOS. Nous allons appliquer notre script précédent à la transformation d'un grand nombre de fichiers.
Tout d'abord, la ligne de commande sera : echange "^M" "" fichiers que vous pourrez utiliser, par exemple, dans le cadre du résultat de find regroupé avec xargs :
find racine predicats -print | xargs echange "^M" ""
NB : pour obtenir un ^M sous vi, il faut entrer la séquence CTRL-V CTRL-M, alors que sous emacs, il s'agit de CTRL-Q CTRL-M.
Nous n'avons vu ici qu'une infime partie des possibilité offertes par cet outil formidable qu'est sed. Si vous devez aller plus loin, vous pouvez consulter la liste des sites ouaib suivants, ou le formidable bouquin (dont je me suis partiellement inspiré) Sed et awk,programmation avancée de Dale Dougherry publié chez O'Reilly.
Vous pouvez également consulter la liste des questions fréquemment posées sur sed.