Page suivante   Page précédente   Table des matières  

2. Les secrets sombres et cachés de Bash

Par Ben Okopnik ben-fuzzybear@yahoo.com

« Il y a deux produits majeurs qui sont sortis de Berkeley : le LSD et UNIX. Nous ne pensons pas que ce soit une coïncidence. »
-- Jeremy Anderson

Dans les profondeurs de la page de manuel de bash se terrent de terribles choses qui ne doivent être approchées ni par le timide, ni par l'inexpérimenté... Attention, Pèlerin : le dernier explorateur imprudent a avoir plongé dans ces régions mystérieuses a été retrouvés, des semaines plus tard, en train de psalmodier d'étranges incantations qui sonnaient comme "nullglob", "dotglob", et "MAILPATH='/usr/spool/mail/bfox?"You have mail": /shell-mail?"$_ has mail!"'" (Il a, par la suite, été immédiatement engagé par une Compagnie Sans Nom dans la Silicon Valley pour un salaire non divulgué (mais énorme)... mais c'est en dehors du sujet)

Et alors, quel intérêt ? Je suis déjà allé faire du paravoile et de la plongée sous-marine ce mois-ci (et je partirai bientôt pour une traversée de 500 milles sur le Gulf Stream) ; Vivons La Vida Loca ! :-)

2.1 Expansions des paramètres

Les capacités de l'analyseur syntaxique de bash sont plutôt minimes comparées à, par exemple, celles de perl ou awk. Pour ma part, et dans le meilleur des cas, cet analyseur syntaxique n'est pas fait pour des tâches sérieuses. Il sait en faire juste assez pour s'occuper des tâches mineures. Quoi qu'il en soit, il est tout de même très utile pour cela.

