Archives par mot-clé : sed

Remplacer des caractères spéciaux avec sed

Sid - © Blue Sky Studios
Sid – © Blue Sky Studios

Mais non pas Sid ! « sed » le petit programme sympa qui permet de faire des traitements sur du texte !

Un problème courant est d’avoir des caractères indésirables dans un fichier texte, qu’il s’agisse de données, d’un code source ou d’une page Web. Il arrive même qu’on ait affaire à des caractères ne s’affichant pas correctement, ou bien qui empêchent carrément le fonctionnement d’un logiciel.

Connaître le code hexadécimal des caractères avec « od »

Aujourd’hui tout le monde devrait utiliser l’encodage UTF-8. Dans les faits, c’est très loin d’être le cas : il faut souvent faire avec l’existant (Windows-1258, ISO-8859-*…), et l’UTF-8 ne couvrant pas tous les caractères possibles et imaginables, on peut aussi tomber sur des trucs exotiques issus de l’UTF-16 ou l’UTF-32.

Pour convertir un caractère spécial, une solution générique consiste à d’abord connaître son code hexadécimal (on devrait peut-être même dire « ses codes hexadécimaux » étant donné que ces caractères spéciaux sont encodés sur plusieurs octets). C’est un peu roots, mais ça aura le mérite de fonctionner dans n’importe quel shell de n’importe quel environnement.

Prenons par exemple le caractère €, et voyons à quoi il ressemble avec l’aide de la commande « od » :

printf '€' | od -vAn -tx1
 e2 82 ac

C’est donc cette suite de codes hexadécimaux que nous allons fournir à sed, moyennant une écriture légèrement différente puisque chaque code sera écrit de la forme \x??.

Cas des caractères qu’on n’arrive pas à taper au clavier ou à afficher

Certains caractères peuvent être particulièrement problématiques quand on souhaite connaître leurs codes hexadécimaux, tout simplement parce qu’on ne parvient pas à les taper au clavier, voire à les copier-coller. Dans ce cas, le mieux est d’isoler ces caractères dans un tout petit fichier texte et d’analyser les caractères avec od :

cat analyse.txt | od -vAn -tx1
 31 32 33 2e 34 35 20 e2 82 ac 20 54 54 43

Conversion des codes hexadécimaux avec sed

Maintenant que nous connaissons les caractères que nous souhaitons supprimer/modifier/remplacer (rayer les mentions inutiles), nous pouvons commencer par tester la commande sed avec les codes hexadécimaux :

echo '€' | sed 's/\xe2\x82\xac/\€/'
€

Dans l’exemple ci-dessus, les apostrophes (« simples quotes ») permettent de ne pas échapper les codes hexadécimaux ; mais cette autre écriture est tout à fait équivalente :

echo '€' | sed s/\\xe2\\x82\\xac/'\€'/
€

Il faut toutefois souligner que dans le cas présent, où l’on va remplacer le symbole € par son entité HTML, il est indispensable que l’entité soit d’une part entre apostrophes, d’autre part que le caractère & soit échappé avec un antislash. Ceci vient du fait que sed interprète par défaut & comme une recopie sur la sortie du motif trouvé… Et à ceci s’ajoute le shell qui interprète lui aussi ce caractère !

Remplacer plusieurs caractères spéciaux

Avec l’argument « -r » passé à sed, il est également possible de remplacer plusieurs motifs par quelque chose, en séparant ces motifs par « | » (la joie des expressions régulières ! :mrgreen:). Par exemple, si l’on souhaite changer tous les E accentués d’un texte (ÉÈÊË) en E, il suffit de faire :

sed -r s/\\xc3\\x89\|\\xc3\\x88\|\\xc3\\x8b\|\\xc3\\x8a/E/g origine.txt > resultat.txt

Voilà tout !

Redirection vers la sortie d’erreur standard

La plupart des commandes *nix utilisent la sortie d’erreur standard pour l’affichage d’informations, qu’elles soient liées à un mauvais paramètre ou à une anomalie de traitement, entre autre.

