Les fonctions en shell

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 :

  1. éviter ces répétitions ;
  2. diminuer les risques de bogues ;
  3. augmenter la lisibilité du script pour un humain.

Pourquoi des fonctions ?

Un programme sans fonction

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.

Soyons honnêtes : cet exemple est un peu tiré par les cheveux, car il existe une commande très simple pour faire cela :
rm -i *.aux *.log
Mais 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 !

Problèmes des programmes sans fonction

Ce programme présente certains aspects déplaisants :

  1. il n'est pas très lisible ;
  2. il comporte des répétitions relativement longues (une dizaine de lignes) ;
  3. si vous voulez changer le moindre détail, vous devrez rechercher à la main toutes les occurrences de ce que vous voulez changer, ce qui :
    1. est fatigant ;
    2. est fastidieux ;
    3. est, surtout, très peu fiable : si vous oubliez une occurrence ou que vous modifiez un endroit alors qu'il ne le fallait pas, les conséquences peuvent être graves.

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.

Définition et appel d'une fonction

L'utilisation des fonctions se fait en deux moments :

  1. d'abord, il faut définir la fonction : vous décrivez quelle série de commandes il faudra exécuter lorsque l'on appellera la fonction ;
  2. ensuite, il faut appeler la fonction à chaque endroit que vous voulez.

Analogies avec le monde humain

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

Comment définir les fonctions ?

Définir une fonction est extrêmement simple :

nom () {
instruction1
instruction2
...
}
  1. On commence par trouver un nom à la fonction. Vous pouvez choisir ce nom à votre guise, par exemple liste_des_fichiers, effacer_fichier, etc. Il est toutefois fortement recommandé :
  2. une fois que vous avez donné un nom à la fonction, notez des parenthèses ouvrante et fermante : ce sont elles qui indiquent à l'interpréteur du script qu'il s'agit d'une définition de fonction ;
  3. enfin, entre accolades, notez la série des instructions qu'il faudra exécuter à chaque appel de la fonction.

Où définir les fonctions ?

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.

Appeler une fonction

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.

Les appels entre fonctions

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

Abusez des fonctions

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 :

  1. un gain de lisibilité ;
  2. un gain d'efficacité ;
  3. un gain de souplesse.

Gain de lisibilité

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.

Gain d'efficacité

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.

Gain de souplesse

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.

Auteur : Baptiste Mélès.