Ecrire un script shell

De Wiki de la communauté Mandriva.

Les scripts sont des ensembles de commandes qui sont enregistrées dans un fichier. Le shell peut lire un tel fichier et mettre en œuvre ces commandes, comme si elles avaient été tapées sur le clavier. Le shell fournit cependant aussi des outils de programmation spécifiques pour les scripts, qui vous permettront d'écrire des programmes puissants.
Attention !
Si vous n'avez pas - ou très peu - de connaissances sur le shell, il pourrait être approprié pour vous de commencer par lire les pages du Wiki Le shell sans peine et Le shell sans peine : techniques avancées, avant d'attaquer ce qui suit.



© 2000-2007, William Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any medium, provided this copyright notice is preserved.

© 2000-2007, William Shotts, Jr. Les copies et distributions textuelles et intégrales de cet article sont autorisées sur tout support, à la condition que cette note de copyright soit conservée.


NdT signifie Nota du Traducteur et apporte des compléments de traduction ou d'information nécessaires à la compréhension. Ils ne sont pas dans le document original.

Sommaire


C'est ici que le plaisir commence

Avec des milliers de commandes disponibles pour l'utilisateur de la ligne de commandes, comment peut-il se les rappeler toutes ? La réponse est, qu'il ne peut pas ! La vraie puissance de l'ordinateur est sa capacité à faire le travail à votre place. Pour obtenir qu'il le fasse, nous utilisons la puissance du shell pour automatiser les choses. Nous écrivons des scripts.

Les scripts sont des ensembles de commandes qui sont enregistrées dans un fichier. Le shell peut lire un tel fichier et mettre en œuvre ces commandes, comme si elles avaient été tapées sur le clavier. En plus des choses que vous avez apprises jusqu'à maintenant, le shell fournit aussi diverses possibilités de programmation pour rendre vos scripts vraiment puissants.

A quoi servent donc les scripts ? Eh bien, grâce à eux, toute une gamme de tâches peuvent être automatisées. Voici, par exemple, quelques-unes des tâches que j'automatise à l'aide de scripts :

  • Un script rassemble tous les fichiers (plus de 2000) de ce site sur mon ordinateur et les transmet à mon serveur web.
  • Les pages SuperMan sont entièrement construites par un script.
  • Chaque vendredi soir, tous mes ordinateurs copient leurs fichiers vers un « serveur de sauvegarde » sur mon réseau. Ceci est réalisé par un script.
  • Un script récupère automatiquement les mises à jour disponibles chez mon fournisseur Linux et maintient un dépôt des mises à jour vitales. Il m'envoie un courrier électronique incluant un rapport des tâches qui doivent être réalisées.

Comme vous pouvez le voir, les scripts libèrent la puissance de votre machine Linux. Allons-y donc et amusons-nous bien !

Ecrire son premier script et le faire fonctionner

Pour réussir l'écriture d'un script de shell, vous devez faire trois choses :

  1. Ecrire un script.
  2. Lui attribuer les permissions nécessaires pour que le shell puisse l'exécuter.
  3. Le placer en un endroit où le shell pourra le trouver.

Ecrire un script

Un script shell est un fichier qui contient du texte ASCII. Pour créer un script shell, vous devez utiliser un « éditeur de texte ». Un éditeur de texte est un programme, analogue à un traitement de texte, qui lit et écrit des fichiers ASCII. Il en existe beaucoup, beaucoup d'éditeurs de texte sont disponibles pour votre système Linux, aussi bien dans un environnement en mode texte que dans un environnement graphique. Voici une liste des plus usités :

vi 
L'ancêtre des éditeurs de texte. La réputation de vi a parfois souffert de sa relative difficulté et de la structure non intuitive de ses commandes. D'un autre côté, vi est puissant, léger et rapide. Apprendre vi est un rite de passage vers Unix, car il est universellement disponible sur les systèmes Unix/Linux. Utilisable en mode graphique et texte.
emacs 
Le véritable géant du monde des éditeurs de texte est Emacs par Richard Stallman. Emacs contient (ou on peut lui faire contenir) toutes les possibilités jamais conçues pour un éditeur de texte. Il faut remarquer que les fans de vi et d'Emacs se combattent dans d'amères guerres de religion pour savoir lequel est le meilleur. Utilisable en mode graphique et texte.
pico 
pico est l'éditeur de texte fourni avec le programme de courrier électronique pine. pico est très simple à utiliser mais de possibilités limitées. Je recommande pico pour les utilisateurs débutants qui ont besoin d'un éditeur de texte.
gedit 
gedit est l'éditeur de texte fourni avec l'environnement de bureau Gnome. (Editeur graphique)
kedit 
kedit est l'éditeur de texte fourni avec l'environnement de bureau KDE. (Editeur graphique)
kwrite 
kwrite est l'« éditeur avancé » fourni par KDE. Il possède la coloration syntaxique, une caractéristique pratique pour les programmeurs et rédacteurs de scripts. (Editeur graphique)
nedit 
Mon favori, nedit possède la coloration syntaxique, des macros, un correcteur d'orthographe, des fenêtres multiples et beaucoup d'options de configuration. Si vous souhaitez en apprendre plus au sujet de nedit, vous pouvez visiter http://www.nedit.org

Note du Trad. : Citons aussi l'excellent petit éditeur minimaliste nano (un paquetage pour mandriva est disponible), utilisable dans les consoles en mode texte et graphiques, et l'excellent éditeur graphique de KDE : kate.

Maintenant, chauffez votre éditeur de texte et tapez votre premier script comme suit :


# !/bin/bash
# Mon premier script

echo "Bonjour le Monde"

Les plus malins d'entre vous ont bien sûr déjà compris comment copier et coller le texte dans leur éditeur de texte ;-)

S'il vous est arrivé d'ouvrir un livre sur la programmation, vous avez immédiatement reconnu cela comme étant le traditionnel "Hello World" qu'on inflige aux débutants... Enregistrez votre fichier avec un nom explicite. Pourquoi pas mon_script ?

La première ligne du script est importante. C'est un indice spécial donné au shell indiquant quel programme est utilisé pour interpréter le script. Dans ce cas, c'est /bin/bash. D'autres langages de script tels que perl, awk, tcl, Tk et python peuvent aussi utiliser ce mécanisme.

Le seconde ligne est un commentaire. Tout ce qui apparait après un symbole "#" est ignoré par Bash. Lorsque votre script devient long et compliqué, les commentaires deviennent vitaux. Ils sont utilisés par les programmeurs pour expliquer ce qui se passe afin que d'autres puissent le comprendre. La dernière ligne est la commande echo. Cette commande affiche simplement à l'écran la suite de caractères indiquée.

Etablir les permissions

La chose suivante que nous avons à faire est de donner au fichier les permissions nécessaires pour que le shell puisse exécuter le script. Ceci est fait avec la commande chmod comme suit :

Image:Konsole.png
[utilisateur@ordi ~]$ chmod 755 mon_script

Le code numérique 755 vous donne la permission de lire, écrire et exécuter le fichier. Tout autre utilisateur aura seulement la permission de le lire et de l'exécuter. Si vous voulez que votre script soit privé et que vous seul puissiez le lire et l'exécuter, utilisez 700 à la place.

Le mettre dans votre chemin

Au point où nous en sommes maintenant, votre script peut fonctionner. Essayez donc de lancer cette commande :

Image:Konsole.png
[utilisateur@ordi ~]$ ./mon_script

Vous devriez voir "Bonjour le Monde" s'afficher. Si ce n'est pas le cas, regardez dans quel répertoire vous avez réellement enregistré votre script. Entrez-y et essayez à nouveau.

Avant d'aller plus loin, je dois m'arrêter et parler un peu des chemins. Quand vous tapez un nom de commande, le système ne cherche pas dans tout l'ordinateur où est situé le programme. Cela prendrait beaucoup de temps. Vous avez remarqué que vous n'avez pas en général à spécifier le chemin complet vers le programme que vous désirez exécuter, le shell semble tout simplement le connaître déjà.

Eh bien, vous avez raison. Le shell le connait. Voici comment : le shell maintient une liste de répertoires où sont placés les fichiers exécutables (les programmes), et ne cherche que dans les répertoires de cette liste. S'il ne trouve pas le programme, après avoir cherché en vain dans chaque répertoire de cette liste, il affichera le fameux message d'erreur command not found (= la commande n'a pas été trouvée).

Cette liste de répertoires est appelée le path ou parfois, un peu bizarrement (car en fait il s'agit d'une liste de plusieurs chemins), le chemin. Vous pourrez voir la liste de ces répertoires en lançant la commande suivante :

Image:Konsole.png
[utilisateur@ordi ~]$ echo $PATH

Elle vous retournera une liste de répertoires, séparés les uns des autres par des deux-points, dans lesquels s'effectuera la recherche si un nom de chemin spécifique n'est pas donné quand la commande est entrée. Lors de notre premier essai pour exécuter notre nouveau script, nous avons spécifié un nom de chemin (./) vers le fichier (le point représente pour le shell le répertoire courant, le répertoire dans lequel vous vous trouvez).

Vous pouvez ajouter des répertoires à votre chemin avec la commande suivante, où répertoire est le nom du répertoire que vous désirez ajouter :

Image:Konsole.png
[utilisateur@ordi ~]$ export PATH=$PATH:répertoire

Une meilleure façon de faire aurait été d'éditer votre fichier de configuration du shell .bash_profile pour y inclure la commande ci-dessus. De cette façon, elle sera exécutée automatiquement à chaque connexion.

La plupart des distributions Linux modernes encouragent une pratique par laquelle chaque utilisateur possède un répertoire spécifique pour tous les programmes qu'il ou qu'elle utilise personnellement. Ce répertoire est appelé bin et est un sous répertoire de votre répertoire personnel (/home/<votre_nom>). Si vous n'en avez pas déjà un, créez-le avec la commande suivante :

Image:Konsole.png
[utilisateur@ordi ~]$ mkdir bin

NoteduTrad. Une autre possibilité parfois utilisée est de créer un répertoire bin dans /opt, à ajouter alors aussi à votre path.

Déplacez votre script dans votre nouveau répertoire bin et tout sera correctement paramétré. Maintenant, vous n'avez qu'à taper :

Image:Konsole.png
[utilisateur@ordi ~]$ mon_script

et il sera exécuté.

Editer les scripts que vous possédez déjà

Avant que vous ne vous lanciez dans l'écriture de nouveaux scripts, je voudrais attirer votre attention sur le fait que vous en possédez déjà ! Ces scripts ont été placés dans votre répertoire personnel quand votre compte a été créé, et ils sont utilisés pour configurer le comportement de votre ordinateur lors de vos sessions. Vous pouvez éditer ces scripts pour y opérer des modifications.

Dans cette leçon, nous allons regarder quelques-uns de ces scripts et apprendre quelques nouveaux concepts importants au sujet du shell.

Des commandes, des commandes partout !

Jusqu'à maintenant, nous n'avons pas vraiment discuté de ce que sont les commandes. Les commandes peuvent être de diverses sortes. Quelques-unes sont contenues dans le shell lui-même. Autrement dit, le shell comprend automatiquement, par lui-même, un certain nombre de commandes. Les commandes cd et pwd, par exemple, appartiennent à ce groupe. Les commandes implémentées dans le shell sont appelées « commandes internes du shell » (shell builtins). Pour voir une liste de commandes implémentées dans Bash, lancez la commande help.

Le second type de commandes sont les programmes exécutables. La plupart des commandes appartiennent à ce groupe. Les programmes exécutables sont tous les fichiers inclus dans les répertoires de votre path.

Les deux derniers types de commandes font partie de votre environnement d'exécution (NdT runtime environment). Pendant votre session, le système garde dans sa mémoire bon nombre de faits se rapportant au monde extérieur. Ces informations constituent ce qu'on appelle l'environnement. Il contient des choses telles que votre path, votre nom d'utilisateur, le nom des fichiers où votre courrier est délivré, et beaucoup d'autres choses. Vous pouvez voir une liste complète de ce qui est dans votre environnement avec la commande set

Les deux types de commandes contenues dans l'environnement sont les alias et les fonctions du shell.

Les alias

Maintenant, avant que tout ce que je viens de dire ne suscite trop de confusion dans votre esprit, créons un alias. Assurez-vous que vous êtes dans votre répertoire personnel. Utilisez votre éditeur de texte favori, ouvrez le fichier de configuration du shell .bash_profile et ajoutez cette ligne à la fin du fichier :

alias l='ls-l'

Le fichier .bash_profile est un script shell qui est exécuté à chaque connexion. En ajoutant des commandes alias au fichier, nous avons créé une nouvelle commande appelée l qui exécute ls -l. Pour essayer votre nouvelle commande, déconnectez-vous et reconnectez-vous.


Nota Bene :

Notez que sous la Mandriva, un alias prédéfini, fait de l, un raccourci pour la commande ls employée sans l'option -l, l'alias placé en fin de .bash_profile prendra cependant le pas sur l'alias Mandriva par défaut....
Nous verrons plus bas qu'on place en général plutôt les alias personnels dans le fichier ~/.bashrc (qui ne nécessite pas une reconnexion pour être activé : il vous suffira alors pour l'activer de relancer un shell, même sans nouvelle connexion). Les alias pour l'ensemble des utilisateurs du système sont quant à eux à placer dans le fichier/etc/profile.d/alias.sh. Pour plus de détails sur tout cela, voir Le shell sans peine#Les alias et Les fichiers de configuration du shell).


En utilisant cette technique, vous pouvez créer autant de commandes personnalisées que souhaitées pour vous-même. En voici une autre à essayer :

alias aujourdhui='date +"%A, %-d %B %Y"'

Cet alias crée une nouvelle commande appelée aujourdhui qui affiche la date du jour avec une agréable mise en forme.

Profitons de l'occasion pour noter que la commande alias est un nouvel exemple de commande interne du shell (shell builtin).

Vous pouvez créer vos alias directement à l'invite de la ligne de commande ; cependant, dans ce cas, si vous ne les écrivez pas dans un fichier de configuration, ils ne resteront opérationnels que le temps de votre session. Ce sera le cas, par exemple, si vous tapez simplement ceci en console :

[moi@linuxbox moi]$ alias l='ls-l'

Les fonctions du shell

Les alias sont appropriés pour des commandes simples, mais si vous désirez créer quelque chose de plus complexe, vous devriez essayer les fonctions du shell. Elles peuvent être conçues comme des scripts à l'intérieur d'un script, autrement dit comme de petits sous-scripts. Essayons-en une. Ouvrez à nouveau .bash_profile dans votre éditeur de texte et remplacez l'alias pour aujourdhui par ce qui suit :

function aujourdhui {
 echo "la date d'aujourd'hui est :"
 date +"%A, %-d %B, %Y"
}

function est aussi une commande interne du shell ( shell builtin), et comme avec un alias, vous pouvez entrer des fonctions du shell directement après l'invite de la ligne de commande.

[moi@linuxbox moi]$ function aujourdhui {
> echo "la date d'aujourd'hui est :"
> date +"%A, %-d %B %Y"
> }
[moi@linuxbox moi]$

Les types de commande

Etant donné qu'il y a beaucoup de types de commandes, il peut vite devenir difficile de distinguer un alias, d'une fonction du shell ou d'un fichier exécutable. Pour déterminer ce qu'est une commande, utilisez la commande type. type affichera de quel type de commande il s'agit. Il peut être utilisé comme suit :

[moi@linuxbox moi]$ type commande

(avant de créer un alias ou une fonction, vérifiez que la commande n'existe pas, en essayant avec type le nom que vous projetiez d'utiliser).

.bashrc

Bien que placer vos alias et fonctions du shell dans votre .bash_profile fonctionnera, ce n'est pas considéré comme correct. Il existe un fichier particulier nommé .bashrc qui est destiné à être utilisé pour ce genre de choses. Vous pouvez remarquer un morceau de code, vers le début de votre .bash_profile, qui ressemble à cela :

if [-f  ~/.bashrc]; then
 .  ~/.bashrc
fi

Ce morceau de script vérifie s'il existe un fichier .bashrc dans votre répertoire personnel. S'il en trouve un, alors le script lit et exécute son contenu. Si ce morceau de code est bien présent dans votre .bash_profile, vous devriez alors éditer le fichier .bashrc et y mettre vos alias et fonctions du shell.

Notez cependant que si vous voulez que des alias ou fonctions soient disponibles pour tous les utilisateurs vous devrez, sous Mandriva, les placer dans le fichier /etc/profile.d/alias.sh.

Documents en ligne (Here scripts)

Dans les leçons suivantes, nous construirons une application utile. Elle produira un document HTML qui contient des informations concernant votre système. J'ai passé beaucoup de temps à réfléchir sur la meilleure façon d'enseigner la programmation du shell, et l'approche que j'ai retenue est très différente de la plupart des approches que j'ai vues. Souvent, ces approches privilégient un traitement systématique de toutes les nombreuses caractéristiques du shell, et elles supposent une expérience dans d'autres langages de programmation. Bien que je ne présume pas que vous sachiez déjà programmer, je sais bien que beaucoup de gens aujourd'hui savent écrire le HTML, aussi notre premier programme sera de faire une page Web. En construisant notre script, nous découvrirons pas à pas les outils à notre disposition nécessaires pour résoudre les problèmes.

Ecrire un fichier HTML avec un script

Comme vous devez le savoir, un fichier HTML bien structuré contient les éléments suivants :

<HTML>

<HEAD>

<TITLE>
Le titre de votre page
</TITLE>

</HEAD>

<BODY>

Le contenu de votre page s'inscrit ici.

</BODY>

</HTML>

Maintenant, avec ce que nous connaissons déjà, on pourrait écrire un script pour produire le résultat ci-dessus :


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML

echo "<HTML>"
echo "<HEAD>"
echo "  <TITLE>"
echo "  Le titre de votre page"
echo "  </TITLE>"
echo "</HEAD>"
echo ""
echo "<BODY>"
echo "  Le contenu de votre page s'inscrit ici."
echo "</BODY>"
echo "</HTML>"

Le script peut être utilisé comme suit :

[moi@linuxbox moi]$ edition_page > page.html

La sortie du script est redirigée vers le fichier page.html qui sera créé s'il n'existe pas et dont le contenu sera remplacé par la sortie du script s'il existe déjà.

Sur l'opérateur de redirection >, voir Le shell sans peine#Pour remplacer le contenu d'un fichier existant par un autre contenu ou créer un fichier doté d'un contenu défini et Le shell sans peine#Redirection vers un fichier ou en provenance d'un fichier.

Il a été dit que les plus grands programmeurs sont les plus fainéants. Ils écrivent des programmes pour s'économiser du travail. De même, quand des programmeurs intelligents écrivent des programmes, ils essayent d'économiser de la frappe.

La première amélioration de ce script sera de remplacer l'utilisation répétée de la commande echo par un document en ligne (Here document), comme ceci :


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML

cat << _EOF_

<HTML>
<HEAD>
    <TITLE>
    Le titre de votre page
    </TITLE>
</HEAD>

<BODY>
     Le contenu de votre page s'inscrit ici.
</BODY>
</HTML>
_EOF_

(NdT: La dénomination anglaise here script sera conservée dans la suite du document, de même end of file sera conservé pour fin de fichier)

Un here script (aussi quelquefois appelé un here document) est une forme de redirection des Entrées/Sorties. Il fournit un moyen d'inclure du contenu qui sera donné à l'entrée standard d'une commande. Dans le cas du script ci-dessus, la commande cat reçoit à son entrée standard, un flux de données provenant de notre script (ces données correspondent à ce qui est écrit entre les deux _EOF_). Elle renvoie ensuite les données ainsi reçues vers la sortie standard, par défaut l'écran du terminal : l'opérateur de redirection > permet de rediriger cette sortie vers un fichier.

Un here script est construit comme ceci :

commande << marque

contenu devant être utilisé comme entrée standard de la commande

marque

La marque peut être n'importe quelle chaîne de caractères. J'utilise _EOF_ (EOF est une abréviation pour "End Of File", qui signifie fin de fichier) parce que c'est traditionnel, mais vous pouvez utiliser n'importe quoi, aussi longtemps que cette marque n'entre pas en conflit avec un mot réservé de Bash. La marque qui termine le here script doit être strictement identique à celle qui le commence, sinon, le reste de votre script sera interprété comme la continuation de l'entrée standard de la commande.

Il existe une astuce supplémentaire qui peut être utilisée avec un here script. Souvent, vous désirerez introduire une indentation de l'ensemble de la partie du script correspondante au here script avec des tabulations, pour améliorer la lisibilité du script. Vous pouvez faire cela si vous changez le script comme suit :


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML

cat <<- _EOF_

    <HTML>
    <HEAD>
       <TITLE>
       Le titre de votre page
       </TITLE>
    </HEAD>

    <BODY>
       Le contenu de votre page s'inscrit ici.
    </BODY>
    </HTML>
_EOF_

Remplacer le "<<" par "<<-" fait que Bash ignorera les tabulations en début de ligne dans le here script (tout en tenant compte des espaces de début de ligne). La sortie de la commande cat ne contiendra donc aucune des tabulations en début de ligne.

Bien ! Revenons maintenant à notre page. Nous allons éditer notre page pour lui faire dire quelque chose :


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML

cat <<- _EOF_

    <HTML>
    <HEAD>
        <TITLE>
        Informations sur mon système
        </TITLE>
    </HEAD>

    <BODY>
    < H1 >Informations sur mon système</H1>
    </BODY>
   </HTML>
_EOF_

(NdT: Relecteur, ne pas omettre les blancs autour de la balise <H1>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Substitutions - 1ère partie

Maintenant que nous avons un script qui fonctionne, améliorons-le. Avant toute chose, nous allons effectuer quelques changements, parce que nous voulons être un peu flemmard. Dans le script ci-dessus, nous voyons que la phrase "Informations sur mon système" est répétée. C'est de la frappe inutile (et du travail supplémentaire!) aussi nous allons l'améliorer comme ceci :


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML

titre="Informations sur mon système"

cat <<- _EOF_

    <HTML>
    <HEAD>
        <TITLE>
        $titre
        </TITLE>
    </HEAD>

    <BODY>
    < H1 >$titre< /H1 >
    </BODY>
    </HTML>
_EOF_

(NdT: Relecteur, ne pas omettre les blancs autour de la balise <H1>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Comme vous pouvez le voir, nous avons ajouté une ligne au commencement du script et remplacé les deux occurrences de la phrase "Informations sur mon système" par $titre.

Variables

Ce que nous avons fait est d'introduire une notion fondamentale qui apparaît dans presque tous les langages de programmation, la notion de « variable ». Les variables sont des zones de mémoire qui peuvent être utilisées pour enregistrer une information et qui sont référencées par un nom. Dans le cas de notre script, nous avons créé une variable nommée titre et placé la phrase Informations sur mon système en mémoire. A l'intérieur du here script qui contient notre code HTML , nous utilisons $titre pour dire au shell de remplacer la variable par son contenu.

Comme nous le verrons, le shell effectue plusieurs sortes de substitutions lors de l'exécution des commandes. Les caractères de remplacement (ou jokers) pour les noms de fichiers en sont un exemple. Quand le shell lit une ligne qui contient un caractère de remplacement, il développe la signification de celui-ci puis continue l'exécution de la ligne de commande. Pour voir cette action, essayez cela :

[moi@linuxbox moi]$ echo *

Le shell affichera alors la liste des noms de fichiers du répertoire courant.

Les variables sont traitées exactement de la même façon par le shell. Chaque fois que le shell voit un mot qui commence par un $, il essaye de trouver la valeur qui a été assignée à la variable dont le nom suit immédiatement le $, et effectue le remplacement de $mot par la valeur de la variable mot.

Comment créer une variable ?

Pour créer une variable, écrivez dans votre script une ligne qui contient le nom de la variable suivi immédiatement d'un signe égal (=). Après le signe égal, donnez l'information que vous désirez enregistrer dans la variable. Remarquez qu'aucun espace n'est autorisé de part et d'autre du signe égal.

D'où viennent le nom des variables ?

C'est à vous de les inventer, vous devez choisir un nom pour vos variables, en respectant quelques règles :

  1. Il doit commencer par une lettre ou par un caractère de soulignement (_).
  2. Il ne doit pas comporter d'espaces. Mettez à la place des caractères de soulignement (_). (NdT : Il ne doit pas non plus contenir de lettres accentuées.)
  3. N'utilisez pas de signe de ponctuation.
  4. N'utilisez pas un nom qui est déjà un mot utilisé par Bash : de tels mots sont appelés « mots réservés » et les mots réservés, comme leur nom le suggère, ne doivent pas être utilisés comme noms de variables. Si vous utilisez un de ces mots, Bash sera perturbé. Pour voir la liste des mots réservés, utilisez la commande help.

Comment satisfaire notre paresse

L'ajout de la variable titre dans notre script rend notre vie plus facile de deux façons. Premièrement, cela réduit la quantité de frappe que nous avons à accomplir. Deuxièmement - et cela est plus important - cela rend notre script plus facile à mettre à jour.

Au fur et à mesure que vous écrirez davantage de scripts shell (ou n'importe quel autre genre de programme), vous vous rendrez compte que les programmes sont rarement définitivement terminés. Ils sont modifiés et améliorés par leur créateur mais aussi par d'autres programmeurs. Et après tout, le développement de logiciels libres (Open Source) est conçu pour cela. Supposons, par exemple, que vous vouliez remplacer la phrase Informations sur mon système par Informations sur Linuxbox. Dans l'ancienne version du script, vous auriez eu à faire la modification en deux endroits. Dans la nouvelle version, qui fait usage de la variable titre, vous n'avez à faire la modification qu'en un seul endroit. Du fait que notre script est très bref, cela peut sembler insignifiant, mais lorsqu'un script devient plus long et plus compliqué, ce genre de considérations devient primordial. Jetez un coup d'œil à quelques scripts dans la Script Library pour avoir une idée de ce à quoi peut ressembler un script doté d'une taille importante.

Variables d'environnement

Quand vous commencez une session shell, des variables sont déjà prêtes pour votre usage. Elles sont définies dans des scripts qui sont exécutés chaque fois qu'un utilisateur se connecte. Pour voir toutes les variables qui sont dans votre environnement, utilisez la commande set. Une des variables de votre environnement contient le nom d'hôte de votre système. Il s'agit de la variable HOSTNAME. Nous allons ajouter cette variable à notre script comme cela :


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML

titre="Informations sur le système de"

cat <<- _EOF_

    <HTML>
    <HEAD>
        <TITLE>
        $titre $HOSTNAME
        </TITLE>
    </HEAD>

    <BODY>
     < H1 >$title $HOSTNAME< /H1 >
     </BODY>
     </HTML>
_EOF_

(NdT: Relecteur, ne pas omettre les blancs autour de la balise <H1>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Maintenant notre script inclura toujours le nom de la machine sur laquelle nous travaillons. Notez que pas convention les variables d'environnement sont toujours en majuscules.

Substitutions - 2ème partie

Dans notre dernière leçon, nous avons appris comment créer des variables et réaliser leur substitution. Dans cette leçon, nous allons étendre cette idée pour montrer comment nous pouvons substituer les résultats qui proviennent d'une commande.

La dernière fois que nous avons quitté notre script, il pouvait créer une page HTML qui contenait quelques simples lignes de texte, incluant le nom d'hôte de la machine que nous avons obtenu grâce à la variable d'environnement HOSTNAME. Maintenant, nous allons ajouter un horodatage à notre page pour indiquer quand la mise à jour a été faite pour la dernière fois, et quel utilisateur a fait cette mise à jour.


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML

titre="Informations sur le système de"

cat <<- _EOF_

    <HTML>
    <HEAD>
        <TITLE>
        $titre $HOSTNAME
        </TITLE>
    </HEAD>

    <BODY>
    < H1 >$titre $HOSTNAME</H1>
    <P>Mis à jour le $(date +"%x %r %Z") par $USER</P>
    </BODY>
    </HTML>
_EOF_

(NdT: Relecteur, ne pas omettre les blancs autour de la balise <H1>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Comme vous pouvez le voir, nous avons utilisé une autre variable d'environnement, USER, pour obtenir le nom de l'utilisateur. De plus, nous avons utilisé cette chose à l'air bizarre :

$(date +"%x %r %Z")

L'expression $() dit au shell, « substitue les résultats de la commande insérée entre les parenthèses ». Dans notre script, nous voulons que le shell insère les résultats de la commande date +"%x %r %Z" qui retourne la date courante et l'heure. La commande date possède de nombreux paramètres et options de mise en forme. Pour les voir tous, essayez ceci :

[moi@linuxbox moi]$ date --help | less

Sachez qu'il existe une syntaxe, plus ancienne et alternative pour "$(commande)" qui utilise le caractère " ` " appelé backtick (il s'agit en fait d'un accent grave flottant). Cette forme plus ancienne est compatible avec le premier shell Bourne (sh). J'ai tendance à ne pas utiliser l'ancienne forme puisque j'enseigne le Bash ici, pas le sh, et de plus, je trouve les backticks affreux. Le shell Bash supporte parfaitement les scripts écrits pour sh, aussi les formes suivantes sont équivalentes :

$(commande)

`commande`

--help et autres trucs

Comment apprenez-vous les commandes ? Eh bien, en plus de lire ce qui les concerne sur LinuxCommand.org, ou dans l'extraordinaire Guide avancé d'écriture des scripts Bash (fr) de Mendel Cooper, vous pouvez essayer d'utiliser les pages man pour les commandes concernées. Les Pages SuperMan sur LinuxCommand.org contiennent un jeu complet pour Red Hat 8.0. Mais que se passe-t-il si la commande n'a pas de page man ?

La première chose à utiliser est l'option --help. Tous les outils écrits par le projet GNU de la Free Software Foundation disposent de cette possibilité. Pour obtenir une brève liste des options d'une commande, tapez simplement :

[moi@linuxbox moi]$ commande --help

Beaucoup de commandes (en plus des outils GNU) soit accepteront l'option --help ou bien la considèreront comme une option invalide et afficheront un message d'usage de la commande que vous trouverez aussi bien pratique.

Si le résultat de l'option --help déroule plusieurs écrans, envoyez les résultats, dans un tube, vers less pour les voir, comme ceci :

[moi@linuxbox moi]$ commande --help | less

Certaines commandes n'ont pas de message d'aide, ou n'utilisent pas --help pour les appeler. Avec ces commandes vous pouvez utiliser la commande whatis pour obtenir une brève description de la commande (en anglais). Par exemple, ceci :

whatis cp

vous donnera une brève description de la commande cp.

Avec des commandes mystérieuses, j'utilise aussi parfois l'astuce suivante.

Premièrement, trouvez où est placé le fichier exécutable (cette astuce ne fonctionne qu'avec les programmes, pas avec les commandes internes (builtins) du shell). Ceci est facilement réalisable en tapant :

[moi@linuxbox moi]$ which commande

La commande which vous donnera le chemin et le nom de fichier du programme exécutable. Ensuite, utilisez la commande strings pour afficher le texte qui pourrait être intégré à l'intérieur du fichier exécutable. Par exemple, si vous désirez regarder à l'intérieur du programme Bash, vous pouvez faire :

[moi@linuxbox moi]$ which bash
/bin/bash
[moi@linuxbox moi]$ strings /bin/bash

La commande strings affichera tout contenu enterré dans le programme lisible par un humain. Cela peut inclure les notices de copyright, les messages d'erreur, le texte d'aide, etc.

Finalement, si vous avez une nature très inquisitrice, procurez-vous le code source de la commande et lisez-le. Même si vous ne pouvez pas pleinement comprendre le langage de programmation dans laquelle la commande est écrite, vous devriez être capable d'en acquérir un aperçu valable en lisant les commentaires de l'auteur dans les sources du programme.

Enfin, si vous cherchez des commandes concernant un certain aspect du système, vous pouvez aussi utiliser La commande apropos. Par exemple, pour trouver des commandes qui concernent le système xkb de gestion du clavier, vous pourriez taper apropos xkb.

Assignation du résultat d'une commande à une variable

Vous pouvez aussi assigner les résultats d'une commande à une variable :

maintenant=$(date +"%x %r %Z")

Vous pouvez même placer le contenu d'une variable à l'intérieur d'une autre variable, comme ceci :

maintenant=$(date +"%x %r %Z")
horodatage="Mis à jour le $maintenant par $USER"

Constantes

Comme le suggère le nom variable, le contenu d'une variable est sujet à modification. Ceci signifie qu'il est attendu que pendant l'exécution du script, une variable voie son contenu modifié par ce que vous faites.

D'un autre côté, il peut y avoir des valeurs qui, une fois établies, ne doivent jamais changer. Elles sont appelées constantes. J'aborde cela car c'est un concept répandu en programmation. La plupart des langages de programmation ont des aptitudes particulières pour utiliser des valeurs qui ne sont pas autorisées à modification. Bash aussi possède ces aptitudes, mais, pour être honnête, je ne l'ai jamais vu utilisé. A la place, si une valeur est voulue constante, on lui donne simplement un nom en majuscules. Les variables d'environnement sont habituellement considérées comme des constantes puisqu'elles sont rarement changées. Comme les constantes, les variables d'environnement reçoivent par convention un nom en majuscules. Dans les scripts qui suivent, j'utiliserai cette convention - noms en majuscules pour les constantes et noms en minuscules pour les variables.

Ainsi, avec tout ce que nous connaissons, notre programme ressemble à cela :


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML

TITRE="Informations sur le système de $HOSTNAME"
MAINTENANT=$(date +"%x %r %Z")
HORODATAGE="Mis à jour le $MAINTENANT par $USER"

cat <<- _EOF_

    <HTML>
    <HEAD>
        <TITLE>
        $TITRE
        </TITLE>
    </HEAD>

    <BODY>
    < H1 >$TITRE</H1>
    <P>$HORODATAGE</P>
    </BODY>
    </HTML>
_EOF_

(NdT: Relecteur, ne pas omettre les blancs autour de la balise <H1>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Protéger par des guillemets

Nous allons quitter notre script le temps d'une pause pour discuter de quelque chose que nous avons déjà mis en œuvre sans l'expliquer encore. Dans cette leçon nous aborderons la protection de certaines expressions par des guillemets. Cette technique est utilisée pour atteindre deux objectifs :

  1. Contrôler (en fait : restreindre) les possibilités de substitution.
  2. Réaliser des regroupements de mots.

Nous avons déjà utilisé les guillemets. Dans notre script, l'attribution de texte à nos constantes était réalisée à l'aide de guillemets :

TITRE="Informations sur le système de $HOSTNAME"
MAINTENANT=$(date +"%x %r %Z")
HORODATAGE="Mis à jour le $MAINTENANT par $USER"

Dans ce cas, le texte est entouré par des guillemets doubles. La raison pour laquelle nous utilisons les guillemets est ici de regrouper des mots : Bash va placer tous ce qui est entre guillemets, en bloc, dans la variable. Si nous ne les utilisions pas, Bash pourrait croire que tous les mots qui suivent le premier mot sont des commandes supplémentaires. Essayez, par exemple, cela :

[moi@linuxbox moi]$ TITRE=Informations sur le système de $HOSTNAME

Bash commence par placer la chaine de caractères Informations et elle seule dans la variable TITRE, puis il poursuit et tente de traiter le mot sur comme une commande, il s'aperçoit aussitôt que la commande sur n'existe pas : il émet alors un message d'erreur.

Guillemets doubles et guillemets simples

Le shell reconnait à la fois les guillemets simples et les guillemets doubles. Les lignes suivantes sont équivalentes :

var="ceci est du texte"
var='ceci est du texte'

Il existe cependant une différence importante entre les guillemets simples et les guillemets doubles. Les guillemets simples restreignent les possibilités de substitution. Comme nous l'avons vu dans la précédente leçon, vous pouvez placer des variables précédées du signe $ dans du texte compris entre des guillemets doubles et le shell réalise alors la substitution, il remplace la variable par son contenu. Vous pouvez observer cela avec la commande echo :

[moi@linuxbox moi]$ echo "Mon nom d'hôte est $HOSTNAME."
Mon nom d'hôte est linuxbox.

La valeur de la variable HOSTNAME est affichée.

Si nous remplaçons les guillemets doubles par des guillemets simple, le comportement change :

[moi@linuxbox moi]$ echo 'Mon nom d'hôte est $HOSTNAME.'
Mon nom d'hôte est $HOSTNAME.

Ici c'est le nom de la variable qui est affiché, et non son contenu.

Les guillemets doubles n'empêchent pas la substitution des mots qui commencent par "$" mais ils empêchent, en revanche, le développement des caractères de remplacement de l'expansion des noms de fichier. Par exemple, essayez ce qui suit :

[moi@linuxbox moi]$ echo *
[moi@linuxbox moi]$ echo "*"

Le joker * entre guillemets est affiché comme un simple caractère, sans être remplacé par quoi que ce soit.

Protéger (échapper) un seul caractère

Il existe un autre caractère de protection que vous pouvez rencontrer. C'est la barre oblique inverse (NdT ou antislash ou en anglais backslash). La barre oblique inverse dit au shell « d'ignorer le sens spécial du caractère qui suit ». On l'appelle aussi « caractère d'échappement ». Voici un exemple :

[moi@linuxbox moi]$ echo "Mon nom d'hôte est \$HOSTNAME."
Mon nom d'hôte est $HOSTNAME.

En utilisant la barre oblique inverse, le shell ignore le sens spécial qu'a d'habitude le symbole "$", $ est alors traité comme un caractère ordinaire. Etant donné que le shell a ignoré ce sens spécial, il ne réalise pas la substitution de $HOSTNAME. Voici un exemple plus utile :

[moi@linuxbox moi]$ echo "Mon nom d'hôte est \"$HOSTNAME\"."
Mon nom d'hôte est "linuxbox".

Comme vous pouvez le voir, utiliser la séquence \" vous permet de faire afficher des guillemets doubles par echo.

Comparer à ce qui vous obtiendriez si vous n'aviez pas « échappé » les guillemets doubles autour de la variable :

[moi@linuxbox moi]$ echo "Mon nom d'hôte est "$HOSTNAME"."
Mon nom d'hôte est linuxbox.

(ici echo affiche successivement trois arguments : la suite de caractères (sans les guillemets qui la délimitent) : "Mon nom d'hôte est ", puis la valeur de la variable HOSTNAME, puis la suite de caractères (réduite à un caractère), toujours sans les guillemets qui la délimitent : ".")
Quand ils ne sont pas échappés les guillemets sont considérés par la commande echo comme de simples délimiteurs de suites de caractères, qui n'ont pas à être affichés....

Vu ?

D'autres astuces avec la barre oblique inverse

Si vous regardez les pages de man de n'importe quel programme écrit par le Projet GNU, vous remarquerez qu'en ligne de commande, en plus des options s'écrivant avec un tiret et une seule lettre, il y a aussi des options avec des noms longs commençant avec deux tirets. Par exemple, ce qui suit est équivalent :

ls -r
ls --reverse

Pourquoi supporter les deux ? La forme courte est pour les opérateurs de saisie fainéants en ligne de commande et la forme longue est pour les scripts. J'utilise quelquefois des options obscures, et je trouve la forme longue utile si je dois revoir mes scripts des mois après leur saisie. Voir la forme longue m'aide à comprendre ce que fait l'option, m'économisant un voyage dans les pages de man. Un peu plus de frappe maintenant, et beaucoup moins de travail ensuite. La fainéantise, toujours et encore...

Comme vous pouvez le supposer, utiliser la forme longue peut rendre une simple ligne de commande très longue. Pour améliorer ce problème, vous pouvez utiliser une barre oblique inverse pour dire au shell d'ignorer le caractère de retour à la ligne, comme ceci :

ls -l \
   --reverse \
   --human-readable \
   --full-time

Utiliser une barre oblique inverse de cette manière, nous permet d'introduire des retours à la ligne dans notre commande. Remarquez que pour que cette astuce fonctionne, le retour à la ligne doit être frappé immédiatement après la barre oblique inverse. Si vous mettez une espace juste après la barre oblique, c'est l'espace qui sera ignorée, pas le retour à la ligne !

Notez que le caractère d'échappement lui-même peut être échappé par lui-même... Un \\ sera compris comme une simple barre oblique sans rôle spécial, voyez par exemple, pour reprendre un de nos exemples précédents en le modifiant un peu :

[moi@linuxbox moi]$ echo "Mon nom d'hôte est \\"$HOSTNAME\\"."
Mon nom d'hôte est \linuxbox\.

(la commande affiche : "Mon nom d'hôte est \\", qui se termine par une simple barre oblique, puis la valeur de la variable HOSTNAME, puis une barre oblique (\\) puis le point contenu dans ".")

Les barres obliques inverses sont aussi utilisées pour insérer des caractères spéciaux dans notre texte. On les appelle des « caractères spéciaux échappés ». Voici les plus communs :

Caractère spéciaux échappés Nom Utilisations possibles
\n nouvelle ligne Ajouter des lignes blanches au texte
\t tabulation Insertion d'une tabulation horizontale au texte
\a alerte Fait faire un bip au terminal
\f alimentation papier Envoyé à l'imprimante pour éjecter une page

Pour que la commande echo « comprenne » ces caractères, elle doit être employée avec l'option -e.

L'utilisation du caractère d'échappement antislash pour définir certains caractères spéciaux est très courante. Cette idée est apparue pour la première fois dans le langage de programmation C. Aujourd'hui, le shell, C++, perl, python, awk, tcl, et bien d'autres utilisent ce concept. Utiliser la commande echo avec l'option -e va nous permettre de le mettre en évidence :

$ echo -e "Insertion de plusieurs ligne blanches\n\n\n"

$ echo -e "Mots\tséparés\tpar\tdes\ttabulations\thorizontales."
mots  séparés   par   des    tabulations  horizontales

$ echo -e "\aMon ordinateur a fait \"bip\"."
Mon ordinateur à fait "bip"

Utiliser les fonctions du shell

Au fur et à mesure que les programmes deviennent plus longs et plus complexes, ils deviennent aussi plus difficiles à concevoir, à écrire et et à mettre à jour. Même lorsqu'on a beaucoup de courage, il est donc souvent préférable de séparer une seule et grande tâche en plusieurs petites tâches.

Dans cette leçon, nous allons ainsi décomposer notre script monobloc en plusieurs fonctions séparées.

Pour vous familiariser avec ce concept, considérons la description d'une tâche quotidienne : aller sur le marché acheter des aliments.

Imaginons que nous décrivions cette tâche à un martien.

Notre description de premier niveau pourrait ressembler à ceci :

  1. Quitter la maison.
  2. Conduire jusqu'au marché.
  3. Garer la voiture.
  4. Entrer dans le marché.
  5. Acheter la nourriture.
  6. Conduire jusqu'à la maison.
  7. Garer la voiture.
  8. Entrer dans la maison.

Cette description couvre la totalité du processus consistant à aller au marché ; cependant notre martien, réclamera sans doute plus de détails. Par exemple, la sous-tâche « Garer la voiture » pourrait être décrite comme suit :

  1. Trouver une place libre.
  2. Placer la voiture à sur une place libre.
  3. Arrêter le moteur.
  4. Mettre le frein à mains.
  5. Sortir de la voiture.
  6. Verrouiller la voiture.

Bien sûr, la tâche « Arrêter le moteur » possède plusieurs étapes comme « tourner la clé de contact » et « enlever la clé », etc.

Ce procédé consistant à identifier les étapes de premier niveau puis à développer de façon progressive des étapes plus détaillées de ce premier niveau est appelé « conception descendante ». Cette technique vous permet de décomposer une tâche importante et complexe en plusieurs tâches petites et simples.

Alors que notre script continue de grandir, nous allons utiliser la conception descendante pour nous aider à planifier et écrire le code de notre script.

Si nous regardons les tâches de premier niveau de notre script, nous trouvons la liste suivante :

  1. Ouvrir la page.
  2. Ouvrir la section HEAD.
  3. Ecrire le titre.
  4. Fermer la section HEAD.
  5. Ouvrir la section BODY.
  6. Ecrire le titre.
  7. Ecrire l'horodatage.
  8. Fermer la section BODY.
  9. Fermer la page.

Toutes ces tâches sont implémentées, mais nous souhaitons en ajouter. Insérons quelques tâches additionnelles après la tâche 7 :

  1. Ecrire l'horodatage
  2. Ecrire l'information de version du système
  3. Ecrire le temps de fonctionnement
  4. Donner l'espace disque
  5. Donner l'espace du répertoire personnel.
  6. Fermer la section BODY
  7. Fermer la page

Ce serait bien s'il pouvait exister des commandes qui réalisaient ces tâches additionnelles. S'il y en avait, on pourrait utiliser la substitution de commandes pour les placer dans notre script comme cela :


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML d'information sur le système

##### Constantes

TITRE="Informations sur le système de $HOSTNAME"
MAINTENANT=$(date +"%x %r %Z")
HORODATAGE="Mis à jour le $MAINTENANT par $USER"

##### Partie principale

cat <<- _EOF_

    <html>
    <head>
        <title>$TITRE</title>
    </head>

    <body>
        < h1 >$TITRE</h1>
        <p>$HORODATAGE</p>
        $(info_système)
        $(montrer_fonctionnement)
        $(espace_disque)
        $(espace_home)
    </body>
    </html>
_EOF_

(NdT: Relecteur, ne pas omettre les blancs autour de la balise <h1>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Puisqu'il n'existe pas de commandes qui font exactement ce que nous voulons, on peut les créer en utilisant les « fonctions du shell ».

Comme nous l'avons appris dans la leçon sur les fonctions du shell, ces fonctions se comportent comme « de petits programmes à l'intérieur de programmes » et nous permettent de suivre les principes de conception descendante. Pour ajouter des fonctions du shell à notre script, nous le modifions ainsi :


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML d'information sur le système

##### Constantes

TITRE="Informations sur le système de $HOSTNAME"
MAINTENANT=$(date +"%x %r %Z")
HORODATAGE="Mis à jour le $MAINTENANT par $USER"

##### Fonctions

function info_système
{

}

function montrer_fonctionnement
{

}

function espace_disque
{

}

function espace_home
{

}

function montrer_info
{

}

##### Partie principale

cat <<- _EOF_

    <html>
    <head>
        <title>$TITRE</title>
    </head>

    <body>
        < h1 >$TITRE</h1>
        <p>$HORODATAGE</p>
        $(info_système)
        $(montrer_fonctionnement)
        $(espace_disque)
        $(espace_home)
    </body>
    </html>
_EOF_

(NdT: Relecteur, ne pas omettre les blancs autour de la balise <h1>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Deux choses importantes au sujet des fonctions : tout d'abord, elles doivent apparaître avant qu'on tente de s'en servir. Ensuite, le corps de la fonction (la portion entre les caractères { et }) doit contenir au moins une commande valide. Tel qu'il est écrit, le script ne s'exécutera pas sans erreur, car le corps des fonctions est vide. Le moyen simple d'éviter cela est de placer une instruction de retour (une ligne réduite à : return, par exemple) dans chaque corps de fonction. Après avoir fait cela, le script s'exécutera avec succès (mais les fonctions ne feront toujours rien, bien sûr, puisqu'elles seront encore réduites à une instruction qui leur demande de s'arrêter... Nous allons voir à la section suivante un moyen plus astucieux de rendre exécutables des fonctions encore non écrites).

Conservez vos scripts en état de marche

Quand vous développez un programme, il est souvent recommandé d'ajouter une petite portion de code, exécuter le script, ajouter un peu plus de code, exécuter le script, et ainsi de suite. De cette façon, si vous introduisez une erreur dans votre code, ce sera plus facile de la trouver et de la corriger.

Au fur et à mesure que vous ajoutez des fonctions à votre script, vous pouvez aussi utiliser une technique appelée élément de remplacement (NdT stubbing en anglais) pour vous aider à mieux apercevoir la logique de votre script en développement. L'élément de remplacement fonctionne comme cela : Imaginez que nous voulions créer une fonction appelée info_systeme mais que nous n'ayons pas encore trouvé tous les détails de son code. Plutôt que de bloquer le développement du script jusqu'à ce que nous en ayons terminé avec info_systeme, nous ajoutons simplement une commande echo comme cela :

function info_systeme
{
     # Elément de remplacement temporaire de la fonction
     echo "fonction info_systeme"
}

De cette façon, notre script s'exécutera correctement, même si nous n'avons pas encore terminé la fonction info_système. Nous pourrons remplacer plus tard l'élément de remplacement temporaire par la version complète en état de fonctionnement.

La raison pour laquelle nous utilisons une commande echo est que, ainsi, nous obtenons un retour d'information du script qui indique que les fonctions ont bien été exécutées.

Continuons et écrivons les éléments de remplacement pour nos nouvelles fonctions et conservons notre script en état de marche.


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML d'information sur le système

##### Constantes

TITRE="Informations sur le système de $HOSTNAME"
MAINTENANT=$(date +"%x %r %Z")
HORODATAGE="Mis à jour le $MAINTENANT par $USER"

##### Fonctions

function info_systeme
{
     # Elément de remplacement temporaire de la fonction
     echo "fonction info_systeme"
}

function montrer_fonctionnement
{
     # Elément de remplacement temporaire de la fonction
     echo "fonction montrer_fonctionnement"
}

function espace_disque
{
     # Elément de remplacement temporaire de la fonction
     echo "fonction espace_disque"
}

function espace_home
{
     # Elément de remplacement temporaire de la fonction
     echo "fonction espace_home"
}

function montrer_info
{
     # Elément de remplacement temporaire de la fonction
     echo "fonction montrer_info"
}

##### Partie principale

cat <<- _EOF_

    <html>
    <head>
        <title>$TITRE</title>
    </head>

    <body>
        < h1 >$TITRE</h1>
        <p>$HORODATAGE</p>
        $(info_systeme)
        $(montrer_fonctionnement)
        $(espace_disque)
        $(espace_home)
    </body>
    </html>
_EOF_

(NdT: Relecteur, ne pas omettre les blancs autour de la balise <h1>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Du vrai travail

Dans cette leçon, nous allons développer quelques-unes de nos fonctions du shell et obtenir de notre script qu'il produise une information utile.

montrer_fonctionnement

La fonction montrer_fonctionnement affichera la sortie de la commande uptime. Elle renseigne sur plusieurs faits intéressants concernant le système, y compris le temps depuis lequel le système est "up" (en fonctionnement) depuis le dernier redémarrage, le nombre d'utilisateurs et le taux de charge du système.

[moi@linuxbox moi]$ uptime
18:35:48 up  15:11,  2 users,  load average: 0.41, 0.18, 0.10

(sous Mandriva vous trouverez une explication claire, en français, de la sortie de la commande uptime dans man uptime).

Pour inscrire la sortie de la commande uptime dans notre page HTML, nous allons coder notre fonction du shell comme ceci, en remplaçant notre élément de remplacement temporaire par la version finale :

function montrer_fonctionnement
{
     echo "< h2 >fonctionnement du système</h2>"
     echo "<pre>"
     uptime
     echo "< /pre >"
}

(NdT: Relecteur, ne pas omettre les blancs autour des balises <h2> et </pre>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Comme vous pouvez le voir, cette fonction renvoie du texte contenant un mélange de balises HTML avec la sortie de la commande. Quand se produit la substitution dans body de la partie principale de notre programme, la sortie de notre fonction écrit quelques lignes du here script.

espace_disque

La fonction espace_disque utilise la commande df pour fournir un état de la place utilisée par tous les systèmes de fichiers montés.

[moi@linuxbox moi]$ df

Sys. de fich.         Tail. Occ. Disp. %Occ. Monté sur

/dev/sda5             9,8G  4,4G  5,0G  47% /
/dev/sda11             22G   11G   12G  49% /home
/dev/sda9             7,7G  4,5G  2,8G  62% /user
/dev/sda8              19G  1,9G   18G  10% /mnt/backup
/dev/sda1              79G   17G   62G  21% /mnt/windows

En termes de structure, la fonction espace_disque est très similaire à la fonction montrer_fonctionnement :

function espace_disque
{
     echo "< h2 >Espace disque des systèmes de fichiers</h2>"
     echo "<pre>"
     df
     echo "< /pre >"
}

(NdT: Relecteur, ne pas omettre les blancs autour des balises <h2> et </pre>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

espace_home

La fonction espace_home affichera la quantité d'espace disque occupé par chaque utilisateur dans son répertoire personnel. Il l'affichera sous forme d'une liste, triée par ordre décroissant de la quantité d'espace occupé.

function espace_home
{
     echo "< h2 >Espace disque occupé par utilisateur</h2>"
     echo "<pre>"
     echo "octets répertoires"
     du -sk /home/*|sort -nr
     echo "< /pre >"
}

(NdT: Relecteur, ne pas omettre les blancs autour des balises <h2> et </pre>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Remarquez que pour faire fonctionner correctement cette fonction, le script doit être exécuté par l'administrateur du système (root), puisque la commande du exige les privilèges d'administrateur pour examiner le contenu du répertoire /home.

info_système

Nous ne sommes pas encore prêts pour pouvoir terminer la fonction info_système. En attendant, nous allons améliorer le code de l'élément de remplacement, afin qu'il produise du code HTML valide :

function info_système
{
     echo "< h2 >Informations sur le système</h2>"
     echo "<p>fonction pas encore implémentée</p>"
}

(NdT: Relecteur, ne pas omettre les blancs autour de la balise <h2>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Contrôle des flux - 1ère partie

Dans cette leçon, nous allons regarder comment ajouter de l'intelligence à notre script. Jusqu'à maintenant, notre script a seulement consisté en une séquence de commandes qui commence à la première ligne et continue ligne par ligne jusqu'à atteindre la fin. La plupart des programmes font mieux que cela. Ils prennent des décisions et réalisent différentes actions dépendantes de conditions.

Le shell fournit plusieurs mots-clés que nous pouvons utiliser pour contrôler le flux de l'exécution dans notre programme. ils comprennent :

  • if
  • exit
  • for
  • while
  • until
  • case
  • break
  • continue

if

Le premier mot-clé que nous regarderons est if. Il est d'apparence assez simple ; il permet une décision basée sur une condition. if possède trois formes d'emploi :

(NdT: traduction des mots réservés:

  • if : si
  • then : alors
  • else : sinon)

# Première forme

if condition ; then
     commandes
fi

# Deuxième forme

if condition ; then
     commandes
else
     commandes
fi

# Troisième forme

if condition ; then
     commandes
elif condition
     commandes
fi

Dans la première forme, si la condition est vraie, alors les commandes sont exécutées. Si la condition est fausse, rien n'est fait.

Dans le seconde forme, si la condition est vraie, alors le premier jeu de commandes est réalisé. Si la condition est fausse, alors le deuxième jeu de commandes est réalisé.

Dans la troisième forme, si la condition est vraie, alors le premier jeu de commandes est réalisé. Si la condition est fausse et la deuxième condition est vraie, alors le deuxième jeu de commandes est réalisé.

Qu'est ce qu'une "condition" ?

Pour être honnête, cela m'a pris beaucoup de temps pour vraiment comprendre comment cela fonctionne ! Et pour répondre à cette question, il nous faudra d'abord discuter ici un autre comportement fondamental des commandes.

Les codes de sortie

Une application Unix correctement écrite doit informer le système d'exploitation du succès ou de l'échec de son exécution. Elle le fait au moyen de codes de sortie. Il s'agit d'une valeur numérique comprise entre 0 et 255. 0 indique le succès ; toute autre valeur indique l'échec. Les codes de sortie possèdent deux caractéristiques importantes. Premièrement, ils peuvent être utilisés pour détecter et gérer les erreurs et deuxièmement, ils peuvent être utilisés pour réaliser des tests vrai/faux.

Il est aisé de voir à quoi gérer les erreurs pourrait être utile. Par exemple, dans un script de notre cru nous pourrions souhaiter savoir quel type de matériel est installé, afin de l'inclure dans notre rapport. Typiquement, nous allons essayer d'interroger le matériel, et si une erreur est rapportée par tout outil que nous utilisons pour l'interrogation, notre script sera capable de sauter la portion de script qui traite le matériel manquant.

Nous pouvons aussi utiliser les codes de sortie pour prendre une simple décision vrai/faux. Nous traitons ce cas maintenant.

Tests

La commande test est utilisée le plus souvent avec la commande if pour prendre une décision vrai/faux. La commande est inhabituelle en cela qu'elle a deux formes syntaxiques différentes :


# Première forme

test expression

# Seconde forme

[expression]

La commande test fonctionne simplement. Si l'expression donnée est vraie, test quitte avec le code zéro ; sinon, elle quitte avec le code 1.

La caractéristique agréable de test est la diversité des expressions que vous pouvez créer. Voici un exemple :

if [-f .bash_profile]; then
     echo "Vous avez un fichier .bash_profile. Tout va bien"
else
     echo "Aïe ! Vous n'avez pas de fichier .bash_profile !"
fi

Dans cet exemple, nous utilisons l'expression "-f .bash_profile". Elle demande  « Est-ce que .bash_profile est un fichier ? » Si l'expression est vraie, alors test quitte avec le statut zéro (signifiant vrai) et la commande if exécute la commande suivant le mot then. Si l'expression est fausse, alors test quitte avec le statut un et la commande if exécute la commande suivant le mot else.

Voici une liste partielle des conditions que test peut évaluer. Puisque test est une commande interne du shell (shell builtin), utilisez "help test"" pour avoir une liste complète.

Expression Description
-d fichier Vrai si fichier est un répertoire
-e fichier Vrai si fichier existe
-f fichier Vrai si fichier existe et est un fichier ordinaire
-L fichier Vrai si fichier est un lien symbolique
-r fichier Vrai si fichier est un fichier lisible par vous
-w fichier Vrai si fichier est un fichier modifiable par vous
-x fichier Vrai si fichier est un fichier exécutable par vous
fichier1 -nt fichier2 Vrai si fichier1 est plus récent (d'après l'heure de modification) que fichier2
fichier1 -ot fichier2 Vrai si fichier1 est plus vieux que fichier2
-z chaîne_de_caractères Vrai si chaîne_de_caractères est vide
-n chaîne_de_caractères Vrai si chaîne_de_caractères n'est pas vide
chaîne_de_caractères1 = chaîne_de_caractères2 Vrai si chaîne_de_caractères1 = chaîne_de_caractères2
chaîne_de_caractères1 != chaîne_de_caractères2 Vrai si chaîne_de_caractères1 différent de chaîne_de_caractères2

Avant que nous ne continuions, je voudrais expliquer le reste de l'exemple ci-dessus, car il contient aussi d'autres concepts importants.

Dans la première ligne du script, nous voyons un if suivi par la commande test, suivie par un point-virgule, et finalement le mot then. J'ai choisi d'utiliser la forme [ expression ] de la commande test parce que la plupart des gens pensent qu'elle est plus facile à lire. Remarquez que les espaces entre le "[" et le début de l'expression sont nécessaires. De même pour l'espace entre la fin de l'expression et le "]" de fin.

Le point-virgule est un séparateur de commandes. Son utilisation vous permet de mettre plusieurs commandes sur une ligne. Par exemple :

[moi@linuxbox moi]$ clear; ls

effacera l'écran et exécutera la commande ls.

J'ai utilisé le point-virgule comme je l'ai fait pour me permettre de mettre le mot then sur la même ligne que le if, parce que je pense que c'est plus facile à lire de cette manière.

Sur la seconde lligne, il y a notre vieil ami echo. La seule chose à noter sur cette ligne est l'indentation. Toujours au bénéfice de la lisibilité, il est traditionnel d'appliquer une indentation à tous les blocs de code conditionnel, c'est à dire à tout code qui ne sera exécuté que si certaines conditions sont remplies. Le shell ne l'exige pas ; c'est fait pour rendre le code plus facile à lire.

En d'autres termes, on pourrait écrire ce qui suit et obtenir le même résultat :


# Forme équivalente

if [-f .bash_profile]
then
     echo "Vous avez un fichier .bash_profile. Tout va bien"
else
     echo "Aïe ! Vous n'avez pas de fichier .bash_profile !"
fi

# Autre forme équivalente

if [-f .bash_profile]
then echo "Vous avez un fichier .bash_profile. Tout va bien"
else echo "Aïe ! Vous n'avez pas de fichier .bash_profile !"
fi

exit

Si nous voulons être de bons rédacteurs de scripts, nous devrons déterminer le code de sortie de fin de script. Pour cela, nous devons utiliser la commande exit. Elle provoque l'arrêt immédiat du script et attribue au code de sortie la valeur donnée en argument. Par exemple :

exit 0

arrête le script et fixe le code de sortie à 0 (succès), alors que

exit 1

arrête le script et fixe le code de sortie à 1 (échec).

Détecter l'administrateur

La dernière fois que nous avons quitté notre script, nous avions besoin qu'il soit exécuté avec les privilèges d'administrateur. Ceci parce que la fonction espace_home a besoin d'examiner la taille du répertoire personnel de chaque utilisateur et que seul l'administrateur peut faire cela.

Mais qu'arrive t-il si un simple utilisateur exécute notre script ? Il produit alors quantité d'horribles messages d'erreur. Ne pourrait-on pas faire quelque chose dans le script pour l'arrêter si un utilisateur ordinaire essaye de l'exécuter ?

La commande id peut nous dire qui est l'utilisateur en cours. Lorsqu'elle est utilisée avec l'option -u, elle renvoie l'identifiant numérique de l'utilisateur en cours.

[moi@linuxbox moi]$ id -u
501
[moi@linuxbox moi]$ su
password:
[moi@linuxbox moi]# id -u
0

Si l'administrateur exécute id -u, la commande affichera "0". Ce résultat peut être la base de notre test :

if [$(id -u) = "0"]; then
     echo "Administrateur"
fi

Dans cet exemple, si la sortie de la commande id -u est égale à la chaîne de caractères "0", alors s'affiche la chaîne de caractères "Administrateur".

Bien que ce code détecte si l'utilisateur est l'administrateur, il ne résout pas vraiment notre problème. Nous désirons arrêter le script si l'utilisateur n'est pas l'administrateur, aussi nous allons l'écrire comme ceci :

if [$(id -u) != "0"]; then
     echo "Vous devez être l'administrateur pour exécuter ce script" >&2
     exit 1
fi

Avec ce script, si la sortie de la commande id -u n'est pas égal à 0, alors le script affiche un message d'erreur, s'arrête, et fixe le code de sortie à 1, indiquant au système d'exploitation que le script s'est achevé sur un échec.

Remarquez le ">&2" à la fin de la commande echo. C'est une autre forme de redirection des entrées/sorties. Vous la remarquerez souvent dans les routines qui affichent des messages d'erreur. Si cette redirection n'était pas faite, le message d'erreur irait dans la sortie standard. Avec cette redirection, le message est envoyé vers la sortie erreur standard. Puisque nous exécutons notre script et redirigeons sa sortie standard vers un fichier, nous souhaitons que les messages d'erreur soient séparés de la sortie normale.

Nous pourrions mettre cette routine près du début de notre script pour qu'il ait une chance de détecter une possible erreur avant qu'il ne soit trop tard, mais afin de pouvoir exécuter ce script en tant qu'utilisateur ordinaire, nous utiliserons plutôt la même idée et modifierons la fonction espace_home pour tester la disponibilité des privilèges requis, comme cela :

function espace_home
{
     # Seul l'administrateur peut obtenir cette information

     if ["$(id -u)" = "0"]; then
          echo "< h2 >Espace disque du répertoire home par utilisateur</h2>"
          echo "<pre>"
          echo "Octets   Répertoire"
              du -sk  /home/* |sort -nr
          echo "< /pre >"
     fi
} # fin de espace_home

(NdT: Relecteur, ne pas omettre les blancs autour des balises <h2> et </pre>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

De cette façon, si un utilisateur ordinaire exécute le script, le code qui pose problème sera sauté et non pas exécuté, et le problème sera résolu.

Evitez les ennuis

Maintenant que nos scripts deviennent un peu plus compliqués, je désire souligner des erreurs classiques que vous pourriez faire. Pour cela, créez le script suivant appelé trouble.bash. Assurez vous de l'entrer exactement tel qu'il est écrit :


# !/bin/bash

nombre=1
if [$nombre = "1"]; then
     echo "Le nombre égale 1"
else
    echo "Le nombre n'égale pas 1"
fi

Quand vous exécutez ce script, il devrait afficher la ligne "Le nombre égale 1" car, eh bien, le nombre égale 1. Si vous n'obtenez pas la sortie attendue, vérifiez votre frappe ; vous avez fait une erreur.

Variables vides

Editez le script pour changer la ligne 3 :

nombre=1

en

nombre=

et exécutez le script à nouveau. Cette fois vous devriez obtenir ceci :

[moi@linuxbox moi]$ ./trouble.bash
./trouble.bash: line 4: [: =: unary operator expected
Le nombre n'égale pas 1

Comme vous pouvez le voir, Bash affiche un message d'erreur quand nous exécutons le script. Vous pensez probablement qu'en effaçant le "1" en ligne 3, nous avons créé une erreur de syntaxe dans cette ligne 3, mais non ! Regardons à nouveau le message d'erreur :

./trouble.bash: line 4: [: =: unary operator expected

Nous voyons que ./trouble.bash rapporte une erreur et que l'erreur vient de "[". Rappelez-vous que "[" est une abréviation pour la commande du shell test. De cela, on peut déduire que l'erreur s'est produite ligne 4, et non ligne 3.

Tout d'abord, laissez-moi vous dire qu'il n'y a rien d'incorrect dans la ligne 3. nombre= est une syntaxe tout à fait correcte. Vous pouvez quelquefois avoir besoin d'une variable dont la valeur est... rien. Vous pouvez avoir confirmation de cela en l'essayant en ligne de commande :

[moi@linuxbox moi]$ nombre=
[moi@linuxbox moi]$

Vous voyez bien : pas de message d'erreur. Mais alors, qu'est-ce qui ne va pas ligne 4 ? Cela fonctionnait pourtant bien auparavant.

Pour comprendre cette erreur, nous devons voir ce que voit le shell. Rappelez-vous que le shell passe beaucoup de son temps à substituer du texte. En ligne 4, le shell substitue la valeur de nombre là où il voit $nombre. Dans notre premier essai (quand nombre=1), le shell remplace $nombre par 1, ce qui donne :

if [1 = "1"]; then

Cependant, quand on ne fixe rien pour nombre (nombre=), le shell voit cela après la substitution :

if [= "1"]; then

Ce qui est une erreur. Cela explique aussi le reste du message d'erreur reçu. Le "=" est un opérateur binaire ; c'est-à-dire qu'il attend deux valeurs pour agir - une de chaque côté. Ce que le shell tentait de nous dire était qu'il n'y avait qu'une valeur et il aurait dû y avoir dans ce cas un opérateur unaire (NdT : unary operator expected = opérateur unaire attendu) comme "!", qui opère sur une seule valeur.

Pour résoudre le problème, remplacez la ligne 4 par :

if ["$nombre" = "1"]; then

Maintenant quand le shell réalise la substitution, il voit :

if ["" = "1"]; then

lequel exprime correctement notre intention.

Ceci nous amène à une chose importante dont il convient de se souvenir quand on écrit un script. Voyons ce qui arrive lorsqu'une variable est positionnée sur... rien.

Guillemets oubliés

Editez la ligne 5 et effacez le dernier guillemet à la fin de la ligne :

echo "Le nombre égale 1

exécutez à nouveau le script. vous devriez avoir cela :

[moi@linuxbox moi]$ ./trouble.bash
./trouble.bash: line 7: unexpected EOF while looking for matching `''
./trouble.bash: line 9: syntax error: unexpected end of file

(NdT:

./trouble.bash: ligne 7: fin de fichier inattendue pendant l'attente du " correspondant

./trouble.bash: ligne 9: erreur de syntaxe: fin de fichier inattendue)

Ici nous avons un autre cas d'erreur dans une ligne provoquant un problème plus loin dans le script. Ce qui arrive est que le shell surveille l'arrivée du guillemet de fermeture pour lui indiquer où est la fin de la chaîne de caractères, mais arrive à la fin du fichier avant de l'avoir trouvée.

Ces erreurs peuvent être très difficiles à trouver dans un long script. C'est une raison pour laquelle vous devriez tester vos scripts fréquemment quand vous les écrivez, ainsi il y a moins de code nouveau à tester à chaque fois. Je pense aussi que les éditeurs de texte avec la surbrillance syntaxique (comme nedit, kate ou kwrite) rendent ces bogues plus faciles à trouver.

Isoler les problèmes

Trouver les bogues dans vos programmes peut parfois être très difficile et frustrant. Voici quelques techniques qui pourront vous être utiles :

Isolez des blocs de code en "les transformant en commentaires". Ce truc consiste à ajouter des caractères de déclaration de commentaire au début de chaque ligne de code pour interdire au shell de les lire. Dans bien des cas, vous appliquerez cela à un bloc de code pour voir si un problème particulier disparaît. Ainsi, vous pourrez détecter la partie du programme qui cause (ou ne cause pas) le problème.

Par exemple, quand nous cherchions les guillemets manquants on aurait pu faire cela :


# !/bin/bash

nombre=1
if [$nombre = "1"]; then
     echo "Le nombre égale 1

# else
# echo "Le nombre n'égale pas 1"

fi

En transformant en commentaire la clause else et en exécutant le script, on pouvait montrer que le problème n'était pas dans la clause else même si le message d'erreur le suggérait.

Utilisez la commande echo pour vérifier vos hypothèses. Alors que vous gagnez en expérience dans la traque des bogues, vous découvrirez que bien souvent les bogues ne sont pas là où on les attendait. Un problème fréquent est de faire une fausse hypothèse dans l'interprétation de votre programme. Vous verrez un problème se manifester à un certain point du programme et supposerez que le problème est là. C'est souvent faux, comme nous l'avons vu. Pour éviter cela, vous devriez placer une commande echo dans votre code alors que vous cherchez le bogue, pour faire afficher des messages qui vous confirmeront que le programme est bien en train de réaliser ce qui est attendu. Il existe deux sortes de messages que vous pouvez insérer.

Le premier type annonce simplement l'atteinte d'un certain point du programme. Nous avons vu un cas de ce genre dans notre précédente discussion sur les éléments de remplacement. Il est utile de s'assurer que le flux du programme se produit tel que nous l'espérons.

Le second type affiche la valeur d'une variable (ou de variables) utilisée(s) dans un calcul ou un test. Vous découvrirez souvent qu'une portion de votre programme échoue car quelque chose de présumé correct plus en amont dans le programme est en réalité faux et cause l'échec du programme par la suite.

Regardez votre script fonctionner

Il est possible d'obtenir de Bash qu'il vous montre ce qu'il fait lorsque vous exécutez votre script. Pour cela, ajoutez un "-x" à la première ligne du script, comme ceci :


# !/bin/bash -x

Maintenant, lorsque vous exécutez le script, Bash affichera chaque ligne (avec les substitutions réalisées) telles qu'il les exécute. Cette technique est appelée traçage. Voici à quoi cela ressemble :

[moi@linuxbox moi]$ ./trouble.bash
+ nombre=1
+ '[' 1 = 1 ']'
+ echo 'Le nombre égale 1'
Le nombre égale 1

Alternativement, vous pouvez utiliser la commande set à l'intérieur du script pour activer ou désactiver le traçage. Utiliser

set -x pour activer et set +x pour désactiver. Par exemple :


# !/bin/bash

nombre=1

set -x
if [$nombre = "1"]; then
     echo "Le nombre égale 1"
else
     echo "Le nombre n'égale pas 1"
fi
set +x

Entrée clavier et arithmétique

Jusqu'à maintenant, nos scripts n'étaient pas interactifs. C'est vrai, ils ne demandaient pas l'entrée d'informations par l'utilisateur. Dans cette leçon, nous allons voir comment le script peut poser des questions, obtenir et utiliser les réponses.

read

Pour obtenir l'entrée d'informations depuis le clavier, vous pouvez utiliser la commande read (NdT : to read = lire), cette commande enregistre l'entrée du clavier et l'assigne à une variable. Voici un exemple :


# !/bin/bash

echo -n "Entrez du texte > "
read texte
echo "Vous avez entré : $texte"

Comme vous pouvez le voir, à la ligne 3, nous avons provoqué l'affichage d'une invite. Remarquez que le "-n" donné à la commande echo provoque le maintien du curseur sur la même ligne ; avec cette option, la commande echo ne génèrera pas de retour charriot après le texte de l'invite que vous lui avez demandé d'afficher.

Ensuite, nous invoquons la commande read avec son argument "texte". Le programme attendra alors, jusqu'à ce que l'utilisateur frappe quelque chose qui soit suivi par un retour charriot (touche Entrée), puis il assignera ce qui aura été frappé à la variable texte.

Voici le script en action :

[moi@linuxbox moi]$ read_demo.bash
Entrez du texte > Voici du texte
Vous avez entré : Voici du texte

Si vous n'avez pas donné de nom de variable à la commande read pour l'enregistrement de l'entrée, elle utilisera la variable d'environnement REPLY.

La commande read possède aussi quelques options. Les deux plus intéressantes sont -t et -s. L'option -t suivie d'un nombre de secondes amène une limitation du temps pour la commande read. Cela signifie que la commande abandonnera après le nombre de secondes spécifié si aucune réponse n'a été reçue de l'utilisateur. Cette option pourrait être utilisée dans le cas d'un script qui doit continuer (peut-être en utilisant une réponse par défaut) même si l'utilisateur ne répond pas à l'invite. Voici l'option -t en action :


# !/bin/bash

echo -n "Dépéche toi et tape quelque chose ! > "
if read -t 3 reponse; then
     echo "Bravo, vous l'avez fait à temps !"
else
     echo "Désolé, vous êtes trop lent !"
fi

L'option -s interdit l'affichage de ce que frappe l'utilisateur. C'est utile quand vous demandez à l'utilisateur d'entrer un mot de passe ou toute autre information liée à la sécurité.

Arithmétique

Etant donné que nous travaillons sur un ordinateur, il est naturel de s'attendre à ce qu'il puisse réaliser du calcul arithmétique simple. Le shell est pourvu de possibilités de calcul des nombres entiers.

Qu'est-ce qu'un entier ? Ce sont tous les nombres comme 1, 2, 458, -2859. Ce ne sont pas les nombres décimaux comme 0,5 ; 0,333 ; ou 3,1415. Si vous avez affaire à des nombres décimaux, il existe un programme à part appelé bc qui fournit un langage de calcul de précision quelconque. Il peut être utilisé dans des scripts shell, mais cela dépasse le périmètre de ce tutoriel.

Supposons que vous désirez utiliser la ligne de commande comme calculatrice élémentaire. Vous pouvez le faire comme ceci :

[moi@linuxbox moi]$ echo $((2+2))

Comme vous le voyez, quand vous entourez une expression arithmétique avec des doubles parenthèses, le shell réalisera l'opération arithmétique.

Remarquez l'absence d'importance des espaces :

[moi@linuxbox moi]$ echo $((2+2))
4
[moi@linuxbox moi]$ echo $(( 2+2 ))
4
[moi@linuxbox moi]$ echo $(( 2 + 2 ))
4

Le shell peut réaliser diverses opérations arithmétiques courantes (ou pas si courantes). Voici un exemple :


# !/bin/bash

premier_nom=0
second_nom=0

echo -n "entrez le premier nombre --> "
read premier_nom
echo -n "entrez le second nombre --> "
read second_nom

echo "premier nombre + second nombre = $((premier_nom + second_nom))"
echo "premier nombre - second nombre = $((premier_nom - second_nom))"
echo "premier nombre * second nombre = $((premier_nom * second_nom))"
echo "premier nombre / second nombre = $((premier_nom / second_nom))"
echo "premier nombre % second nombre = $((premier_nom % second_nom))"
echo "premier nombre élevé à la"
echo "puissance du second nombre  = $((premier_nom ** second_nom))"

Remarquez que le caractère de début "$" n'est pas nécessaire pour faire référence à des variables à l'intérieur des expressions arithmétiques telles que premier_nom + second_nom.

Essayez ce programme et regardez comment il gère la division (souvenez vous qu'il s'agit d'une division avec uniquement des nombres entiers) et comment il gère les grands nombres. Les nombres trop grands dépassent la capacité comme un compteur kilométrique dans une voiture quand vous dépassez le nombre maximum de kilomètres pour lesquels il a été conçu. Il redémarre, mais en passant d'abord par tous les nombres négatifs en raison de la façon dont les nombres entiers sont représentés en mémoire. La division par zéro (qui est mathématiquement invalide) provoque, comme il se doit, une erreur.

Je suis sûr que vous reconnaissez les quatre premières opérations que sont l'addition, la soustraction, la multiplication et la division, mais que la cinquième vous est peut-être moins familière. Le symbole "%" représente le reste (aussi appelé modulo). Cette opération réalise une division mais au lieu d'en donner le quotient, elle en donne le reste. Bien que cela ne semble pas très utile, elle fournit, en fait, une précieuse aide pour écrire des programmes. Par exemple, quand le reste d'une opération est zéro, cela indique que le premier nombre est un multiple entier du second. Cela peut être très pratique :


# !/bin/bash

nombre=0

echo -n "Entrez un nombre >"
read nombre

echo "Le nombre est $nombre"
if [$((nombre % 2)) -eq 0]; then
     echo "Le nombre est pair"
else
     echo "Le nombre est impair"
fi

Ou, dans ce programme qui formate un nombre quelconque de secondes en heures et minutes :


# !/bin/bash

secondes=0

echo -n "Entrez un nombre de secondes >"
read secondes

heures=$((secondes / 3600))
secondes=$((secondes % 3600))
minutes=$((secondes / 60))
secondes=$((secondes % 60))

echo "$heures heure(s) $minutes minute(s) $secondes seconde(s)"

Contrôle des flux - 2ème partie

Accrochez-vous à votre chapeau. Cette leçon va en être une grosse !

Plus de branchements

Dans la leçon précédente sur le contrôle du flux, nous avons appris à manier les if et comment les utiliser pour modifier le flux du programme à partir d'une condition. En termes de programmation, ce type de flux du programme est appelé branchement car c'est comme dans un arbre. Vous arrivez à une bifurcation dans un arbre et l'évaluation d'une condition détermine quelle branche doit être prise.

Il y a un second type plus complexe de branchement appelé case (NdT : on peut traduire par cas). C'est un branchement à choix multiple. Contrairement à la branche simple, où vous prenez un chemin parmi deux possibilités, case permet plusieurs résultats possibles à partir de l'évaluation d'une condition.

Vous pourriez construire ce type de branchement avec de multiples if. Dans l'exemple ci-dessous, nous évaluons une entrée de l'utilisateur :


# !/bin/bash

echo -n "Entrez un nombre compris entre 1 et 3 inclus >"
read nombre
if ["$nombre" = "1"]; then
     echo "Vous avez entré 1"
else
     if ["$nombre" = "2"]; then
          echo "Vous avez entré 2"
     else
          if ["$nombre" = "3"]; then
               echo "Vous avez entré 3"
          else
               echo "Vous n'avez pas entré de nombre"
               echo "entre 1 et 3"
          fi
     fi
fi

Pas très présentable.

Heureusement, le shell propose une solution plus élégante à ce problème. Il fournit une instruction appelée case, qui peut être utilisée pour construire un programme équivalent :


# !/bin/bash

echo -n "Entrez un nombre compris entre 1 et 3 compris >"
read nombre
case $nombre in
     1 ) echo "Vous avez entré 1"
          ;;
     2) echo "Vous avez entré 2"
          ;;

     3) echo "Vous avez entré 3"
          ;;
     *) echo "Vous n'avez pas entré de nombre"
        echo "entre 1 et 3"
esac

La commande case s'utilise de la façon suivante :

case mot in

     motifs ) instructions ;;

esac

L'instruction case exécute sélectivement les instructions qui correspondent au motif. Vous pouvez avoir une quantité quelconque de motifs et d'instructions. Les motifs peuvent contenir du texte ou des jokers (caractères de remplacement). Vous pouvez avoir de multiples motifs séparés par le caractère "|". Voici un exemple plus évolué pour montrer ce que je veux dire :


# !/bin/bash

echo -n "Entrez un nombre ou une lettre >"
read caractere
case $caractere in
          # Vérification pour une lettre
     [a-z] | [A-Z] ) echo "Vous avez entré la lettre $caractere"
          ;;

          # Vérification pour un nombre
     [0-9]  )        echo "Vous avez entré le nombre $caractere"
           ;;

          #Vérification pour tout autre chose
     * )             echo "Vous n'avez pas entré de nombre ni de lettre"
esac

Remarquez le motif spécial "*". Il correspond à n'importe quoi, aussi il est utilisé pour pour tous les cas qui n'ont pas eu de correspondance précédemment. Introduire ce motif à la fin est astucieux, puisqu'il peut détecter une entrée invalide.

Les boucles

Le dernier type de contrôle du flux du programme que nous allons étudier est appelé boucle. C'est l'exécution répétée et sous condition d'une partie du programme. Le shell fournit trois commandes pour les boucles : while, until et for. (NdT : tant que, jusqu'à ce que et pour). Nous allons considérer while et until dans cette leçon et for dans une leçon suivante.

La commande while permet l'exécution répétée d'un bloc de code, aussi longtemps qu'une condition est vraie. Voici l'exemple simple d'un programme qui compte de zéro à neuf :


# !/bin/bash

nombre=0
while [$nombre -lt 10] ; do
     echo "Nombre = $nombre"
     nombre=$((nombre + 1))
done

Ligne 3, nous créons une variable nombre et initialisons sa valeur à 0. Ensuite, nous commençons la boucle while. Comme vous le voyez, nous avons spécifié une condition qui teste la valeur de nombre. Dans notre exemple, nous testons pour voir si nombre a une valeur inférieure à 10.

Remarquez le mot do ligne 4 et le mot done ligne 7 (NdT : faire et fait). Ils entourent le bloc de code qui doit être répété aussi longtemps que la condition est remplie.

La plupart du temps, le bloc de code qui est répété doit faire quelque chose qui finalement change le résultat de la condition, sinon vous allez obtenir ce qu'on appelle une boucle sans fin ; c'est à dire, une boucle qui ne se termine jamais.

Dans l'exemple, le bloc de code qui est répété affiche la valeur de nombre (commande echo, ligne 5) et incrémente nombre de 1 en ligne 6. A chaque exécution du bloc de code, la condition est à nouveau testée. Après la dixième itération de la boucle, nombre a été incrémenté dix fois et la condition n'est plus vraie. A ce moment, le flux du programme reprend avec l'instruction qui suit le mot done. Puisque c'est la dernière ligne de notre exemple, le programme se termine.

La commande until fonctionne exactement de la même manière, sauf que le bloc de code est répété aussi longtemps que la condition est fausse. Dans l'exemple ci-dessous, remarquez comment la condition a été changée par rapport à l'exemple de while pour arriver au même résultat :


# !/bin/bash

nombre=0
until [$nombre -ge 10] ; do
     echo "Nombre = $nombre"
     nombre=$((nombre + 1))
done

Construire un menu

Une façon usuelle de présenter une interface utilisateur, pour un programme, est d'utiliser un menu. Un menu est une liste de propositions parmi lesquelles l'utilisateur peut faire un choix.

Dans l'exemple ci-dessous, nous utilisons notre nouvelle connaissance des boucles et des instructions case pour construire une application pilotée par un menu simple :


# !/bin/bash

selection=
until ["$selection" = "0"] ; do
     echo ""
     echo "MENU DU PROGRAMME"
     echo "1 - Affiche l'espace disque libre"
     echo "2 - Affiche la mémoire libre"
     echo ""
     echo "0 - Sortie du programme"
     echo ""
     echo -n "Entrez votre choix :"
     read selection
     echo ""
     case $selection in
          1 ) df ;;
          2 ) free ;;
          0 ) exit ;;
          * ) echo "Veuillez entrer 1, 2 ou 0"
     esac
done

L'intérêt de la boucle until dans ce programme et d'afficher à nouveau le menu après chaque exécution d'une sélection. La boucle continuera jusqu'à ce que la sélection soit égale à "0", le choix de sortie. Remarquez comment on se défend contre les entrées de l'utilisateur qui ne sont pas des choix valides.

Pour donner meilleure allure à ce programme pendant son exécution, on peut l'améliorer en ajoutant une fonction qui demande à l'utilisateur d'appuyer sur la touche entrée après chaque exécution d'une sélection, et efface l'écran avant d'afficher le menu à nouveau. Voici l'exemple amélioré :


# !/bin/bash

function presser_entree
{
     echo ""
     echo -n "Appuyer sur entrée pour continuer"
     read
     clear
}

selection=
until ["$selection" = "0"] ; do
     echo ""
     echo "MENU DU PROGRAMME"
     echo "1 - Affiche l'espace disque libre"
     echo "2 - Affiche la mémoire libre"
     echo ""
     echo "0 - Sortie du programme"
     echo ""
     echo -n "Entrez votre choix :"
     read selection
     echo ""
     case $selection in
          1 ) df ; presser_entree ;;
          2 ) free ; presser_entree ;;
          0 ) exit ;;
          * ) echo "Veuillez entrer 1, 2 ou 0"; presser_entree
     esac
done

Lorsque votre ordinateur se fige

Nous avons tous fait l'expérience d'une application qui se fige (ou dans le cas de systèmes propriétaires, c'est l'ordinateur complet). Le figement se produit lorsqu'un programme semble subitement s'arrêter et ne plus répondre. Bien que vous puissiez penser que le programme s'est arrêté, le plus souvent il est toujours en cours d'exécution mais il est embourbé dans une boucle sans fin.

Imaginez cette situation : vous avez un périphérique externe connecté à votre ordinateur, tel qu'un lecteur de disque USB, mais vous avez oubliè de le mettre sous tension. Vous essayez d'utiliser le périphérique, mais au lieu de cela l'application se fige. Quand cela arrive, vous pouvez vous imaginer le dialogue suivant se poursuivant entre l'application er l'interface de votre périphérique :

Application : Etes vous prêt ?

Interface : Périphérique non prêt.

Application : Etes vous prêt ?

Interface : Périphérique non prêt.

Application : Etes vous prêt ?

Interface : Périphérique non prêt.

Application : Etes vous prêt ?

Interface : Périphérique non prêt.

Et ainsi de suite pour toujours.

Un logiciel bien écrit évite cette situation en instituant un délai d'expiration. Cela signifie que la boucle compte le nombre de ses tentatives ou mesure la quantité de temps qu'elle a attendu avant que ne se produise quelque chose. Si le nombre de tentatives ou la quantité de temps attribué est dépassé, la boucle s'arrête, le programme génère une erreur et s'arrête.

Paramètres positionnels

Dans l'état où nous l'avions laissé, notre script ressemblait à quelque chose comme ceci :


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML d'information sur le système

##### Constantes

TITRE="Informations sur le système de $HOSTNAME"
MAINTENANT=$(date +"%x %r %Z")
HORODATAGE="Mis à jour le $MAINTENANT par $USER"

##### Fonctions

function info_systeme
{
     echo "< h2 >Informations de version sur le système</h2>"
     echo "<p>fonction pas encore implémentée</p>"

}    # fin de info_systeme

function montrer_fonctionnement
{
     echo "< h2 >fonctionnement du système</h2>"
     echo "<pre>"
     uptime
     echo "< /pre >"

}    # fin de montrer_fonctionnement

function espace_disque
{
     echo "< h2 >Espace disque des systèmes de fichiers</h2>"
     echo "<pre>"
     df
     echo "< /pre >"

}    # fin de espace_disque

function espace_home
{
     # Seul l'administrateur peut obtenir ces informations

     if ["$(id -u)" = "0"]; then
          echo "< h2 >Espace disque occupé par utilisateur</h2>"
          echo "<pre>"
          echo "octets répertoires"
          du -sk  /home/*|sort -nr
          echo "< /pre >"
     fi

}    # fin de espace_home

##### Partie principale

cat <<- _EOF_

    <html>
    <head>
        <title>$TITRE</title>
    </head>
    <body>
        < h1 >$TITRE</h1>
        <p>$HORODATAGE</p>
        $(info_systeme)
        $(montrer_fonctionnement)
        $(espace_disque)
        $(espace_home)
    </body>
    </html>
_EOF_

(NdT: Relecteur, ne pas omettre les blancs autour des balises <h1>, <h2> et </pre>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Tout cela fonctionne, en grande partie, mais je souhaite y ajouter des perfectionnements :

  1. Je veux spécifier le nom du fichier de sortie dans la ligne de commande, et aussi fixer un nom de fichier de sortie par défaut si aucun nom n'est spécifié.
  2. Je veux offrir un mode interactif qui demandera un nom de fichier et avertira l'utilisateur si ce fichier existe, auquel cas il invitera l'utilisateur à écrire par dessus.
  3. Naturellement, nous désirons avoir une possibilité d'aide qui affichera un message d'utilisation.

Toutes ces caractéristiques impliquent l'utilisation des options et arguments en ligne de commande. Pour manipuler les options en ligne de commande, nous utilisons une facilité du shell appelée paramètres positionnels. Ce sont une série de variables spéciales (de $0 à $9) qui contiennent les éléments d'une ligne de commande. Imaginons la ligne de commande suivante :

[moi@linuxbox moi]$ un_programme mot1 mot2 mot3

Si un_programme était un script en shell bash, le programme pourrait lire chaque élément sur la ligne de commande car les paramètres positionnels contiennent ce qui suit :

  • $0 contiendrait "un_programme"
  • $1 contiendrai "mot1"
  • $2 contiendrai "mot2"
  • $3 contiendrai "mot3"

Voici un script que vous pouvez utiliser pour faire un essai :


# !/bin/bash

echo "Paramètres positionnels"
echo '$0 = ' $0
echo '$1 = ' $1
echo '$2 = ' $2
echo '$3 = ' $3

Détection des arguments en ligne de commande

Souvent vous chercherez à vérifier si vous avez des arguments sur lesquels agir. Il existe deux façons pour y arriver. Premièrement, vous pouvez simplement vérifier si $1 contient quelque chose, comme cela :


# !/bin/bash

if ["$1" != ""]; then
     echo "Le paramètre positionnel 1 contient quelque chose"
else
     echo "Le paramètre positionnel 1 est vide"
fi

Deuxièmement, le shell conserve une variable appelée $# qui contient le nombre d'éléments sur la ligne de commande en plus du nom de la commande ($0).


# !/bin/bash

if [$# -gt 0]; then
     echo "Votre ligne de commande contient $# arguments"
else
     echo "Votre ligne de commande ne contient pas d'arguments"
fi

Options en ligne de commande

Comme nous en discutions auparavant, beaucoup de programmes, particulièrement ceux du Projet GNU, supportent à la fois les modes courts et longs pour les options en ligne de commande. Par exemple, pour afficher un message d'aide pour beaucoup de ces programmes, vous pouvez utiliser soit l'option "-h" soit l'option longue "--help". Les noms d'option longs sont typiquement précédés d'un double tiret. Nous adopterons cette convention pour nos scripts.

Voici le code que nous utiliserons pour déchiffrer la ligne de commande (NdT : Celle que l'utilisateur a tapé pour lancer notre script) :

interactif=
nom_fichier=~/page_systeme.html

while ["$1" != ""]; do
     case $1 in
          -f | --file )        shift
                               nom_fichier=$1
                               ;;
          -i | --interactive ) interactif=1
                               ;;
          -h | --help          usage
                               exit
                               ;;
          * )                  usage
                               exit 1
     esac
     shift
done

Ce code est un peu délicat, il va donc falloir que vous vous armiez de patience pendant que je tente de vous l'expliquer !

Les deux premières lignes sont assez faciles. Nous positionnons la variable interactif sur une valeur... vide. Cela indiquera que le mode interactif n'a pas été requis. Puis, nous positionnons la variable nom_fichier de telle sorte qu'elle contienne par défaut un certain nom de fichier. Si rien d'autre que le nom du script n'est spécifié sur la ligne de commande, ce nom par défaut sera utilisé. Avec ces deux variables, les paramètres par défaut sont en place, pour traiter le cas où l'utilisateur ne spécifie aucune option.

Ensuite, nous construisons une boucle while qui balayera par cycles successifs tous les éléments de la ligne de commande et traitera chacun d'eux avec l'instruction case. case détectera chaque option possible et agira en conséquence. Maintenant, nous arrivons à la partie la plus délicate. Comment fonctionne cette boucle ? Elle est basée sur la magie de shift (NdT shift = décaler).

shift est une commande interne du shell (built-in) qui opère sur les paramètres positionnels. Chaque fois que vous invoquez la commande shift, elle "décale" (diminue) de un la position de chacun des paramètres positionnels : $2 devient $1, $3 devient $2, $4 devient $3, etc. Et l'ancien $1 ? eh bien l'ancien $1 disparaît, purement et simplement, sa valeur est perdue ! Essayez cela :


# !/bin/bash

echo "Vous démarrez avec $# paramètres positionnels"

# Boucle jusqu'à ce que tous les paramètres positionnels soient utilisés"

while ["$1" != ""]; do
     echo "Paramètre 1 vaut $1"

     # décaler chaque paramètre d'une position
     shift

     # afficher le nouveau nombre de paramètres
     echo "Vous avez maintenant $# paramètres positionnels"
done

Obtenir l'argument d'une option

Notre option -f exige un argument, un nom de fichier valide. Nous utilisons à nouveau shift pour obtenir l'élément suivant de la ligne de commande et l'assignons à nom_fichier. Plus tard, nous devrons vérifier le contenu de nom_fichier pour s'assurer qu'il est valide.

Intégration du déchiffrage de la ligne de commande dans le script

Nous avons à déplacer certaines choses et à ajouter une fonction usage pour insérer cette nouvelle routine dans notre script. Nous allons aussi ajouter du code de test pour vérifier que le déchiffrage de la ligne de commande fonctionne correctement. Notre script ressemble maintenant à cela :


# !/bin/bash

# edition_page - Un script pour produire un fichier HTML d'information sur le système

##### Constantes

TITRE="Informations sur le système de $HOSTNAME"
MAINTENANT=$(date +"%x %r %Z")
HORODATAGE="Mis à jour le $MAINTENANT par $USER"

##### Fonctions

function info_systeme
{
     echo "< h2 >Informations de version sur le système</h2>"
     echo "<p>fonction pas encore implémentée</p>"

}    # fin de info_systeme

function montrer_fonctionnement
{
     echo "< h2 >fonctionnement du système</h2>"
     echo "<pre>"
     uptime
     echo "< /pre >"

}    # fin de montrer_fonctionnement

function espace_disque
{
     echo "< h2 >Espace disque des systèmes de fichiers</h2>"
     echo "<pre>"
     df
     echo "< /pre >"

}    # fin de espace_disque

function espace_home
{
     # Seul l'administrateur peut obtenir ces informations

     if ["$(id -u)" = "0"]; then
          echo "< h2 >Espace disque occupé par utilisateur</h2>"
          echo "<pre>"
          echo "octets répertoires"
          du -sk  /home/*|sort -nr
          echo "< /pre >"
     fi

}    # fin de espace_home

function ecriture_page
{
     cat <<- _EOF_
         <html>
         <head>
         <title>$TITRE</title>
         </head>
         <body>
        < h1 >$TITRE</h1>
        <p>$HORODATAGE</p>
        $(info_systeme)
        $(montrer_fonctionnement)
        $(espace_disque)
        $(espace_home)
         </body>
    </html>
_EOF_

}

function usage
{
     echo "usage: edition_page [[[-f file] [-i]] | [-h]]"
}

##### Partie principale

interactif=
nom_fichier=~/page_systeme.html

while ["$1" != ""]; do
     case $1 in
          -f | --file )        shift
                               nom_fichier=$1
                               ;;
          -i | --interactive ) interactif=1
                               ;;
          -h | --help )        usage
                               exit
                               ;;
          * )                  usage
                               exit 1
     esac
     shift
done

# Code de test pour vérifier le déchiffrage de la ligne de commande

if ["$interactif" = "1"]; then
     echo "mode interactif"
else
     echo "mode non interactif"
fi
echo "fichier de sortie = $nom_fichier"

# Ecriture page (laisser en commentaire jusqu'à la fin des tests)

# ecriture_page > $nom_fichier

(NdT: Relecteur, ne pas omettre les blancs autour des balises <h1>, <h2> et </pre>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Ajout du mode interactif

Le mode interactif est implémenté avec le code suivant:

if ["$interactif" = "1"]; then

     reponse=

     echo -n "Entrez le nom du fichier de sortie [$nom_fichier] > "
     read reponse
     if [-n "$reponse"]; then
          nom_fichier=$reponse
     fi

     if [-f "$reponse"]; then
          echo -n "Le fichier de sortie existe. L'écraser ? (o/n) > "
          read reponse
          if ["$reponse" != "o"]; then
               echo "Sortie du programme."
               exit 1
          fi
     fi
fi

Premièrement, nous vérifions si le mode interactif est activé, sinon, nous n'avons rien à faire. Ensuite, nous demandons à l'utilisateur de nous donner un nom de fichier. Remarquez la syntaxe de la demande :

echo -n "Entrez le nom du fichier de sortie [$nom_fichier] > "

Nous affichons la valeur courante de nom_fichier puisque, de la façon dont la routine est codée, si l'utilisateur appuie juste sur la touche Entrée, la valeur par défaut de nom_fichier sera utilisée. Cela est accompli dans les lignes suivantes où la valeur de réponse est vérifiée (dans ce cas ni le test [-n "$reponse"], ni le test [-f "$reponse"], ne sera satisfait étant donné qu'aucune chaîne de caractères n'a été placée par la commande read dans la variable reponse). Si reponse n'est pas vide, alors nom_fichier reçoit la valeur de reponse. Autrement, nom_fichier est laissé inchangé, conservant sa valeur par défaut.

Après avoir reçu le nom du fichier de sortie, nous vérifions s'il existe déjà à l'aide du test [-f "$reponse"]. Si oui, nous interrogeons l'utilisateur. Si sa réponse n'est pas "o", nous abandonnons et quittons le programme, sinon nous pouvons continuer.

Contrôle des flux - 3ème partie : Les boucles for

Maintenant que vous avez appris les paramètres positionnels, il est temps de voir l'instruction qui reste : l'instruction for.

Comme while et until, for est utilisé pour construire des boucles. for fonctionne comme ceci :

for variable in mots; do
     instructions
done

(NdT : for = pour ; in = dans ; do = faire ; done = fait)

Pour l'essentiel, for assigne à la variable spécifiée un des mots de la liste, exécute les instructions, et répète cela en boucle jusqu'à ce que tous les mots aient été utilisés. Voici un exemple :


# !/bin/bash

for i in mot1 mot2 mot3; do
     echo $i
done

Dans cet exemple, à la variable i est assignée la chaîne de caractères mot1, puis l'instruction echo $i est exécutée, ensuite à la variable i est assignée la chaîne de caractères mot2, et la commande echo $i est exécutée à nouveau avec la nouvelle valeur de la variable i, et ainsi de suite, jusqu'à ce que tous les mots de la liste de mots aient été traités.

Une particularité intéressante de for est qu'elle autorise de nombreuses façons de construire la liste des mots. Toute sorte de substitutions peuvent être utilisées. Dans l'exemple suivant, nous construisons la liste des mots par l'entremise d'une commande :


# !/bin/bash

compte=0
for i in $(cat ~/.bash_profile); do
     compte=$((compte + 1))
     echo " Le mot $compte ($i) contient $(echo -n $i | wc -c) caractères"
done

Ici nous prenons le fichier .bash_profile et nous comptons le nombre des mots de ce fichier et le nombre des caractères pour chaque mot.

Mais qu'est-ce que for a à voir avec les paramètres positionnels ? Eh bien, une des caractéristiques de for est qu'il est possible d'utiliser les paramètres positionnels comme liste de mots :


# !/bin/bash

for i in "$@"; do
     echo $i
done

La variable du shell $@ contient la liste des arguments de la ligne de commande. Cette technique est une approche très répandue pour exploiter une liste de fichiers donnée en ligne de commande. En voici un exemple :


# !/bin/bash

for nom_fichier in "$@"; do
     resultat=
     if [-f "$nom_fichier"]; then
          resultat="$nom_fichier est un fichier ordinaire"
     else
          if [-d "$nom_fichier"]; then
               resultat="$nom_fichier est un répertoire"
          fi
     fi
     if [-w "$nom_fichier"]; then
          resultat="$resultat et il est accessible en écriture"
     else
          resultat="$resultat et il n'est pas accessible en écriture"
     fi
     echo "$resultat"
done

Essayez ce script. Donnez-lui comme arguments une liste de fichiers ou le caractère de remplacement * pour le voir marcher.

Voici un autre exemple de script. Celui-là compare les fichiers présents à la racine de deux répertoires. Il affiche la liste des fichiers présents à la racine du premier répertoire qui sont absents à la racine du deuxième.


# !/bin/bash

# cmp_dir - programme pour comparer deux répertoires

# Verifier la présence des arguments requis

if [$# -ne 2]; then
     echo "Syntaxe : $0 répertoire_1 répertoire_2" 1>&2
     exit 1
fi

# S'assurer que les deux arguments sont des répertoires

if [! -d $1]; then
     echo "$1 n'est pas un répertoire" 1>&2
     exit 1
fi

if [! -d $2]; then
     echo "$2 n'est pas un répertoire" 1>&2
     exit 1
fi

# Traiter chaque fichier de répertoire_1, comparer à répertoire_2

manquant=0
for nom_fichier in $1/*; do
     fn=$(basename "$nom_fichier")
     if [-f "$nom_fichier"]; then
          if [! -f "$2/$fn"]; then
               echo "$fn est manquant dans $2"
               manquant=$((manquant + 1))
          fi
     fi
done
echo "$manquant fichiers manquants"

Maintenant, revenons à notre travail en cours. Nous allons améliorer la fonction espace_home dans notre script pour extraire plus d'informations. Vous vous souvenez que notre précédente version ressemblait à :

function espace_home
{
     # Seul l'administrateur peut obtenir ces informations

     if ["$(id -u)" = "0"]; then
          echo "< h2 >Espace disque occupé par utilisateur</h2>"
          echo "<pre>"
          echo "octets répertoires"
          du -sk  /home/*|sort -nr
          echo "< /pre >"
     fi

}    # fin de espace_home
Attention, en ce qui concerne la commande du, quelque chose peut vous troubler.

Sous Mandriva, un alias a pour effet que la commande est par défaut exécutée avec l'option -h qui utilise à l'affichage des unités de taille différentes selon l'importance du fichier (la taille est donnée tantôt en kilo-octets, tantôt en méga-octets etc.).

Notez que l'option k de du, que nous avons rajoutée, neutralise en quelque sorte cet alias et assure alors que toutes les tailles de fichiers seront affichées dans la même unité : le kilo-octet, ce qui est nécessaire pour donner un sens au classement par taille imposé ensuite par sort : faute de cela, les chiffres seraient tantôt donnés dans telle unité, tantôt dans telle autre, selon l'importance du fichier, et il n'y aurait pas grand sens à effectuer un classement numérique dans ces conditions....

Une autre solution consisterait à désactiver temporairement l'alias de du avant d'exécuter le script, par un unalias du. du donnerait alors toutes les tailles en blocs de 510 octets.

Voici la nouvelle version :

function espace_home
{
     echo "< h2 >Espace disque occupé par utilisateur</h2>"
     echo "<pre>"
     format="%8s%10s%10s   %-s\n"
     printf "$format" "Reps" "Fichiers" "Taille" "Repertoire"
     printf "$format" "----" "--------" "-----" "----------"
     if [$(id -u) = "0"]; then
          dir_list="/home/*"
     else
          dir_list=$HOME
     fi
     for home_dir in $dir_list; do
          total_dirs=$(find $home_dir -type d | wc -l)
          total_files=$(find $home_dir -type f | wc -l)
          total_size=$(du -s $home_dir)
          printf "$format" $total_dirs $total_files $total_size
     done
     echo "< /pre >"
}    # fin de espace_home

(NdT: Relecteur, ne pas omettre les blancs autour des balises <h2> et </pre>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Maintenant les fichiers ne sont plus classés par taille (sort a disparu). Du coup, nous avons opté pour une utilisation de du sans l'option k. L'alias de Mandriva mentionné dans l'encadré précédent garde alors toute sa valeur et les tailles sont affichées dans des unités diverses, de façon à être aisément lisibles par l'utilisateur humain.

Cette version améliorée introduit un nouvelle commande printf, qui est utilisée pour créer une sortie formatée conformément au contenu d'une chaîne de format. printf provient du langage de programmation C et a été implémenté dans beaucoup d'autres langages, notamment C++, perl, awk, java, PHP, et bien sûr Bash. Vous pouvez en apprendre plus sur les chaînes de caractères de formatage de printf à :

Nous avons aussi introduit la commande find. Elle est utilisée pour rechercher des fichiers ou des répertoires qui répondent à un ou plusieurs critère(s) spécifique(s). Dans la fonction espace_home, nous utilisons la fonction find pour lister les répertoires et les fichiers ordinaires dans chaque répertoire personnel. Avec la commande wc, nous comptons le nombre de fichiers et de répertoires trouvés (cette commande, employée avec l'option -l, compte en fait le nombre de lignes qui figurent à la sortie de find). La chose vraiment intéressante dans espace_home est la manière de gérer le problème de l'accès de l'administrateur. Vous remarquerez que nous testons une session administrateur avec id et, suivant la réponse du test, nous assignons telle ou telle chaîne de caractères à la variable dir_list, qui devient ensuite la liste des mots pour la boucle for qui suit. De cette façon, si un utilisateur ordinaire exécute le script, seul son répertoire personnel (toujours abrité dans la variable d'environnement HOME) sera listé.

Une autre fonction qui peut utiliser une boucle for est la fonction info_systeme, que nous n'avons pas encore implémentée. Nous pouvons la construire comme cela :

function info_systeme
{
     # Trouver tout fichier d'information de version dans /etc
     if ls /etc/*release 1>/dev/null 2>&1; then
        echo "< h2 >Informations de version sur le système</h2>"
        echo "<pre>"
        for i in /etc/*release; do

        #Puisque nous ne pouvons être sûrs de la longueur du fichier,
        #n'afficher que la première ligne.

             head -n 1 $i
        done
        uname -orp
        echo "< /pre >"
     fi
}    # fin de info_systeme

(NdT: Relecteur, ne pas omettre les blancs autour des balises <h2> et </pre>, ils sont nécessaires ici pour ne pas être interprétés par la commande de présentation. Utilisateur, penser à enlever ces blancs en cas de recopie du code)

Dans cette fonction, nous déterminons d'abord s'il existe des fichiers d'information sur la version distribuée qui seraient disponibles pour un traitement. Ces fichiers contiennent le nom du distributeur (vendor) et la version de la distribution. Ils sont situés dans le répertoire /etc. Pour les détecter, nous lançons une commande ls et rejetons tout ce qui en sort vers le « trou noir » du système Linux, le célèbre périphérique spécial /dev/null. La sortie standard 1 est ainsi redirigée vers /dev/null (1>/dev/null) et la sortie des erreurs 2 est redirigée vers la sortie standard 1 (2>&1), de sorte qu'elle aussi se retrouve absorbée par le trou noir... Nous ne sommes en effet intéressés que par le code de retour de ls. Il sera vrai si au moins un fichier est trouvé.

Ensuite, nous reprenons les codes HTML pour cette section de la page, puisque nous savons maintenant qu'il y a des fichiers d'information sur la version à traiter. Pour cela, nous commençons une boucle for pour traiter chacun d'eux. A l'intérieur de la boucle, nous utilisons la commande head pour retourner la première ligne de chaque fichier.

Enfin, nous utilisons la commande uname avec les options o, r, et p pour obtenir des informations supplémentaires sur le système.


Exécuter le contenu d'un script dans un autre avec la commande source

La commande source permet de faire exécuter les lignes de code d'un script dans un autre. Cette commande peut être écrite en toutes lettres : source mais elle est le plus souvent représentée par un simple point.

Il en est fait abondamment usage dans les fichiers de configuration. Par exemple dans les fichiers de configuration du shell /etc/profile et /etc/bashrc, vous trouverez cette boucle, qui a pour effet de lire et exécuter toutes les lignes de tous les fichiers *.sh du répertoire /etc/profile.d :

for i in /etc/profile.d/*.sh ; do
        if [ -r $i ]; then
                . $i
        fi
done


Erreurs et signaux et pièges (Oh mon Dieu !) - 1ère partie

Dans cette leçon, nous allons apprendre à traiter les erreurs pendant l'exécution de vos scripts.

La différence entre un bon programme et un médiocre est souvent mesuré en termes de robustesse du programme. C'est à dire la capacité du programme à gérer les situations dans lesquelles quelque chose ne va pas.

Codes de sortie

Comme vous vous le rappelez, pour l'avoir lu dans nos précédentes leçons, tout programme correctement écrit retourne un code de sortie bien déterminé quand il s'arrête. Si un programme se termine avec succès, le code de sortie sera zéro. Si le code de sortie est autre chose que zéro, alors le programme à échoué d'une manière ou d'une autre.

Il est très important de vérifier les codes de sortie des programmes que vous appelez dans vos scripts. Il est important aussi que vos scripts retournent un code de sortie bien déterminé quand ils se terminent. J'ai vu une fois un administrateur de système Unix qui avait écrit un script pour un système de production contenant les deux lignes de code suivantes :


# Exemple d'une idée vraiment mauvaise

cd   $un_repertoire
rm  *

Pourquoi est-ce une si mauvaise idée de faire cela ? Ce ne l'est pas si rien ne va mal. Les deux lignes changent le répertoire de travail pour le nom contenu dans $un_repertoire et effacent les fichiers de ce répertoire. C'est le comportement attendu. Mais qu'arrive t-il si le répertoire cité dans $un_repertoire n'existe pas ? Dans ce cas, la commande cd échouera et le script exécutera la commande {{cmd|rm} dans le répertoire de travail courant !! Ce n'est pas du tout le comportement attendu !

Et justement, ce script d'administrateur de système malchanceux a rencontré exactement cette défaillance et a détruit une grande partie d'un important système de production. Ne laissez pas cela vous arriver !

Le problème avec ce script était qu'il ne vérifiait pas le code de sortie de la commande cd avant d'exécuter la commande rm.

Vérifier les codes de sortie

Il existe plusieurs moyens pour obtenir et traiter correctement le code de sortie d'un programme. Premièrement, vous pouvez examiner le contenu de la variable d'environnement $?. Elle contient le code de sortie de la dernière commande exécutée. Vous pouvez voir cela fonctionner dans ce qui suit :

[moi]$ true; echo $?
0
[moi]$ false; echo $?
1

(NdT : true = vrai, false = faux)

Les commandes true et false sont des programmes qui ne font rien d'autre que retourner un code de sortie respectivement de zéro et de un. Elles illustrent donc très clairement le fait que la variable d'environnement $? contient le code de sortie du programme ou de la commande qui vient de s'achever.

Ainsi, pour vérifier le code de sortie de cd, nous pourrions écrire le script de cette manière :


# vérifier le code de sortie

cd $un_repertoire
if ["$?" = "0"]; then
   rm  *
else
   echo "Impossible de changer de répertoire !" 1>&2
   exit 1
fi

Dans cette version, nous examinons le code de sortie de la commande cd et si ce n'est pas zéro, nous affichons un message d'erreur sur la sortie standard des erreurs et terminons le script avec un code de sortie de 1.

Bien que cette solution fonctionne, il y a des méthodes plus subtiles qui nous économisent de la frappe. L'approche suivante que nous pouvons essayer utilise directement l'instruction if, puisqu'elle évalue le code de sortie des commandes qui lui sont données.

En utilisant if, nous pourrions écrire ceci :


# Une meilleure façon de vérifier le code de sortie
# de cd

if cd $un_repertoire; then
 rm  *
else
 echo "Impossible de changer de répertoire ! Abandon." 1>&2
 exit 1
fi

Ici nous vérifions si la commande cd est un succès et c'est seulement dans ce cas que rm est exécutée ; autrement un message d'erreur est retourné et le programme s'arrête avec un code de 1, indiquant qu'une erreur s'est produite.

Une fonction de sortie en cas d'erreur

Etant donné que nous vérifierons souvent les erreurs dans nos programmes, il paraît judicieux d'écrire une fonction qui affichera les messages d'erreur. Cela économisera de la frappe et encouragera le farniente.


# Une fonction de sortie en cas d'erreur

function sortie_erreur
{
     echo "$1" 1>&2
     exit 1
}

# Utiliser sortie_erreur

if cd $un_repertoire; then
 rm  *
else
 sortie_erreur "Impossible de changer de répertoire ! Abandon."
fi

Les opérateurs AND (&&) et OR (||)

(NdT : AND = ET ; OR = OU)

Finalement, nous pouvons simplifier encore plus le script en utilisant les opérateurs de contrôle AND et OR. Vous trouverez un exemple simple d'utilisation de AND ici : Le shell sans peine#Le &&.

Pour expliquer le fonctionnement de ces deux opérateurs, je citerai aussi la page man de Bash :


Les opérateurs de contrôle && et || représentent respectivement des listes AND et des listes OR. Une liste AND est de la forme :

commande1 && commande2

commande2 est exécutée si et seulement si, commande1 retourne le code d'erreur zéro.

Une liste OR est de la forme :

commande1 || commande2

commande2 est exécutée si et seulement si, commande1 retourne un code d'erreur différent de zéro.

Le code de retour des listes AND et OR est le code de sortie de la dernière commande exécutée de la liste.


A nouveau, nous pouvons utiliser les commandes true et false pour voir cela fonctionner :

[moi]$ true || echo "echo exécuté"
[moi]$ false || echo "echo exécuté"
echo exécuté
[moi]$ true && echo "echo exécuté"
echo exécuté
[moi]$ false && echo "echo exécuté"
[moi]$

En utilisant cette technique, nous pouvons écrire une version encore plus simple :


# La plus simple de toutes

cd $un_repertoire || error_exit "Impossible de changer de répertoire ! Abandon."
rm  *

Si l'arrêt du programme n'est pas requis en cas d'erreur, alors vous pouvez même faire cela :


# Et si l'arrêt n'est pas nécessaire...

cd $un_repertoire && rm  *

Amélioration de la fonction de sortie des erreurs

Nous pouvons apporter encore beaucoup d'améliorations à la fonction error-exit.

J'aime inclure le nom du programme dans le message d'erreur pour indiquer clairement d'où vient l'erreur. Ceci devient plus important au fur et à mesure que vos programmes deviennent plus complexes et que vous commencez à avoir des scripts qui lancent d'autres scripts, etc.

Remarquez aussi la présence de la variable d'environnement LINENO qui vous aidera à identifier la ligne exacte de votre script responsable de l'erreur.


# !/bin/bash

# Une routine de gestion des erreurs

# Je mets dans mes scripts une variable nommée
# PROGNAME qui contient le nom du programme
# en cours d'exécution.
# Cette valeur peut être extraite du premier élément
# de la ligne de commande ($0)

PROGNAME=$(basename $0)

function error_exit
{

# ---------------------------------------------------------------
# Fonction pour une sortie due à une erreur fatale
# du programme.
# Accepte un argument :
# chaîne de caractères
# contenant le message d'erreur descriptif
# ---------------------------------------------------------------

   echo "${PROGNAME}: ${1:-"Erreur inconnue"}" 1>&2
   exit 1
}

# L'exemple appelle la fonction error_exit.
# Remarquez l'introduction de la variable
# d'environnement LINENO.
# Elle contient le numéro de ligne en cours.

echo "Exemple d'erreur avec le numéro de la ligne et le message"
error_exit "$LINENO: Une erreur s'est produite."

(NdT : LINENO pour LINE NUMBER = NUMERO DE LIGNE ; PROGNAME pour PROGRAM NAME = NOM DE PROGRAMME)

Erreurs et signaux et pièges (Oh mon Dieu !) - 2ème partie

Les erreurs ne constituent pas la seule façon pour un script de se terminer de façon inattendue. On doit aussi penser aux signaux, dont certains peuvent aussi arrêter un programme. Considérons, par exemple, le programme suivant :


# ! /bin/bash

echo "Ce script bouclera sans fin jusqu'à ce que vous l'arrêtiez"
while true; do
     : # Ne fait rien
done

Après avoir lancé ce script il semble se figer. En fait, comme la plupart des programmes qui semblent plantés, il est en réalité coincé dans une boucle. Dans le cas ci-dessus, il attend que la commande true retourne un statut de sortie différent de zéro, ce qui n'arrivera jamais. Une fois démarré, le script continuera jusqu'à ce que Bash reçoive un signal qui l'arrêtera. Notez dans le corps de la boucle la commande :, une commande qui ne fait rien et dont l'état de sortie est « vrai » (0)... Vous pouvez envoyer au script un signal d'arrêt en tapant Ctrl-c qui émet le signal appelé SIGINT (abréviation pour SIGnal INTerrupt).

Nettoyez derrière vous

OK, ainsi un signal peut se présenter et provoquer l'arrêt du script. Qu'est-ce que cela peut faire ? Eh bien, dans de nombreux cas cela ne fait rien et vous pouvez ignorer les signaux, mais quelquefois on ne le peut pas.

Jetons un coup d'œil à un autre script. Ce script s'appellera printfile ; il servira à imprimer un fichier texte en y ajoutant des en-tête :


# !/bin/bash

# Programme printfile
# Pour imprimer un texte avec en-têtes

TEMP_FILE=/tmp/printfile.txt

pr $1 > $TEMP_FILE

echo -n "Imprimer fichier ? [o/n] : "
read
if ["$REPLY" = "o"]; then
     lpr $TEMP_FILE
fi

Ce script traite à l'aide de la commande pr un fichier texte spécifié sur la ligne de commande et stocke le résultat dans un fichier temporaire. Ensuite, il demande à l'utilisateur s'il veut imprimer le fichier. Si l'utilisateur tape o, alors le fichier temporaire est transmis au programme lpr pour impression (vous pouvez remplacer lpr par less si vous n'avez pas d'imprimante connectée à votre système).

Pour bien comprendre ce que fait, ou peut faire, la commande pr, essayez-la sur un fichier texte et jetez aussi un coup d'œil à cette page  : [1]. Ici elle ajoute en-tête et pied de page au fichier qui lui est donné comme argument.

Maintenant, j'admets que ce script a beaucoup de défauts de conception. Alors qu'il nécessite un nom de fichier passé sur la ligne de commande, il ne vérifie pas qu'il y en a un, et il ne vérifie pas que le fichier existe. Mais le problème sur lequel je veux attirer votre attention ici est le fait que lorsque le script se termine, il laisse derrière lui un fichier temporaire.

Une bonne pratique voudrait que nous effacions le fichier temporaire $TEMP_FILE quand le script est terminé. Ceci peut se faire facilement en ajoutant ce qui suit à la fin du script :

rm  $TEMP_FILE

Ceci semblerait résoudre le problème, mais qu'arrive-t-il si l'utilisateur tape Ctrl-c quand l'invite affiche Imprimer fichier ? [o/n] : . Le script se terminera au niveau de la commande read et la commande rm ne sera jamais exécutée. En clair, nous avons besoin d'une méthode pour répondre aux signaux tels que SIGINT quand les touches Ctrl-c sont tapées.

Heureusement, Bash fournit une méthode pour exécuter des commandes quand des signaux sont reçus.

trap

La commande trap vous permet d'exécuter une commande quand un signal est reçu par votre script. Elle fonctionne ainsi :

trap arg signaux

signaux est la liste des signaux à intercepter et arg est une commande à exécuter quand un des signaux est reçu. Pour notre script d'impression printfile, on pourrait gérer le problème du signal comme ceci :


# !/bin/bash

# Programme printfile
# Pour imprimer un texte avec en-têtes

TEMP_FILE=/tmp/printfile.txt

trap  "rm $TEMP_FILE; exit"  SIGHUP SIGINT SIGTERM

pr $1 > $TEMP_FILE

echo -n "Imprimer fichier ? [o/n] : "
read
if ["$REPLY" = "o"]; then
     lpr $TEMP_FILE
fi
rm $TEMP_FILE

Ici nous avons ajouté une commande trap qui exécutera rm $TEMP_FILE si l'un des signaux énumérés est reçu. Les trois signaux énumérés sont les plus fréquents, mais il en existe beaucoup d'autres qui peuvent tous être spécifiés. Pour avoir une liste complète des signaux, tapez trap -l. Vous pouvez lister les signaux par leur nom, comme nous l'avons fait, mais vous pouvez aussi les spécifier par leur numéro : dans la sortie de trap -l le nom de chaque signal est précédé par son numéro.

Signal 9 depuis l'espace intersidéral

Il existe un signal que vous ne pouvez pas intercepter avec trap : SIGKILL (le signal 9). Le noyau termine immédiatement tout processus concerné et aucun traitement du signal n'est exécuté. Puisqu'il arrêtera toujours un programme qui est enlisé, figé, ou cafouillant, il est tentant de penser que ce signal est une façon aisée de s'en sortir quand on veut arrêter quelque chose et partir ailleurs. Souvent vous verrez des références à la commande suivante qui envoie le signal SIGKILL :
kill -9 ...

Cependant, malgré sa facilité apparente, vous devez vous rappeler que quand vous envoyez ce signal, aucune exécution de tâche n'est faite par l'application. Souvent c'est ce qui convient, mais avec beaucoup de programmes, ça ne l'est pas. En particulier, beaucoup de programmes complexes (et quelques-uns pas si complexes) créent des fichiers de verrouillage pour empêcher que de multiples copies du programme s'exécute en même temps. Quand un programme qui utilise un fichier de verrouillage reçoit un SIGKILL, il n'a pas la possibilité d'enlever le fichier de verrouillage quand il s'arrête. La présence du fichier de verrouillage empêchera le programme de redémarrer jusqu'à ce qu'il soit enlevé à la main.

Soyez prévenu. Utilisez SIGKILL en dernier recours. Commencez plutôt par lancer le signal SIGTERM (signal 15), qui donne aux applications l'occasion de s'arrêter proprement.

Une fonction pour tout nettoyer

Bien que la commande trap ait résolu le problème, nous voyons qu'elle a des limites. Plus grave, elle n'accepte qu'une seule chaîne de caractères contenant la commande à exécuter quand le signal est reçu. Vous pourriez ruser, utiliser l'opérateur point-vigule (;) et mettre plusieurs commandes dans la chaîne de caractères pour obtenir un comportement plus sophistiqué, mais franchement, ce serait laid. Une meilleure idée serait de créer une fonction qui serait appelée quand vous le souhaitez pour réaliser certaines actions lorsque votre script s'arrête. Dans mes scripts, j'appelle cette fonction nettoyage.


# ! /bin/bash

# Programme printfile
# Pour imprimer un texte
# avec des en-têtes

TEMP_FILE=/tmp/printfile.txt

function nettoyage {
     # Exécute le nettoyage de sortie de programme
     rm   $TEMP_FILE
     exit
}

trap   nettoyage   SIGHUP SIGINT SIGTERM

pr  $1  >  $TEMP_FILE

echo -n "Imprimer fichier ? [o/n] : "
read
if ["$REPLY" = "o"]; then
     lpr $TEMP_FILE
fi
nettoyage

L'utilisation d'une fonction de nettoyage est aussi une bonne idée pour vos routines de traitement des erreurs. Après tout, quand votre programme se termine (quelle qu'en soit la raison), vous devriez nettoyer derrière vous. Voici une version finalisée de notre programme avec un traitement amélioré des erreurs et des signaux.


# ! /bin/bash

# Programme printfile
# Pour imprimer un texte
# avec des en-têtes

# usage:  printfile  fichier

# Créer un nom de fichier temporaire
# qui donne la préférence au répertoire
# tmp local de l'utilisateur et possède un nom sûr
# qui puisse déjouer d'éventuelles attaques malveillantes

if [-d "~/tmp"]; then
     TEMP_DIR=~/tmp
else
     TEMP_DIR=/tmp
fi
TEMP_FILE=$TEMP_DIR/printfile.$$.$RANDOM
PROGNAME=$(basename $0)

function usage {

  # Affiche un message d'usage
  # sur la sortie standard des erreurs
  echo "usage : $PROGNAME fichier" 1>&2
}

function nettoyage {

  # Exécute le nettoyage de sortie de programme
  # L'option -f garantit qu'aucun message
  # d'erreur ne sera émis si $TEMP_FILE
  # n'existe pas
  # Accepte en option un code de sortie ($1)
  rm  -f   $TEMP_FILE
  exit  $1
}

function error_exit {

  # Affiche le message d'erreur et quitte
  echo "${PROGNAME} : ${1:-"Erreur inconnue"}" 1>&2
  nettoyage 1
}

trap   nettoyage   SIGHUP SIGINT SIGTERM

if [$# != "1"]; then
     usage
     error_exit "Spécifier un fichier à imprimer"
fi
if [! -f "$1"]; then
     error_exit "Le fichier $1 ne peut être lu"
fi

pr $1 > $TEMP_FILE || error_exit "Impossible de formater le fichier"

echo -n "Imprimer fichier ? [o/n] : "
read
if ["$REPLY" = "o"]; then
     lpr $TEMP_FILE || error_exit "Impossible d'imprimer le fichier"
fi
nettoyage

La nouvelle version de la fonction error_exit mérite quelques explications.

Notez d'abord que nous avons entouré d'accolades la variable PROGNAME. Pour accéder au contenu de cette variable, $PROGNAME et ${PROGNAME} sont en principe équivalents. Les accolades sont cependant nécessaires lorsqu'on veut distinguer le nom de la variable des caractères qui la suivent immédiatement (echo $PROGNAMECHOIN chercherait le contenu d'une inexistante variable PROGNAMECHOIN, tandis que echo ${PROGNAME}CHOIN afficherait le contenu de la variable PROGNAME suivi de la chaîne de caractères CHOIN).

L'expression ${1:-"Erreur inconnue"} est intéressante. Elle renvoie en général le contenu de la variable $1 ($1 contient le premier (en fait le seul) argument fourni à la fonction, qui est un message d'erreur spécifique), mais si, en revanche, on n'a fourni aucun argument à la fonction error_exit, et donc pas attribué de valeur à $1, cette expression renvoie le message Erreur inconnue, qui constitue une sorte de valeur par défaut.

Pour obtenir une répartition correcte des lignes sur les pages imprimées, vous aurez peut-être besoin d'utiliser l'option -l de la commande pr, option qui détermine le nombre de lignes à imprimer par page. J'ai personnellement eu besoin de recourir à un -l74 pour imprimer correctement le texte du programme printfile.

Création de fichiers temporaires sûrs

Dans le programme ci-dessus, diverses précautions ont été prises pour sécuriser le fichier temporaire utilisé par ce script.

C'est une tradition Unix que d'utiliser un répertoire appelé /tmp pour y placer les fichiers temporaires utilisés par les programmes. Tout le monde a donc la possibilité de créer des fichiers dans ce répertoire. Cela conduit naturellement à des inquiétudes pour la sécurité. Autant que possible, évitez donc de créer des fichiers dans le répertoire /tmp. Une meilleure technique consiste à les créer dans un répertoire local tel que ~/tmp (un sous-répertoire /tmp du répertoire personnel de l'utilisateur). Si vous devez écrire des fichiers dans /tmp, vous devez prendre des précautions pour vous assurer que le nom de ces fichiers n'est pas prévisible. Les noms de fichiers prévisibles permettent à un attaquant de créer des liens symboliques vers d'autres fichiers que l'attaquant souhaite vous voir écraser.

Un bon nom de fichier vous aidera à deviner ce que contient le fichier, mais ne sera pas totalement prévisible.

Dans le script ci-dessus, la ligne suivante de code crée le nom du fichier temporaire $TEMP_FILE :

TEMP_FILE=$TEMP_DIR/printfile.$$.$RANDOM

La variable $TEMP_DIR contient soit /tmp ou ~/tmp suivant la disponibilité du répertoire personnel de l'utilisateur qui a lancé le script. C'est une pratique répandue d'enchâsser le nom d'un programme dans un nom de fichier. Nous avons fait cela avec la chaîne de caractères printfile (rappelons que printfile est le nom de notre script). Ensuite, nous utilisons la variable du shell $$ pour enchâsser l'identifiant de processus (PID) du programme lui-même. Le PID d'un processus est unique : deux processus ne peuvent avoir le même PID à un moment donné, sur votre système. Ceci facilite aussi l'identification du processus qui a créé le fichier. Aussi surprenant que cela puisse paraître, l'identifiant du processus à lui tout seul n'est cependant pas suffisamment imprévisible pour rendre le fichier sûr, aussi ajoutons-nous la variable du shell $RANDOM pour ajouter un nombre aléatoire au nom de fichier. Avec cette technique, nous créons un nom de fichier qui est à la fois facilement identifiable et imprévisible.

Autres ressources sur l'écriture de scripts : documentation et exemples de scripts

Lectures diverses (livres, sites)

  • On pourra lire le remarquable manuel suivant (traduit en français) :
Guide avancé d'écriture des scripts Bash par Mendel Cooper
ou sa version anglaise (plus récente) :
Advanced Bash-Scripting Guide
  • Sur les scripts associés aux services du système Linux (appelés aussi "initscripts" ou "scripts système") voir :
Services#Les scripts associés aux services

Il existe aussi de nombreux livres sur l'art des scripts, j'en citerait deux qui m'ont intéressé :

  • 100 scripts shell Unix, de Dave Taylor, Eyrolles (les scripts du livre sont téléchargeables sur le site de l'éditeur).
  • Introduction aux Scripts Shell, de Arnold Robbins & Nelson H. F. Beebe, O'Reilly.
  • Citons aussi le manuel officiel de Bash (en anglais) :
GNU Bash Reference Manual, de Chet Ramey et Brian Fox, publié par Network Theory Limited, ISBN 0-9541617-7-7.

Exemples de scripts

  • Un exemple de quatre scripts destinés à protéger vos fichiers de l'effacement accidentel :
Une protection contre les effacements accidentels
  • Un exemple de deux scripts destinés à provoquer l'arrêt différé de votre ordinateur :
zzz
zzzstop
(Attention : en raison d'une inadéquation du Wiki les fichiers téléchargés doivent avoir une majuscule initiale et une extension .sh; pour une utilisation optimale vous devrez donc après téléchargement renommer Zzz.sh en zzz et Zzzstop.sh en zzzstop).
  • Un script de nettoyage :
Nettoyage du répertoire tmp des utilisateurs
  • Un script pour limiter les accès à votre disque dur en montant un répertoire en mémoire vive, dû à Eugeni Dodonov
Améliorons la vitesse du système et la durée de vie de nos disques en une seule petite commande
un script lanceur pour le script de Dodonov se trouve ici
pour sécuriser davantage ce script on pourra modifier ses premières lignes en s'inspirant de ceci



© 2000-2007, William Shotts, Jr. Verbatim copying and distribution of this entire article is permitted in any medium, provided this copyright notice is preserved.

Linux® is a registered trademark of Linus Torvalds.

© 2000-2007, William Shotts, Jr. Les copies et distributions textuelles et intégrales de cet article sont autorisées sur tout support, à la condition que cette note de copyright soit conservée.

Linux® est une marque déposée de Linus Torvalds.