Lorsqu’on code en Bash, on écrit très souvent sur la sortie standard dite « normale », ou bien on re-dirige le flux de sortie vers un fichier de log. Lorsqu’on souhaite gérer les erreurs arrivant sur la sortie d’erreur standard, on peut choisir de les ignorer en les re-dirigeant vers /dev/null. On peut aussi choisir de rediriger ces erreurs vers le fichier de log principal, en acceptant que ces informations soient mélangées aux données du traitement. On peut également choisir  de les rediriger vers un second fichier, spécialement dédié aux erreurs.

Gérer les sorties d’erreur de cette façon est tout à fait satisfaisant pour un script monolithique. Ce ne sera pas le cas si on utilise des fichiers sources partageant des fonctions communes toutes faites, dans lesquelles on peut aussi rencontrer des erreurs. C’est dans ces fonctions-là qu’il peut devenir utile de gérer plus proprement le flux de sortie en erreur.

Imaginons par exemple une fonction « factorielle » qui pourrait être utilisée un peu partout dans nos scripts. Celle-ci devra prendre obligatoirement un argument, qui plus est un nombre entier positif. Si notre argument est manquant, ou bien s’il ne correspond pas à un nombre, alors un message d’erreur sera affiché sur la sortie d’erreur standard, en plus de retourner un code d’erreur adéquat.

ARGUMENT_MANQUANT=1
PAS_UN_NOMBRE=2

function factorielle ()
{
    if test $# -eq 0
    then
        echo "Cette fonction prend un nombre en parametre" >&2
        return $ARGUMENT_MANQUANT
    fi

    if test `echo "$1" | sed s/^[0-9]*$/1/g` != "1"
    then
        echo "Le parametre [${1}] n'est pas un nombre valide" >&2
        return $PAS_UN_NOMBRE
    fi

    seq -s \* $1 | bc

    return 0
}

Derrière chacune des deux commandes echo, c’est le >&2 (sans espace !) qui permet de rediriger le flux vers la sortie d’erreur au lieu de la sortie normale. Pour réaliser le calcul de la factorielle, une utilisation maline de seq et bc suffit. Si l’on teste notre fonction dans un terminal, on pourra alors obtenir ces différents résultats :

~$ factorielle
Cette fonction prend un nombre en parametre
~$ echo $?
1

~$ factorielle abc
Le parametre [abc] n'est pas un nombre valide
~$ echo $?
2

~$ factorielle abc 2>> /dev/null
~$ echo $?
2

~$ factorielle 5 2>> /dev/null
120
~$ echo $?
0

Le dot m’habite

Il est des fois où on erre parmi les répertoires depuis son terminal : cd par-ci, cd par-là… Avancer dans 3 sous-dossiers, remonter de 7… Parfois on ne sait plus combien de « .. » il va falloir enchaîner… Pourtant, le nom du dossier (lointain parent), lui, il est juste là, et ce fichu principe de remonter les niveaux un à un… En plus avec la quasi assurance de devoir saisir, dans une seconde commande, un ultime « cd .. » … Bref, des fois, c’est quand-même chiant ! Une petite fonction en Bash, et les « cd ../../.. […] /../../.. » vont vite décéder.

Dans la fonction ci-dessous, les commandes classiques que sont test, pwd et sed, vont nous permettre de remonter au répertoire qui aura été saisi en argument (pourvu que celui-ci existe).

