tuteurs.ens.fr/unix/shell/fonction.tml
Marc Mezzarobba c7f4bbad36 Passage en UTF-8, 2 : lignes <?xml encoding?>
Last-change: ignore this commit
2009-09-27 15:45:57 +02:00

441 lines
12 KiB
XML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?xml version="1.0" encoding="UTF-8"?>
<!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 :
</p>
<ol>
<li> éviter ces répétitions ;</li>
<li> diminuer les risques de bogues ;</li>
<li> augmenter la lisibilité du script pour un humain.</li>
</ol>
<h2>Pourquoi des fonctions ?</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> : les supprimer permet donc de gagner
de l'espace disque sans dommages.
</p>
<p>
Votre script ressemble donc à ceci :
</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 ? (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 ? (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 : cet exemple est un peu tiré par les cheveux, car
il existe une commande très simple pour faire cela :
<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 -i *.aux *.log</code> !
</div>
<h3>Problèmes des programmes sans fonction</h3>
<p>
Ce programme présente certains aspects déplaisants :
</p>
<ol>
<li> il n'est pas très lisible ;</li>
<li> il comporte des répétitions relativement longues (une dizaine de
lignes) ;</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 :
<ol>
<li> est <strong>fatigant</strong> ;</li>
<li> est <strong>fastidieux</strong> ;</li>
<li> est, surtout, <strong>très peu fiable</strong> : 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 :
</p>
<ol>
<li> d'abord, il faut <strong>définir la fonction</strong> : vous
décrivez quelle série de commandes il faudra exécuter lorsque l'on
appellera la fonction ;</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 :
celle-ci commence par dire « au quatrième top, il sera neuf heures
cinquante-deux », puis « top... top... top... top. »
</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) ; une fois
cela clairement défini, l'horloge <em>égrène les quatre
« top »</em>, et le quatrième renvoie à l'énoncé « il est
neuf heures cinquante-deux ».
</p>
<p>
En langage naturel, définir une fonction équivaut à dire :
<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 :
« 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).
</p>
<h3>Définir une fonction</h3>
<h4>Comment définir les fonctions ?</h4>
<p>
Définir une fonction est extrêmement simple :
</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é :
<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 ;</li>
</ul>
</li>
<li>
une fois que vous avez donné un nom à la fonction, notez des
<strong>parenthèses ouvrante et fermante</strong> : ce sont elles
qui indiquent à l'interpréteur du script qu'il s'agit d'une définition
de fonction ;
</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 ?</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 :
« Command not found » (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é :
</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 ? (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 : 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 ; 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 :
</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 : <strong>abusez des
fonctions !</strong> N'hésitez pas à créer des fonctions pour tout
et n'importe quoi. Vous en tirerez :
</p>
<ol>
<li> un gain de lisibilité ;</li>
<li> un gain d'efficacité ;</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> : 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> fois un
même fragment de code comportant <em>p</em> lignes ; en
utilisant des fonctions on économise
(<em>n</em> - 1) x <em>p</em>  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 « à 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.
</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 : 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 : 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 : Baptiste Mélès.
Dernière modification le <date value="$Date: 2007-07-17 10:03:42 $" />.
</div>
</body>
</html>