Disons, par exemple, que nous avons besoin de différencier les noms de fichiers en majuscules de ceux en minuscules en analysant un répertoire -- il m'est arrivé de faire cela pour mes fonds d'écran sous X ; certains rendent mieux sous forme de mosaïque et d'autres étirés sur tout l'écran (dans ce cas, la taille du fichier n'était pas un bon guide). J'ai mis en majuscule tous les noms des images devant être mis en plein-écran, et j'ai mis en minuscules toutes les mosaïques. Puis, dans bkgr, le programme qui change mes fonds d'écran aléatoirement, j'ai mis le code suivant :


fn=$(basename $fnm)                   # Nous n'avons besoin _que_ du
                                      # nom du fichier.
[ -z ${fn##[A-Z]*} ] && MAX="-max"    # Ajoute l'option "-max"
                                      # si c'est vrai.
xv -root -quit $MAX $fnm &            # Éxécuter "xv" avec|sans "-max"
                                      #     selon le résultat du test.

Plutôt déconcertant, n'est-ce pas ? Bien, il y a déjà une partie que nous connaissons : le [ -z ... ] est un test pour détecter une chaîne de caractères vide. Alors, que signifie l'autre partie ?

Afin de protéger l'expansion du paramètre du monde froid et cruel (e.g., si vous vouliez utiliser le résultat dans un nom de fichier, vous auriez eu besoin de cette protection pour le conserver à l'écart des autres caractères), on utilise les accolades pour entourer tout ce morceau. $d est équivalent à ${d} sauf que la deuxième forme peut être combinée à autre chose sans perdre son identité, comme dans :


d=Ros
echo ${d}i        # "Rosi"
echo ${d}e        # "Rose"
echo ${d}a        # "Rosa"
echo ${d}alis     # "Rosalis"

Maintenant que nous l'avons isolé du reste du monde, sans ami et seul... oups, désolé -- c'est un « script de shell », pas un « script de film d'horreur » -- je perds le fil une fois de temps en temps... En tout cas, maintenant que nous avons séparé la variable à l'aide des accolades, nous pouvons appliquer quelques outils intégrés à bash (un petit malin, n'est-ce pas ?) pour faire quelques manipulation sur sa valeur. En voici la liste (pour cet exercice, nous faisons l'hypothèse que $parametre="amanuensis") :

${#parametre} : retourne la longueur de la valeur du paramètre.
Exemple : ${#parametre} = 10

${parametre#motif} : supprime la plus petite occurence à partir du début du paramètre.
Exemple : ${parametre#*n} = uensis

${parametre##motif} : supprime la plus grande occurence à partir du début du paramètre.
Exemple : ${parametre#*n} = sis

${parametre%motif} : supprime la plus petite occurence à partir de la fin du paramètre.
Exemple : ${parametre%n*} = amanue

${parametre%%motif} : supprime la plus grande occurence à partir de la fin du paramètre.
Exemple : ${parametre%%n*} = ama

${parametre:decalage} : retourne la valeur du paramètre débutant à decalage.
Exemple : ${parametre:7} = sis

${parametre:decalage:longueur} : retourne lougueur caractères commençant à decalage.
Exemple : ${parametre:1:3} = man

${parametre/motif/substitut} : remplace la première occurence de motif par substitut.
Exemple : ${parametre/amanuen/paralip} = paralipsis

${parametre//motif/substitut} : remplace toutes les occurences de motif par substitut.
Exemple : ${parametre//a/A} = AmAnuensis

(Pour les deux dernières opérations, si le motif commence avec #, il correspondra au début de la chaîne ; s'il commence avec %, il correspondra à la fin de la chaîne. Si substitut est vide, les occurences seront effacées.)

En réalité, il y en a un peu plus que ça -- des trucs comme des variables d'indirection et le parcours de tableaux -- mais pour ça vous n'aurez qu'à étudiez le manuel vous-même :-). Considérez simplement ceci comme de la motivation.

Bon, maintenant que nous avons découvert les outils, examinons à nouveau le code :


[ -z ${fn##[A-Z]*} ]

Ce n'est plus si difficile que ça, n'est-ce pas ? Ou peut-être que si ; mon processus de pensée, lorsque j'ai affaire à des recherches et correspondances de motifs, tend à être aussi tortueux qu'un bretzel. Ce que j'ai fait ici -- et cela pourrait être fait de nombreuses autres façons en utilisant les outils ci-dessus -- est de trouver la plus grande correspondance de la chaîne (i.e. le nom entier du fichier) qui commencent avec une majuscule. Le [ -z ... ] retourne vrai si le résultat est de longeur nulle (i.e. correspond au motif [A-Z]*), et $MAX est alors mis à « -max ».

Il est à noter que comme nous faisons correspondre la chaîne en entier, ${fn%%[A-Z]*} conviendrait tout autant. Si cela vous semble confus -- si tout ce qu'il y a ci-dessus vous semble confus -- je vous suggère d'expérimenter énormément afin de vous familiariser avec tout cela. C'est facile : donnez une valeur à un paramètre et expérimentez :


Odin:~$ experiment=supercallifragilisticexpialadocious
Odin:~$ echo ${experiment%l*}
supercallifragilisticexpia
Odin:~$ echo ${experiment%%l*}
superca
Odin:~$ echo ${experiment#*l}
lifragilisticexpialadocious
Odin:~$ echo ${experiment##*l}
adocious

... et ainsi de suite. C'est la meilleur façon de bien sentir ce que fait un outil ; prenez-le, codez-le, mettez vos verres de sûreté et appuyez doucement sur la gâchette. Observez toutes les consignes de sécurité puisqu'un effacement aléatoire de données de valeur peut toujours arriver. Les résultats effectifs peuvent varier et vous surprendront souvent.

2.2 États du paramètre

Parfois -- par exemple en voulant vérifier certaines conditions d'erreurs sur les valeurs assignées à des variables -- nous avons besoin de savoir si une variable spécifique a été assignée à une valeur ou non. On pourrait toujours vérifier sa longueur, comme j'ai fait précédemment, mais les utilitaires offerts par bash à cet effet nous fournissent des raccourcis utiles en de telles occasions :

(Ici, nous fairons l'hypothèse que notre variable $joe n'a pas été assignée, ou a une valeur nulle.)

${parametre:-autre} : Si la variable parametre n'est pas assignée, autre est substitué.
Exemple : ${joe:-mary} = mary ($joe reste non-assignée).

${parametre:=autre} : Si la variable parametre n'est pas assignée, assigne-lui autre et retourne la valeur de paramètre.
Exemple : ${joe:=mary} = mary ($joe="mary").

${parametre:?autre} : Affiche autre ou provoque une erreur si la variable parametre n'est pas assignée.
Exemple :


Odin:~$ echo ${joe:?"Not set"}
bash: joe: Not set
Odin:~$ echo ${joe:?}
bash: joe: parameter null or not set

${parametre:+autre} : autre remplace la variable parametre si cette dernière est assignée.
Exemple :


Odin:~$ joe=blahblah
Odin:~$ echo ${joe:+mary}
mary
Odin:~$ echo $joe
blahblah

2.3 Manipulation de tableaux

Une autre capacité interne de bash, un mécanisme de base pour manipuler des tableaux de données, nous permet d'analyser des données qui doivent être indexées, ou au moins gardées dans une structure qui permet un adressage individuel de chacun de ses membres. Considérons le scénario suivant : si j'ai un bottin d'adresse et que je veux envoyer ma dernière « Rubrique du Marin » à toutes les personnes de la catégorie « Amis », comment je fais ? De plus, disons que je veux créer une liste de noms des personnes à qui je l'ai envoyée... ou d'autres formes d'analyse... i.e. qu'il devienne nécessaire de la séparé en champs selon la longueur, et les tableaux deviennent rapidement une des seules options viables.

Regardons ce que ça peut impliquer. Voici un extrait du bottin utilisé pour ce travail :


        Nom              Catégorie Adresse                  e-mail

Jim & Fanny Delamico         Affaires       101101 Digital Dr. LA CA fr@gnarly.com
Fred & Wilma Rocks           amis           12 Cave St. Granite, CT  shale@hill.com
Joe 'Da Fingers' Lucci       Affaires       45 Caliber Av. B-klyn NY tuff@ny.org
Yoda Leahy-Hu                Amis           1 Peak Fribourg Switz.   warble@sing.ch
Cyndi, Wendi, & Myndi        Affaires       5-X Rated St. Holiday FL 3cuties@fl.net

Ouf. On voit bien que l'on doit lire ça par champs, compter les mots ne marcherait pas, ni d'aileurs une recherche de texte. Les tableaux viennent ici nous sauver !


#!/bin/bash

# 'nlmail' envoie les nouvelles mensuelles à une liste d'amis inclus
# dans le bottin.


# bash créerait les tableaux automatiquement, puisque nous utilisons
# la syntaxe 'nom[index]' pour charger les variables. Il se trouve
# toutefois que j'aime les déclarations explicites.
 
declare -- le nom d'une catégorie d'adresse électronique

# Compte le nombre de lignes dans le bottin et boucle ce nombre de
# fois. 

for x in $(seq $(grep -c $ bottin))
do
    x=$(($x))                           # Change '$x' en nombre
    ligne="$(sed -n ${x}p bottin)"      # Imprime la ligne numéro "$x"
    nom[$x]="${line:0:25}"              # Charge la variable 'nom'
    categorie[$x]="${line:25:10}"       #  etc.,
    adresse[$x]="${line:35:25}"         #       etc.,
    email[$x]="${line:60:20}"           #            etc.
done
# La suite plus loin....

À ce point, nous avons le fichier bottin chargé dans quatre tableaux que nous avons créés, prêt à être analysés. Chacun des champs est facilement adressable, permettant une solution triviale au problème d'envoyer le fichier à tous mes amis (cet extrait est la suite du script précédent) :


# La suite de ce qui précède...
for y in $(seq $x)
do
    # Nous ferons correspondre le mot "ami" dans le champ
    # 'categorie', le rendant insensible à la casse et en retirant les
    # caractères qui resterait.
            
    if [ -z $(echo ${categorie[$y]##[Aa]mi*}) ]
    then
        mutt -a Depeche.pdf -s 'S/V Ulysses News, 6/2000' ${email[$y]}
        echo "Mail envoyé à ${nom[$y]}" >> liste_envoie.txt
    fi
done

Ça devrait faire l'affaire, comme de copier le nom des destinataires dans un fichier appelé liste_envoie.txt -- une chouette idée pour vérifier que personne n'a été oublié.

Les possibilités de manipulation de tableaux de bash s'étendent un peu au-delà de ce simple exemple. Suffit-il de dire que pour des cas simples de cette sorte, avec des fichiers en-dessous de disons une centaine de ko, les tableaux bash sont à recommander ? Pour satisfaire à ma curiosité, j'ai créé une liste de noms qui est juste un peu plus grande que 100 ko en utilisant le bottin de l'exemple précédent :


  for n in $(seq 300); do cat bottin >> liste_nom; done

que j'ai ensuite faite tourner sur mon vieux Pentium 233/64MB. 24 secondes ; pas si mal pour 1500 enregistrements et un outil « bricolé ».

Notez que le script ci-dessous peut facilement être généralisé, par exemple, en ajoutant la possibilité de spécifier différents bottins, critères ou actions, directement sur la ligne de commande. Une fois les données divisées dans un format plus facilement adressable, les possibilités sont infinies.

2.4 Pour l'emballage...

bash, en plus d'être très puissant dans son rôle d'interprète de ligne de commande et de shell, possède aussi un grand nombre d'outils sophistiqués pour tout ceux qui ont besoin de créer leurs propres programmes. À mon avis, les scripts shell correspondent parfaitement à leur créneau -- celui d'un langage de programmation simple mais puissant -- juste entre la ligne de commande et la programmation complète (C, Tcl/Tk, Python) et devrait donc faire partie de l'arsenal de tous usagers de *nix. Linux, plus particulièrement, semble encourager l'attitude « faites le vous-même » parmi ses usagers, en leur donnant accès à des outils puissants et les moyens d'automatiser leur usage : quelque chose que je considère comme une meilleure intégration (et un plus grand « quotient d'utilité ») entre la puissance du système d'exploitation et l'environnement de l'usager. « Le pouvoir au peuple ! » :-)

Au mois prochain,

Linuxement !

2.5 Citation du mois

« ...Aussi terrible qu'une dépendance à UNIX puisse être, il y a des destins pire. Si UNIX est l'héroïne des systèmes d'exploitation, alors VMS est une dépendance aux barbituriques, le Mac est du MDMA, et MS-DOS est comme respirer de la colle. (Windows est de remplir vos sinus de lucite et de la laisser sécher.)

Vous devez à l'oracle un programme à douze étapes. »

-- L'Oracle de Usenet

2.6 Références

Les pages de manuel ("man") de bash, l'aide en ligne.

« Introduction aux scripts shell - La base » par Ben Okopnik, LG #52

« Introduction aux scripts shell » par Ben Okopnik, LG #53

« Introduction aux scripts shell » par Ben Okopnik, LG #54

Copyright 2000, Ben Okopnik. Paru dans le numéro 55 de la Linux Gazette de Juillet 2000.

Traduction française de Fabien Niñoles


Page suivante   Page précédente   Table des matières