tuteurs.ens.fr/unix/shell/fonction.tml
meles 26abda08cc Bap: Grosses modifications en série sur tout le site :
1) insertion de balises <date value="$Date$"/>
2) remplacement des espaces insécables par des &nbsp;
3) insertion d'espaces insécables autour des guillemets français
2007-07-13 08:40:16 +00:00

441 lines
12 KiB
XML

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE html
PUBLIC "-//ENS/Tuteurs//DTD TML 1//EN"
"tuteurs://DTD/tml.dtd">
<html>
<head>
<title>Fonctions</title>
</head>
<body>
<h1>Les fonctions en shell</h1>
<p>
Vous savez qu'<strong>un script shell n'est rien d'autre qu'une série de
commandes</strong> (si vous ne le savez pas, lisez la page d'<a
href="script.html">initiation à la programmation en shell</a>). 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.
</p>
<p>
L'usage des <strong>fonctions</strong> permet de&nbsp;:
</p>
<ol>
<li> éviter ces répétitions&nbsp;;</li>
<li> diminuer les risques de bogues&nbsp;;</li>
<li> augmenter la lisibilité du script pour un humain.</li>
</ol>
<h2>Pourquoi des fonctions&nbsp;?</h2>
<h3>Un programme sans fonction</h3>
<p>
Utilisateur de TeX ou <a href="&url.tuteurs;logiciels/latex/">LaTeX</a>
(ou non), vous voulez effacer régulièrement tous les fichiers
<code>.aux</code> et <code>.log</code> 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 <code>.tex</code>&nbsp;: les supprimer permet donc de gagner
de l'espace disque sans dommages.
</p>
<p>
Votre script ressemble donc à ceci&nbsp;:
</p>
<pre>
#!/bin/sh
# fichier &quot;texcleaner&quot; : 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 &quot;$fichier&quot;
echo &quot;Voulez-vous vraiment l'effacer&nbsp;? (o/n)&quot;
# Je lis la réponse de l'utilisateur
read reponse
# Et s'il dit oui, j'efface
if [[ $reponse == &quot;o&quot; ]]
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 &quot;$fichier&quot;
echo &quot;Voulez-vous vraiment l'effacer&nbsp;? (o/n)&quot;
# Je lis la réponse de l'utilisateur
read reponse
# Et s'il dit oui, j'efface
if [[ $reponse == &quot;o&quot; ]]
then rm -f $fichier
fi
done
</pre>
<p>
Vous venez de terminer ce programme, et vous êtes content, car il
fonctionne comme vous le voulez.
</p>
<div class="encadre">
Soyons honnêtes&nbsp;: cet exemple est un peu tiré par les cheveux, car
il existe une commande très simple pour faire cela&nbsp;:
<pre>
rm -i *.aux *.log
</pre>
Mais ici, cet exemple <em>à la limite</em> 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
<code>rm&nbsp;-i&nbsp;*.aux&nbsp;*.log</code>&nbsp;!
</div>
<h3>Problèmes des programmes sans fonction</h3>
<p>
Ce programme présente certains aspects déplaisants&nbsp;:
</p>
<ol>
<li> il n'est pas très lisible&nbsp;;</li>
<li> il comporte des répétitions relativement longues (une dizaine de
lignes)&nbsp;;</li>
<li> si vous voulez changer le moindre détail, vous devrez rechercher <i>à
la main</i> toutes les occurrences de ce que vous voulez changer, ce
qui&nbsp;:
<ol>
<li> est <strong>fatigant</strong>&nbsp;;</li>
<li> est <strong>fastidieux</strong>&nbsp;;</li>
<li> est, surtout, <strong>très peu fiable</strong>&nbsp;: si vous
oubliez une occurrence ou que vous modifiez un endroit alors qu'il ne le
fallait pas, les conséquences peuvent être graves.</li>
</ol>
</li>
</ol>
<p>
Il faudrait, pour pallier ces inconvénients, trouver un moyen de
<strong>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</strong>. C'est pour cela que les fonctions existent.
</p>
<h2>Définition et appel d'une fonction</h2>
<p>
L'utilisation des fonctions se fait en deux moments&nbsp;:
</p>
<ol>
<li> d'abord, il faut <strong>définir la fonction</strong>&nbsp;: vous
décrivez quelle série de commandes il faudra exécuter lorsque l'on
appellera la fonction&nbsp;;</li>
<li> ensuite, il faut <strong>appeler la fonction</strong> à chaque
endroit que vous voulez.</li>
</ol>
<h3>Analogies avec le monde humain</h3>
<p>
Dans le monde naturel, on peut comparer cela à l'horloge parlante&nbsp;:
celle-ci commence par dire «&nbsp;au quatrième top, il sera neuf heures
cinquante-deux&nbsp;», puis «&nbsp;top... top... top... top.&nbsp;»
</p>
<p>
On pourrait dire que dans un premier temps, l'horloge parlante
<em>définit une fonction</em>, en assignant un signal (le quatrième top)
à ce qu'il signale (il est neuf heures cinquante-deux)&nbsp;; une fois
cela clairement défini, l'horloge <em>égrène les quatre
«&nbsp;top&nbsp;»</em>, et le quatrième renvoie à l'énoncé «&nbsp;il est
neuf heures cinquante-deux&nbsp;».
</p>
<p>
En langage naturel, définir une fonction équivaut à dire&nbsp;:
<em>quand je dirai le nom de la fonction, vous l'exécuterez</em>. C'est
un acte de langage, comme quand un arbitre dit aux athlètes&nbsp;:
«&nbsp;Go&nbsp;!&nbsp;»&nbsp;; car tous les athlètes savent que le
signifiant «&nbsp;Go&nbsp;!&nbsp;» (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).
</p>
<h3>Définir une fonction</h3>
<h4>Comment définir les fonctions&nbsp;?</h4>
<p>
Définir une fonction est extrêmement simple&nbsp;:
</p>
<pre>
nom () {
instruction1
instruction2
...
}
</pre>
<ol>
<li>
On commence par <strong>trouver un nom à la fonction</strong>. Vous
pouvez choisir ce nom à votre guise, par exemple
<code>liste_des_fichiers</code>, <code>effacer_fichier</code>, etc. Il
est toutefois fortement recommandé&nbsp;:
<ul>
<li> de <strong>ne pas donner à ses fonctions des noms de commandes
existant déjà</strong>, par exemple <code>ls</code>, <code>mutt</code>,
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 (<code>cd</code>, <code>alias</code>, etc.) et les commandes
externes (<code>mutt</code>, <code>mozilla</code>, etc.). Le
comportement devient difficilement prévisible, et surtout, le script
sera très difficile à débuguer... </li>
<li> de <strong>ne pas utiliser de signes de ponctuation, d'espaces ni
de caractères accentués</strong> dans les noms de fonction&nbsp;;</li>
</ul>
</li>
<li>
une fois que vous avez donné un nom à la fonction, notez des
<strong>parenthèses ouvrante et fermante</strong>&nbsp;: ce sont elles
qui indiquent à l'interpréteur du script qu'il s'agit d'une définition
de fonction&nbsp;;
</li>
<li>
enfin, <strong>entre accolades</strong>, notez la <strong>série des
instructions</strong> qu'il faudra exécuter à chaque appel de la
fonction.
</li>
</ol>
<h4>Où définir les fonctions&nbsp;?</h4>
<p>
Comme l'interpréteur de scripts shell lit des scripts ligne à ligne,
<strong>il faut que la fonction soit définie avant d'être
appelée</strong>. Sinon, vous recevez un message de type&nbsp;:
«&nbsp;Command not found&nbsp;» (commande introuvable). Par convention,
il est préférable de <strong>placer <em>toutes</em> les définitions de
fonction vers le début du programme</strong>, avant toutes les
instructions.
</p>
<h3>Appeler une fonction</h3>
<p>
Notre programme sera donc considérablement allégé&nbsp;:
</p>
<pre>
#!/bin/sh
# fichier &quot;texcleaner&quot; : 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 &quot;$1&quot;
echo &quot;Voulez-vous vraiment l'effacer&nbsp;? (o/n)&quot;
# Je lis la réponse de l'utilisateur
read reponse
# Et s'il dit oui, j'efface
if [[ $reponse == &quot;o&quot; ]]
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
</pre>
<p>
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.
</p>
<p>
Un détail de la fonction <code>effacer_fichier</code> peut vous
étonner&nbsp;: nous utilisons l'argument <code>$1</code>. Comme vous le
savez sans doute (sinon, lisez la page sur
les <a href="commande.html">commandes shell et leurs
arguments</a>), <code>$1</code> désigne le premier argument passé à une
commande&nbsp;; or <strong>les fonctions peuvent recevoir des
arguments</strong>, exactement de la même façon que les commandes. C'est
même tout à fait normal, car <strong>les fonctions sont en fait des
commandes comme les autres, à ceci près qu'elles ne sont valables qu'à
l'échelle d'un script</strong>, et non à celle d'un système tout entier.
</p>
<h3>Les appels entre fonctions</h3>
<p>
Sachez enfin que <strong>des fonctions peuvent appeler d'autres
fonctions</strong>, ce qui donne une extrême souplesse à leur
utilisation.
</p>
<p>
En voici un bref exemple&nbsp;:
</p>
<pre>
#!/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 &quot;Bonjour &quot;
echo $USER
}
# J'appelle la deuxième fonction
saluer_utilisateur
</pre>
<h2>Abusez des fonctions</h2>
<p>
De là, passons à un conseil de programmation&nbsp;: <strong>abusez des
fonctions&nbsp;!</strong> N'hésitez pas à créer des fonctions pour tout
et n'importe quoi. Vous en tirerez&nbsp;:
</p>
<ol>
<li> un gain de lisibilité&nbsp;;</li>
<li> un gain d'efficacité&nbsp;;</li>
<li> un gain de souplesse.</li>
</ol>
<h3>Gain de lisibilité</h3>
<p>
<strong>Un programme sans fonction n'est lisible que s'il est très
petit</strong>&nbsp;: 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'&oelig;il pour un humain.
</p>
<p>
Supposons qu'un programme soit amené à répéter <em>n</em>&nbsp;fois un
même fragment de code comportant <em>p</em>&nbsp;lignes&nbsp;; en
utilisant des fonctions on économise
(<em>n</em>&nbsp;-&nbsp;1)&nbsp;x&nbsp;<em>p</em>&nbsp; lignes (toutes
les occurrences de la répétition, moins la définition de la fonction).
</p>
<p>
Ce gain de lignes est indissociable d'un gain de lisibilité, car les
répétitions fatiguent inutilement le cerveau humain.
</p>
<h3>Gain d'efficacité</h3>
<p>
En recopiant «&nbsp;à la main&nbsp;» 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&nbsp;; au pire,
catastrophiques.
</p>
<p>
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 <em>circonscrire le bogue</em>.
</p>
<h3>Gain de souplesse</h3>
<p>
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&nbsp;: vous apportez vos
modifications de manière centralisée.
</p>
<p>
Pour décrire ce phénomène, on parle souvent de
<strong>modularité</strong>, ou encore
d'<strong>encapsulation</strong>. 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 <em>cache</em> aux
fonctions qui l'appellent la complexité de son travail, pour leur offrir
une interface simple&nbsp;: le travail est divisé en autant de petites
tâches que nécessaires, au lieu d'une seule tâche gigantesque et
labyrinthesque.
</p>
<p>
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.
</p>
<p>
Vous pouvez revenir à la <a href="index.html">page centrale sur le
shell</a>, d'où vous pourrez vous orienter vers d'autres parties du
cours.
</p>
<div class="metainformation">
Auteur&nbsp;: Baptiste Mélès.
Dernière modification le <date value="$Date: 2007-07-13 08:41:45 $" />.
</div>
</body>
</html>