function dcd ()
{
    if test `pwd | sed -e "s/\(.*\<${1}\>.*\)/\!/g"` = '!'
    then
        niveaux=`pwd | sed -e "s;.*\<${1}\>/*\(.*\);\1;" -e "s/\<[^/]*\>/../g"`

        test ${#niveaux} -gt 1 && cd $niveaux
    fi
}

Avant que vous ne soyez pris(e) de panique au vu de ce bout de code (les expressions régulières ont parfois un côté obscur), quelques explications s’imposent peut-être…

Le test qui conditionne le if, permet de vérifier que le paramètre $1 constitue bien le nom d’un des répertoires supérieurs :

  1. on exécute d’abord pwd pour connaître le chemin complet
  2. sed permet ensuite de transformer tout ce chemin en un seul caractère « ! », à l’unique condition que le paramètre $1 soit bien trouvé (en tant que mot au sens des expressions régulières)
  3. si la commande retourne « ! », alors le contenu du if est exécuté

Une fois dans le if, avec l’assurance que le répertoire cible existe, le nombre de répertoires à remonter va être stocké dans une variable « niveaux ». En voici le principe :

  1. on exécute encore une fois pwd, toujours pour connaître le chemin complet
  2. une première expression est soumise à la commande sed (premier « -e » ), afin de retirer de la chaîne tous les caractères devant le répertoire cible (donc en amont), et le répertoire cible avec
  3. une seconde expression est ensuite soumise à sed (deuxième « -e » ), pour remplacer chacun des répertoires encore présents par les fameux deux points « .. »
  4. en testant la longueur de la chaîne de « ../../.. » produite, on s’assure enfin de devoir remonter un certain nombre de niveaux, et c’est à cette seule condition qu’est exécuté le cd $niveaux

Après enregistrement de ce script dans un fichier, et un petit « source … » sur ce même fichier, la commande dcd fraîchement créée peut enfin être testée dans le terminal.

# Là, ça marche :
mynameis@localhost:/etc/locale/fr/LC_MESSAGES$ dcd locale
mynameis@localhost:/etc/locale$
# Là, ça marche pas, et heureusement :
mynameis@localhost:/etc/locale/fr/LC_MESSAGES$ dcd et
mynameis@localhost:/etc/locale/fr/LC_MESSAGES$

Bon… Tout ça c’est bien, mais c’est un peu lourd-dingue alors qu’il n’y a pas de quoi casser trois pattes à un canard… Voici une deuxième solution qui a le mérite d’être plus robuste, tout en étant nettement plus lisible.

function dcd ()
{
    increment=0
 
    for directory in `pwd | sed -e "s/ /5PAC3/g" -e "s/\// /g"`
    do
        directory=`echo $directory | sed "s/5PAC3/ /g"`
        test "$increment" -eq 1 && cd ..
        test "$directory" = "$1" && increment=1
    done
}

Le principe de cette nouvelle version est très simple :

  1. à l’aide de pwd, on dresse la liste des dossiers en amont, que l’on sépare par des espaces avec sed
  2. la chaîne «5PAC3», comme pour un précédent article, nous permet de sauvegarder les éventuels espaces dans le nom des répertoires
  3. pour chaque répertoire listé, on exécute alors le contenu de la boucle for :
    • on recouvre d’abord les espaces précédemment convertis en chaîne «5PAC3»
    • dès que l’on arrive au dossier cible, on le signale en mettant à jour la variable $increment
    • si le dossier cible a été dépassé dans la liste, alors on sait qu’il faut remonter d’un niveau via un « cd .. » , à chaque nouveau passage

Les solutions ne manquent donc pas, et il y en a, à n’en pas douter, sûrement d’autres ; et même meilleures que ces deux-ci.

Creative Commons License
Publié sous licence Creative Commons.

Afficher un texte multi-colore dans le terminal

Dans le précédent article, nous avons vu comment mettre en couleur du texte dans le terminal avec la commande printf.

Il serait parfois intéressant de pouvoir distinguer, dans le terminal, des informations plus ou moins « tabulées ». L’objectif de cet article est de présenter une solution de mise en couleurs d’un texte, c’est-à-dire un ensemble de mots séparés par des espaces. Nous allons rapidement constater une limitation liée à ces fameux espaces, ce que nous tenterons de résoudre dans un deuxième temps.

Concentrons-nous d’abord sur la manière de changer de couleur chaque mot d’un texte. Nous avons tout de même une piste : la fonction printcolor réalisée précédemment, à laquelle nous pouvons passer comme paramètre une valeur de couleur comprise entre 0 et 7. Si nous partons du principe que le fond de notre terminal est noir, nous n’utiliserons pas la valeur 0 et commencerons à 1 par conséquent.

Reste à trouver une façon simple de changer de valeur pour chaque nouveau mot de notre texte. Nous pouvons par exemple nous servir d’une boucle for à laquelle nous allons passer le texte en paramètre. Ainsi, pour chaque mot du texte, nous pouvons exécuter notre fonction printcolor. Et dans le même temps, il suffit d’incrémenter une variable initialisée à 1. Si nous détectons que notre variable $couleur vaut 8, alors nous lui réattribuons la valeur 1. La fonction echorainbow ci-dessous répond à cette première analyse.

function echorainbow ()
{
    couleur=1

    for word in $chaine
    do
        printcolor "$couleur" "$word"

        couleur=$((couleur+1))
        test $couleur -eq 8 && couleur=1
    done
}

Après chargement en session de notre script (via la commande source), ce seul exemple devrait déjà faire de votre terminal une vraie discothèque pour geek :

[benoit:~]$ echorainbow "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Sed non risus. Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. Cras elementum ultrices diam."

Mais comme un texte n’est pas non plus ce qui nous intéresse dans un terminal la plupart du temps, nous allons plutôt voir un exemple un peu plus complexe avec la commande ls. Pour un affichage ligne par ligne, il est dans ce cas nécessaire d’utiliser un petit while derrière un pipe… Si vous ne me croyez pas, essayez d’exécuter « echorainbow `ls` » ! Voici le résultat — nettement perfectible ! — de cette commande :

Première version de la fonction echorainbow
Première version de la fonction echorainbow

Nous nous apercevons que les espaces n’ont pas été conservés, ce qui est logique, puisque les espaces sont utilisés comme séparateurs lors de l’utilisation du for. Ajouter un espace derrière chaque mot n’est pas forcément une solution valable, puisque pour des données tabulées comme c’est le cas avec la commande ls, nous serions quand-même susceptibles de perdre l’alignement vertical permis par l’affichage de plusieurs espaces consécutifs !

Nous nous rendons compte qu’il serait beaucoup mieux de pouvoir garder les espaces. Pour ce faire, il devient donc nécessaire de remplacer chaque espace, non pas par un seul caractère, mais par une chaine de caractères, qui ne risque pas d’être rencontrée parmi tous les mots. A l’aide d’un pipe et de la commande sed, nous allons donc changer tout caractère espace, par exemple en "5PAC3". Ne voyez pas là un clin d’oeil au rappeur 2Pac, mon inspiration est surtout venue du 1337 5p34k… Voici la commande sed correspondante :

sed s/’ ‘/’5PAC3’/g

Cela ne suffit pas. Puisque les espaces nous servent de séparateurs de mots, si nous les supprimons, cela revient à ne plus avoir qu’un seul et unique « mot », mais très long. Comme quoi, parfois, plus c’est long, moins c’est bon… Euh… bref… Il nous faut donc séparer nos mots comme précédemment, par exemple en ajoutant un espace à la suite de chaque série d’espaces à conserver (et donc identifiés par notre super chaine « 5PAC »). Là encore, la commande sed va nous être utile. Mais cette fois-ci, c’est en sortie de la précédente commande sed que nous allons mettre un pipe, afin appliquer cette nouvelle transformation. Cette deuxième commande sed s’écrit ainsi (faites bien attention à l’espace derrière le « 1 » !) :

sed s/"((5PAC3)+)"/"1 "/g

Une fois les deux commandes sed mises bout à bout, voilà ce que ça donne :

sed s/’ ‘/’5PAC3’/g | sed s/"((5PAC3)+)"/"1 "/g

Il ne reste plus qu’à appliquer nos deux sed sur la chaine en entrée, puis, pour chaque mot, rechanger les "5PAC3" en espaces (ce qui peut être fait en sortie de la fonction printcolor, dans la boucle for). Revoyons notre fonction echorainbow avec ces nouveaux éléments pris en compte :

function echorainbow ()
{
    esp="5PAC3"
    chaine=`echo "$*" | sed s/' '/$esp/g | sed s/"\(\($esp\)\+\)"/"\1 "/g`

    j=1

    for word in $chaine
    do
        printcolor "$j" "$word" | sed s/$esp/' '/g

        j=$((j+1))
        test $j -eq 8 && j=1
        done
}

Cette fois-ci, l’exécution de l’exemple avec le ls et le while est bien plus convaincante !

Nouvelle version de la fonction echorainbow
Nouvelle version de la fonction echorainbow

Bien-sûr, l’exemple avec ls n’est pas forcément le plus intéressant… Par contre, l’utilisation de cette fonction peut trouver son sens dans le cas de données « chainées », c’est-à-dire de natures diverses et séparées par un séparateur comme « # » ou « ; ». Prenons l’exemple ci-dessous :

1;2;3;hello world     ;   TOTO;0;TITI;TATA;123;321;Lo;rem;  ip;su;m

La lecture de 10, 20 ou 30 lignes comme celle-ci peut devenir rapidement fastidieuse, surtout lorsqu’on recherche une information précise en plein milieu. Dans ce cas, notre fonction echorainbow va nous permettre de bien distinguer chaque partie… Mais à une condition : il va falloir remplacer les « ; » par des espaces… Mais à une condition (hé oui !) : il va falloir remplacer les bons espaces par un autre caractère… Puis re-remplacer tout ça par les caractères de départ, afin de profiter du même affichage, mais en couleur !

Pour ce faire, nous utiliserons (pour une seule ligne, sinon, ne pas oublier de boucler dans un while) quatre commandes sed, les deux premières avant exécution de la fonction echorainbow, les deux autres après. Ce qui donnera dans notre exemple :

[benoit:~]$ chaine=`echo ‘1;2;3;hello world     ;   TOTO;0;TITI;TATA;123;321;Lo;rem;  ip;su;m’ | sed s/’ ‘/#/g | sed s/’;’/’ ‘/g`
[benoit:~]$ echorainbow "$chaine" | sed s/’ ‘/’;’/g | sed s/#/’ ‘/g

Surtout ne dites rien ! Admirez juste votre terminal… :mrgreen:

Creative Commons License
Publié sous licence Creative Commons.

Répéter un caractère sur toute la largeur du terminal

Un terminal comme Konsole ou Gnome Terminal est une fenêtre redimensionnable. Si l’on souhaite afficher une ligne de séparation sur toute la largeur de celui-ci, il est nécessaire de connaître le nombre de colonnes (ou de caractères) qu’il affiche, forcément variable. Une manière de connaître les dimensions du terminal consiste à lancer la commande stty : celle-ci affiche alors un nombre important d’informations, dont le nombre de colonnes du terminal en cours. Une autre manière de récupérer le nombre de colonnes, consiste plus simplement à lancer la commande tput avec pour argument « cols », dont voici un exemple :

[benoit:~]$ tput cols
237

En encapsulant, en Bash, la commande tput dans $(…), lors de l’exécution cette pseudo-variable sera automatiquement remplacée par sa valeur, 237 dans notre exemple. Voyons maintenant comment nous en servir avec printf pour écrire 237 caractères.

Sans entrer dans les détails de la commande printf, le fait d’écrire printf "%237s" " " permet d’afficher 237 espaces. Puisque notre terminal ne fera pas toujours 237 caractères de large, remplaçons cette valeur par $(tput cols) comme expliqué au-dessus.

Puisque nous souhaitons afficher un élément de séparation dans notre terminal, la série d’espaces devra être remplacée par une série d’autre chose. L’utilisation du « _ » (underscore, souligné, ou «tiret du 8») permettra de simuler une ligne tirée de bout en bout, mais le caractère « * » (étoile ou astérisque), moyennant un « petit ajustement » (ou plutôt un échappement), aurait tout aussi bien fait l’affaire. La commande sed va nous servir à remplacer chacun des espaces par un underscore, via un pipe (le fameux « | ») en sortie du printf. La fonction « printline » ci-dessous prend en compte l’ensemble des points abordés, à savoir l’utilisation de tput, printf et sed.

function printline ()
{
    printf "%$(tput cols)s\n" "" | sed s/' '/"_"/g
}

Cet exemple est déjà un bon début. Maintenant, nous souhaiterions pouvoir afficher une ligne avec n’importe quel caractère, sans avoir à modifier à chaque fois notre script. Pour ce faire, nous allons donc prévoir une variable, laquelle reposera sur le premier argument passé au script. La variable $c à la place du underscore permet de remplacer chaque espace par le caractère (ou les caractères, mais dans ce cas il y aura autant de lignes que de caractères !) passé en argument. Une dernière petite astuce va nous permettre d’utiliser un underscore par défaut. L’écriture ${c:=_} s’explique ainsi : si ma variable $c ne retourne aucun caractère, alors utiliser « _ ». L’exemple ci-dessous reprend la fonction précédemment écrite avec ces quelques améliorations.

function printline ()
{
    c=$1
    printf "%$(tput cols)s" "" | sed s/' '/"${c:=_}"/g | cut -c1-$(tput cols)
}

Cette fonction permet d’obtenir ces différents résultats :

[benoit:~]$ printline
________________________________________________________________________

[benoit:~]$ printline *
************************************************************************

[benoit:~]$ printline Abc
AbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAbcAb