Introduction à l'écriture de scripts shell — Partie 6

Gazette Linux n°116 — Juillet 2005

Ben Okopnik

Article paru dans le n°115 de la Gazette Linux de juin 2005.

Traduction française par Deny .

Relecture de la traduction française par Joëlle Cornavin .

Article publié sous Open Publication License. La Linux Gazette n'est ni produite, ni sponsorisée, ni avalisée par notre hébergeur principal, SSC, Inc.


Table des matières
1. Digressions
2. Un assistant prisé
3. Pris au piège
4. « En cas de problèmes, brisez la glace »
5. Utilisation du source
6. Conclusion de la série
7. Références

1. Digressions

Ce devrait être le dernier article de la série « Introduction à l'écriture de scripts shell » — j'ai eu beaucoup de réactions d'un certain nombre de lecteurs (je vous remercie pour vos aimables commentaires !), puisque nous avons couvert la plupart des bases ; c'était le but originel de la série. Il se peut que je réagisse encore à un moment dans l'avenir, mais ceux d'entre-vous qui ont suivi depuis le début peuvent désormais se considérer comme des experts à plein temps. Au moins, vous devriez savoir comment écrire un script et le faire tourner — ce qui est une compétence appréciable.


2. Un assistant prisé

Il y quelque temps, j'ai dû faire face à quelques difficultés en écrivant un script ; il s'agissait d'un tableau contenant une série de lignes de commandes que je devais exécuter selon certaines conditions. Je pouvais lire le tableau assez facilement ou afficher n'importe laquelle des variables — mais ce dont j'avais besoin était de les exécuter ! Étant dans une impasse, je me souviens avoir renoncé par ignorance d'une telle fonctionnalité et j'ai donc réécrit le script entier (plutôt volumineux), ce qui ne fut pas une expérience agréable. La commande eval aurait été la solution.

Voici comment elle fonctionnet  créez une variable nommée $cmd, telle que :


Odin:~$ cmd='cat .bashrc|sort'

À présent, vous pouvez afficher l'écho de la commande :


Odin:~$ echo $cmd
cat .bashrc|sort'

Cependant, comment l'exécuter ? La simple exécution de la commande $cmd produit une erreur :



Odin:~$ $cmd
cat: .bashrc|sort: No such file or directory

C'est là où la commande eval nous est utile : eval $cmd évalue la chaîne contenue dans la variable comme si elle avait été saisie sur la ligne de commande. Ce n'est pas un problème très fréquent, mais c'est une fonctionnalité du shell dont vous devez tenir compte.


3. Pris au piège

Une des techniques habituelles dans l'écriture de scripts shell (et en programmation en général) consiste à écrire des données dans des fichiers temporaires — il y a beaucoup de raisons de procéder ainsi. Cependant, celle-ci en est une excellente, que se passe-t-il quand un utilisateur interrompt l'exécution d'un script à mi-chemin ? (Pour ceux d'entre vous qui ont des scripts de ce type et qui n'ont pas pensé à ce problème, je suis désolé de vous donner matière à cauchemarder. Au moins, je vais vous indiquer une solution.)

Vous l'avez deviné : c'est une situation confuse. Des fichiers dans /tmp, peut-être des données essentielles, risquent d'être perdues, des fichiers que l'on pensait être à jour qui ne le sont pas... Comment sortir d'un programme avec élégance, malgré l'acharnement d'un adepte du broyage de clavier qui a juste à lancer Quake immédiatement ?

La commande trap fournit une réponse à ce genre de problème :

 
#!/bin/bash

function nettoyage ()
{
	# Ignorons 'Ctrl-C'...
	trap '' 2
	
	echo "Réveille toi, Neo."
	sleep 2
	clear
	echo "Tu es dans les nuages."
	
	echo "Il est encore dedans."|mail admin -s "Mise à jour arrêtée par $USER"
	
	# Restaurer les données originelles
	tar xvzf /mnt/backup/accts_recvbl -C /usr/local/acct
	
	# Supprimons les fichiers tmp
	rm -rf /tmp/in_process/
	
	# Bien, nous avons veillé au bon déroulement du nettoyage. L'heure de la vengeance est venue !!!
	rm /usr/games/[xs]quake
	
	# Fournissons-lui un nouveau mot de passe convenable...
	chpasswd $USER:~X%y!Z@zF%HG72F8b@Moron!&(~64sfgrnntQwvff########^
	
	# Nous allons conserver toutes ces données... Que fait la commande --remove-files ?
	tar cvz --remove-files -f /mnt/timbuktu/bye-bye.tgz /home/$USER
	# Puis...
	umount /mnt/timbuktu
	
	trap 2 # Remettons Ctrl-C en mode normal
	exit        
	# Voilà, c'est terminé...
	
}

trap 'nettoyage' 2
...

N'exécutez pas ce script... bien que ce soit tentant. La caractéristique de trap permet de définir un comportement chaque fois que l'utilisateur appuie sur Ctrl+C (dans ce cas, dès lors que le script se termine), ce qui esy beaucoup plus utile qu'un simple fermeture abrupte du programme ; il vous reste une chance de remettre de l'ordre, de générer des messages d'avertissement, etc.

trap peut aussi intercepter d'autres signaux : la commande kill, malgré son nom, ne « tue » pas d'elle-même un processus — elle envoie un signal. Le processus décide alors ce qu'il doit faire de ce signal (c'est une description sommaire, mais généralement correcte). Si vous souhaitez voir la liste complète des signaux, saisissez simplement trap -l ou kill -l ou même killall -l (qui n'énumère pas les numéros des signaux, mais juste leurs noms). Les signaux les plus couramment utilisés sont :

  1. 1)  SIGHUP ;

  2. 2) : SIGINT ;

  3. 3) : SIGQUIT ;

  4. 9) : SIGKILL ;

  5. 15) : SIGTERM (ce dernier est le signal par défaut pour kill quand aucun nom ou numéro de signal n'est indiqué).

Il existe aussi des signaux « spéciaux » :

  1. 0) : EXIT, généré à chaque sortie du shell ;

  2. DEBUG (pas de numéro affecté). Il peut servir pour dépister les erreurs des scripts shell (il est généré à chaque exécution d'une simple commande). DEBUG est en fait plus qu'un élément « générateur d'informations seul »: vous pouvez avoir cette action précise sans saisir de commande trap, mais en ajoutant simplement -x à votre shebang (reportez-vous à la section « En cas de problèmes, brisez la glace » ci-dessous).

La commande trap est un outil puissant. Dans la Linux Gazette n°37©, James T. Dennis utilisait un court fragment de script pour créer un repertoire sécurisé sous /tmp dans un but particulier, protéger des fichiers temporaires du reste du monde. Ce programme est assez intéressant, je l'ai employé quelquefois depuis.


4. « En cas de problèmes, brisez la glace »

À propos de dépannage, Bash fournit plusieurs outils très utiles qui peuvent vous aider à trouver des erreurs dans votre script. Il s'agit de commutateurs — une partie de la syntaxe de la commande set — que l'on utilise dans la ligne shebang du script lui-même. Les voici :

J'ai constaté que -nv et -x (et peut être -xf) sont les options les plus utiles : la première indique l'emplacement exact d'une ligne « incorrecte » (vous pouvez voir à quel endroit le script va poser problème) ; l'autre, bien que « verbeuse », est pratique pour savoir où les choses ne se passent pas tout à fait comme il le faudrait (dans le cas où, bien que la syntaxe soit correcte, l'action n'est pas celle que vous voulez). Ce sont deux de bons outils de dépannage. À mesure que le temps passe et que vous vous familiarisez avec les particularités du rapport d'erreur, vous les emploierez probablement de moins en moins, mais ils sont inestimables pour un programmeur novice.


5. Utilisation du source

Voici une ligne familière à tous les programmeurs de C :


#include <stdio.h>

Le sourcing (ou importation) de fichiers externes est un concept très utile. Cela signifie qu'un programmeur de C peut écrire des routines (fonctions) qui sont utilisées à plusieurs reprises, les conserver dans une « bibliothèque » (un fichier externe) et s'en servir quand elles sont nécessaires. Ne vous avais-je pas dit que l'écriture de scripts shell est un langage de programmation puissant et bien établi ? — nous pouvons faire la même chose ! Le fichier n'a même à être exécutable ; la syntaxe que nous utilisons dans le sourcing en tient compte. L'exemple ci-dessous est un fragment du début de ma bibliothèque de fonctions, Funky. Elle consiste actuellement en un seul fichier de 2 ko, mais qui prend de plus en plus d'espace.

Il existe une astuce en Bash qu'il vaut la peine de connaître : si vous créez une variable nommée BASH_ENV dans votre fichier .bash_profile, ainsi :


export BASH_ENV="~/.bash_env"

Créez ensuite un fichier appelé .bash_env dans votre répertoire personnel (/home), qui sera relu chaque fois que vous démarrez un « shell non interactif sans ouverture de session », c'est à dire un script shell. C'est de là que provient mon importation « Funky ». De cette manière, tout changement dans celui-ci est immédiatement disponible à n'importe quel script shell. Il peut aussi être importé directement sur la ligne de commande.


calc () # Calculateur en ligne de commande de nombres entiers exclusivement
{
    printf "$(($*))\n"
}

getch () # Obtient discrètement un caractère saisi au clavier, renvoie $GETCH
{
    OLD=`stty -g`
    stty raw -echo
    dd if=/dev/tty bs=1 count=1 2>/dev/null
    stty $OLD
}

colsel () # Sélecteur de couleur - s'exécute avec les choix de couleur de $TERM
{
trap 'echo -en "\E[$40;1m"; clear' 0	# Réinitialiser à la sortie
n=49	# Valeur maximale de la couleur de premier plan
while [ "$n" -ne 0 ]
do
	m=39	# Valeur maximale de la couleur de fond
	while [ "$m" -ne 0 ]
	do
		echo -en "\E[$m;${n}m"
        	clear
		echo "Ceci est un test."
		echo -en "\E[$40;1m"
        	echo -n " $n $m "
        	read
		(( m-- ))
	done
	(( n-- ))
done
}

Pas trop différent d'un script, n'est-ce pas ? Aucun shebang n'est nécessaire, puisque ce fichier ne s'exécute pas par lui-même. Donc, comment pouvons-nous l'employer dans un script ? Voici comment procéder (nous admettrons que je n'importe pas Funky dans .bash_env) :


#!/bin/bash
. Funky
declare -i Total=0
leave ()
{
    echo "Donc, vous êtes allé faire des achats ?"
    [ $total -ne 0 ] && echo "Voici le $total."
    echo "Passez une bonne journée."
    exit
}

# Exécutez la fonction 'leave' pour quitter
trap 'leave' 0

clear

# Boucle infinie !
while :
do
    echo
    echo "Que voulez-vous ? J'ai des concombres, des tomates, des laitues, des oignons"
    echo "et des radis aujourd'hui."
    echo
    
    # Ici nous appelons une fonction importée...
    input=`getch`
    # ...et nous référençons une variable créée par cette fonction.
    case $input
    in
	C|c) Total=$Total+1; echo "Ce sont de bons concombres." ;;
	T|t) Total=$Total+2; echo "Ce sont des tomates mûres, n'est-ce pas ?" ;;
	L|l) Total=$Total+2; echo "J'ai choisi ces laitues moi-même." ;;
	O|o) Total=$Total+1; echo "Assez frais pour vous faire larmoyer !" ;;
	R|r) Total=$Total+2; echo "Radis vraiment croquants." ;;
	*) echo "Je n'en ai pas reçu aujourd'hui, peut-être demain." ;;
    esac
    sleep 2

 done

Notez le point avant « Funky » : c'est un alias de la commande « source ». Lorsqu'il est importé, Funky acquiert une propriété intéressante : exactement comme si nous avions demandé à bash d'exécuter un fichier, il sort et cherche le chemin indiqué dans $PATH. Puisque je maintiens Funky dans /usr/local/bin (une partie de mon $PATH), je n'ai pas besoin de lui donner un chemin explicite.

Si vous êtes sur le point d'écrire des scripts shell, je vous conseille vivement de démarrer votre propre « bibliothèque » de fonctions. (CONSEIL : prenez les fonctions dans l'exemple ci-dessus !) Plutôt que de les saisir à plusieurs reprises, un seul argument « source » vous facilitera considérablement la tâche.


6. Conclusion de la série

En tout, nous avons abordé de nombreux thèmes, expliqué quelques particularités, fait un travail fructueux en rassemblant des informations utiles sur l'écriture de scripts shell. Toutefois, nous avons fait mieux — souvenez-vous que cette série d'articles n'était qu'une introduction à l'écriture de scripts shell — mais ceux qui ont suivi depuis le début et ont persévéré devraient être capables de concevoir, écrire et réparer un script shell tout à fait correct. Le reste — la compréhension et l'écriture de scripts plus complexes, plus sophistiqués — viendra seulement avec la pratique, autrement dit en « faisant de nombreuses erreurs ».

Bon Linux !


7. Références

Ben est le rédacteur en chef de la Linux Gazette© et il est membre de l'« Answer Gang© ».

Né à Moscou (Russie) en 1962, Ben a commencé à s'intéresser à l'électricité à l'âge de six ans. Il l'a démontré rapidement en plantant une fourchette dans une prise électrique, déclenchant ainsi un début d'incendie et depuis lors, il est plongé dans le dédale de la technologie. Il travaille sur les ordinateurs depuis le tout début, lorsqu'il fallait les construire en soudant des composants sur des circuits imprimés et que les programmes devaient tenir dans 4 ko de mémoire. Il paierait chèrement tout psychologue susceptible de le guérir des affreux cauchemars qu'il en a gardés.

Ses expériences ultérieures comprennent la création de logiciels dans près de douze langages, la maintenance de réseaux et de bases de données à l'approche d'un ouragan, la rédaction d'articles pour des publications allant des magazines consacrés à la voile aux revues sur la technologie. Après une croisière en voilier de sept ans sur l'Atlantique et la mer des Caraïbes ponctuée de quelques escales sur la côte est des États-Unis, il a pour l'instant jeté l'ancre à St Augustine (Floride). Il est instructeur technique chez Sun Microsystems© et travaille également à titre privé comme consultant open source/développeur web. Ses passe-temps actuels sont notamment l'aviation, le yoga, les arts martiaux, la moto, l'écriture et l'histoire romaine. Son Palm Pilot© est truffé d'alarmes dont la plupart contiennent des points d'exclamation.

Ben travaille avec Linux depuis 1997 et le crédite de sa perte complète d'intérêt pour les campagnes de guerre nucléaire sur les territoires nord-ouest du Pacifique.