Vous savez qu'un script shell n'est rien d'autre qu'une série de commandes (si vous ne le savez pas, lisez la page d'initiation à la programmation en shell). Mais parfois cela pose des problèmes, car lorsqu'un script devient un peu long et surtout lorsqu'il est obligé de se répéter, les risques de bogues (dysfonctionnements) croissent.
L'usage des fonctions permet de :
Utilisateur de TeX ou LaTeX
(ou non), vous voulez effacer régulièrement tous les fichiers
.aux
et .log
qui polluent vos répertoires.
Pour ceux qui ne connaissent pas TeX, sachez que ce sont des fichiers
construits automatiquement, et que l'on peut recréer facilement à partir
du fichier .tex
: les supprimer permet donc de gagner
de l'espace disque sans dommages.
Votre script ressemble donc à ceci :
#!/bin/sh # fichier "texcleaner" : efface les fichiers aux et log # Je prends chaque fichier .aux du répertoire courant for fichier in *.aux do # J'affiche son nom et demande confirmation pour l'effacer echo "$fichier" echo "Voulez-vous vraiment l'effacer ? (o/n)" # Je lis la réponse de l'utilisateur read reponse # Et s'il dit oui, j'efface if [[ $reponse == "o" ]] then rm -f $fichier fi done # Je prends chaque fichier .log du répertoire courant for fichier in *.log do # J'affiche son nom et demande confirmation pour l'effacer echo "$fichier" echo "Voulez-vous vraiment l'effacer ? (o/n)" # Je lis la réponse de l'utilisateur read reponse # Et s'il dit oui, j'efface if [[ $reponse == "o" ]] then rm -f $fichier fi done
Vous venez de terminer ce programme, et vous êtes content, car il fonctionne comme vous le voulez.
rm -i *.aux *.logMais ici, cet exemple à la limite est là pour illustrer d'une façon simple l'intérêt des fonctions... Disons alors que cet exemple permet de donner une version francisée de la commande
rm -i *.aux *.log
!
Ce programme présente certains aspects déplaisants :
Il faudrait, pour pallier ces inconvénients, trouver un moyen de centraliser les informations destinées à être répétées, afin que l'on puisse s'y référer à chaque endroit où cela est nécessaire. C'est pour cela que les fonctions existent.
L'utilisation des fonctions se fait en deux moments :
Dans le monde naturel, on peut comparer cela à l'horloge parlante : celle-ci commence par dire « au quatrième top, il sera neuf heures cinquante-deux », puis « top... top... top... top. »
On pourrait dire que dans un premier temps, l'horloge parlante définit une fonction, en assignant un signal (le quatrième top) à ce qu'il signale (il est neuf heures cinquante-deux) ; une fois cela clairement défini, l'horloge égrène les quatre « top », et le quatrième renvoie à l'énoncé « il est neuf heures cinquante-deux ».
En langage naturel, définir une fonction équivaut à dire : quand je dirai le nom de la fonction, vous l'exécuterez. C'est un acte de langage, comme quand un arbitre dit aux athlètes : « Go ! » ; car tous les athlètes savent que le signifiant « Go ! » (ou le coup de pistolet tiré en l'air) signifie qu'ils doivent partir (la fonction a été définie dans le règlement officiel de leur sport).
Définir une fonction est extrêmement simple :
nom () { instruction1 instruction2 ... }
liste_des_fichiers
, effacer_fichier
, etc. Il
est toutefois fortement recommandé :
ls
, mutt
,
etc. Cela pourrait en effet poser de graves problèmes, car les fonctions
définies dans un programme sont prioritaires sur les commandes integrées
du shell (cd
, alias
, etc.) et les commandes
externes (mutt
, mozilla
, etc.). Le
comportement devient difficilement prévisible, et surtout, le script
sera très difficile à débuguer... Comme l'interpréteur de scripts shell lit des scripts ligne à ligne, il faut que la fonction soit définie avant d'être appelée. Sinon, vous recevez un message de type : « Command not found » (commande introuvable). Par convention, il est préférable de placer toutes les définitions de fonction vers le début du programme, avant toutes les instructions.
Notre programme sera donc considérablement allégé :
#!/bin/sh # fichier "texcleaner" : efface les fichiers aux et log # Je définis ma fonction effacer_fichier effacer_fichier () { # J'affiche son nom et demande confirmation pour l'effacer echo "$1" echo "Voulez-vous vraiment l'effacer ? (o/n)" # Je lis la réponse de l'utilisateur read reponse # Et s'il dit oui, j'efface if [[ $reponse == "o" ]] then rm -f $1 fi } # Je prends chaque fichier .aux du répertoire courant for fichier in *.aux do # J'appelle la fonction effacer_fichier pour chaque fichier effacer_fichier $fichier done # Je prends chaque fichier .log du répertoire courant for fichier in *.log do # J'appelle la fonction effacer_fichier pour chaque fichier effacer_fichier $fichier done
On économise une dizaine de lignes, on gagne en lisibilité, et les risques de bogues diminuent considérablement car s'il y a des corrections à apporter, c'est à un seul endroit du script, et non d'une façon disséminée sur l'ensemble du programme.
Un détail de la fonction effacer_fichier
peut vous
étonner : nous utilisons l'argument $1
. Comme vous le
savez sans doute (sinon, lisez la page sur
les commandes shell et leurs
arguments), $1
désigne le premier argument passé à une
commande ; or les fonctions peuvent recevoir des
arguments, exactement de la même façon que les commandes. C'est
même tout à fait normal, car les fonctions sont en fait des
commandes comme les autres, à ceci près qu'elles ne sont valables qu'à
l'échelle d'un script, et non à celle d'un système tout entier.
Sachez enfin que des fonctions peuvent appeler d'autres fonctions, ce qui donne une extrême souplesse à leur utilisation.
En voici un bref exemple :
#!/bin/sh # Je définis une première fonction ecrire_sur_une_ligne () { echo -n $* } # Je définis une deuxième fonction qui appelle la première saluer_utilisateur () { ecrire_sur_une_ligne "Bonjour " echo $USER } # J'appelle la deuxième fonction saluer_utilisateur
De là, passons à un conseil de programmation : abusez des fonctions ! N'hésitez pas à créer des fonctions pour tout et n'importe quoi. Vous en tirerez :
Un programme sans fonction n'est lisible que s'il est très petit : une vingtaine de lignes tout au plus. Dès qu'un programme dépasse cette taille, il cesse d'être intelligible d'un seul coup d'œil pour un humain.
Supposons qu'un programme soit amené à répéter n fois un même fragment de code comportant p lignes ; en utilisant des fonctions on économise (n - 1) x p lignes (toutes les occurrences de la répétition, moins la définition de la fonction).
Ce gain de lignes est indissociable d'un gain de lisibilité, car les répétitions fatiguent inutilement le cerveau humain.
En recopiant « à la main » des fragments identiques de codes, vous risquez toujours la faute de frappe. Or, la moindre coquille peut avoir des conséquences, au mieux, imprévisibles ; au pire, catastrophiques.
En isolant les séries d'instructions dans des définitions de fonctions, vous concentrez en un seul endroit la situation possible d'un bogue donné. Vous pouvez circonscrire le bogue.
En utilisant des fonctions, vous donnez à vos programmes une grande souplesse. Si vous voulez apporter une modification d'ensemble à un programme, vous n'avez plus à corriger autant de morceaux du script qu'il y a d'occurrences du même fragment : vous apportez vos modifications de manière centralisée.
Pour décrire ce phénomène, on parle souvent de modularité, ou encore d'encapsulation. Il s'agit en effet de privilégier l'assemblage simple d'éléments simples à l'assemblage complexe d'éléments complexes. Ainsi, chaque fonction cache aux fonctions qui l'appellent la complexité de son travail, pour leur offrir une interface simple : le travail est divisé en autant de petites tâches que nécessaires, au lieu d'une seule tâche gigantesque et labyrinthesque.
Pour toutes ces raisons, n'hésitez surtout pas à créer des fonctions et à les emboîter entre elles. La programmation vous paraîtra de plus en plus facile, à mesure que vous réaliserez des tâches de plus en plus complexes.
Vous pouvez revenir à la page centrale sur le shell, d'où vous pourrez vous orienter vers d'autres parties du cours.