Comme les « vrais » langages de programmation, Bash supporte les fonctions bien qu'il s'agisse d'une implémentation quelque peu limitée. Une fonction est une sous-routine, un bloc de code qui implémente un ensemble d'opérations, une « boîte noire » qui réalise une tâche spécifiée. Quand il y a un code répétitif, lorsqu'une tâche se répète avec quelques légères variations dans la procédure, alors utilisez une fonction.
function nom_fonction {
commande...
}
ou
nom_fonction () {
commande...
}
Cette deuxième forme va réjouir le coeur des programmeurs C (elle est aussi plus portable).
Comme en C, l'accolade ouvrante de la fonction peut apparaître de manière optionnelle sur la deuxième ligne.
nom_fonction ()
{
commande...
}
Une fonction peut être « réduite » à une seule ligne.
fun () { echo "Ceci est une fonction"; echo; } # ^
Néanmoins, dans ce cas, un point-virgule doit suivre la dernière commande de la fonction.
fun () { echo "Ceci est une fonction"; echo } #Erreur ! # ^ + +fun2 () { echo "Même une fonction composée d'une \ seule commande ? Oui !"; } +# ^
Les fonctions sont appelées, lancées, simplement en invoquant leur nom. Un appel de fonction équivaut à une commande.
Exemple 24.1. Fonctions simples
#!/bin/bash JUSTE_UNE_SECONDE=1 funky () { # C'est aussi simple que les fonctions get. echo "Ceci est la fonction funky." echo "Maintenant, sortie de la fonction funky." } # La déclaration de la fonction doit précéder son appel. fun () { # Une fonction un peu plus complexe. i=0 REPETITION=30 echo echo "Et maintenant, les choses drôles commencent." echo sleep $JUSTE_UNE_SECONDE # Hé, attendez une seconde ! while [ $i -lt $REPETITION ] do echo "----------FONCTIONS---------->" echo "<---------AMUSANTES----------" echo let "i+=1" done } # Maintenant, appelons les fonctions. funky fun exit 0
La définition de la fonction doit précéder son premier appel. Il n'existe pas de méthode pour « déclarer » la fonction, comme en C par exemple.
f1 # Donnera un message d'erreur car la fonction "f1" n'est pas encore définie. declare -f f1 # Ceci ne nous aidera pas plus. f1 # Toujours un message d'erreur. # Néanmoins... f1 () { echo "Appeler la fonction \"f2\" à partir de la fonction \"f1\"." f2 } f2 () { echo "Fonction \"f2\"." } f1 # La fonction "f2" n'est pas appelée jusqu'à ce point bien qu'elle soit # référencée avant sa définition. # C'est autorisé. # Merci à S.C.
Les fonctions ne peuvent pas être vides !
#!/bin/bash # fonction-vide.sh vide () { } exit 0 # On n'y arrivera pas ! # $ sh fonction-vide.sh # fonction-vide.sh: line 6: syntax error near unexpected token `}' # fonction-vide.sh: line 6: `}' # $ echo $? # 2 # Remarque : une fonction qui ne contient que des commentaires est #+ encore une fonction vide. func () { # Commentaire 1. # Commentaire 2. # Pour autant c'est encore une fonction vide. # Merci à Mark Bova pour cette remarque. } # On obtient le même message d'erreur que ci-dessus. # Cependant ... pas_tout_a_fait_vide () { commande_interdite } # Un script contentant cette fonction ne va *pas* vous sauter à la #+ figure, pourvu que la fonction ne soit pas appelée. # Merci, Thiemo Kellner, pour cette remarque.
Il est même possible d'intégrer une fonction dans une autre fonction, bien que ce ne soit pas très utile.
f1 () { f2 () # intégrée { echo "La fonction \"f2\", à l'intérieur de \"f1\"." } } f2 # Donne un message d'erreur. # Même un "declare -f f2" un peu avant ne changerait rien. echo f1 # Ne donne rien, car appeler "f1" n'appelle pas automatiquement "f2". f2 # Maintenant, il est tout à fait correct d'appeler "f2", # car sa définition est visible en appelant "f1". # Merci, S.C.
Les déclarations des fonctions peuvent apparaître dans des endroits bien étonnants, même là où irait plutôt une commande.
ls -l | foo() { echo "foo"; } # Autorisé, mais sans intérêt. if [ "$USER" = bozo ] then bozo_salutations () # Définition de fonction intégrée dans une construction if/then. { echo "Bonjour, Bozo." } fi bozo_salutations # Fonctionne seulement pour Bozo #+ et les autre utilisateurs ont une erreur. # Quelque chose comme ceci peut être utile dans certains contextes. NO_EXIT=1 # Active la définition de fonction ci-dessous. [[ $NO_EXIT -eq 1 ]] && exit() { true; } # Définition de fonction dans une "liste ET". # Si $NO_EXIT vaut 1, déclare "exit ()". # Ceci désactive la commande intégrée "exit" en créant un alias vers "true". exit # Appelle la fonction "exit ()", et non pas la commande intégrée "exit". # Ou de façon similaire : fichier=fichier1 [ -f "$fichier" ] && foo () { rm -f "$fichier"; echo "Fichier "$fichier" supprimé."; } || foo () { echo "Fichier "$fichier" introuvable."; touch bar; } foo # Merci, S.C. et Christopher Head
Les fonctions peuvent prendre d'étranges apparences.
_(){ for i in {1..10}; do echo -n "$FUNCNAME"; done; echo; } # ^^^ Pas d'espace entre le nom de la fonction et les parenthèses. # Cela ne fonctionne pas toujours. Pourquoi ? # A présent, appelons cette fonction. _ # __________ # ^^^^^^^^^^ 10 soulignements (10 x nom de la fonction)! # Un caractère de soulignement "nu" constitue un nom de fonction #+ acceptable. # En fait, un "deux-points" est aussi un nom de fonction acceptable. :(){ echo ":"; }; : # À quoi bon tout cela ? # C'est un moyen sournois de rendre illisible le code d'un script.
Voir aussi Exemple A.55, « L'algorithme de chiffrement de Cronsfeld »
Que se passe-t'il quand différentes versions de la même fonction apparaissent dans un script ?
# Comme Yan Chen le précise, # quand une fonction est définie plusieurs fois, # la version finale est celle qui est appelée. # Néanmoins, ce n'est pas particulièrement utile. fonction () { echo "Première version de func ()." } fonction () { echo "Deuxième version de func ()." } fonction # Deuxième version de func (). exit $? # Il est même possible d'utiliser des fonctions pour surcharger ou #+ préempter les commandes systèmes. # Bien sûr, ce n'est *pas* conseillé.
Les fonctions peuvent récupérer des arguments qui leur sont passés et renvoyer un code de sortie au script pour utilisation ultérieure.
nom_fonction $arg1 $arg2
La fonction se réfère aux arguments passés par leur position (comme s'ils étaient des paramètres positionnels), c'est-à-dire $1, $2 et ainsi de suite.
Exemple 24.2. Fonction prenant des paramètres
#!/bin/bash # Fonctions et paramètres DEFAUT=defaut # Valeur par défaut. fonc2 () { if [ -z "$1" ] # Est-ce que la taille du paramètre # #1 a une taille zéro ? then echo "-Le paramètre #1 a une taille nulle.-" # Ou aucun paramètre n'est passé. else echo "-Le paramètre #1 est \"$1\".-" fi variable=${1-$DEFAUT} # Que montre la substitution de echo "variable = $variable" #+ paramètre? # --------------------------- # Elle distingue entre pas de #+ paramètre et un paramètre nul. if [ "$2" ] then echo "-Le paramètre #2 est \"$2\".-" fi return 0 } echo echo "Aucun argument." fonc2 # Appelé sans argument echo echo "Argument de taille nulle." fonc2 "" # Appelé avec un paramètre de taille zéro echo echo "Paramètre nul." fonc2 "$parametre_non_initialise" # Appelé avec un paramètre non initialisé echo echo "Un paramètre." fonc2 premier # Appelé avec un paramètre echo echo "Deux paramètres." fonc2 premier second # Appelé avec deux paramètres echo echo "\"\" \"second\" comme argument." fonc2 "" second # Appelé avec un premier paramètre de taille nulle echo # et une chaîne ASCII pour deuxième paramètre. exit 0
La commande shift fonctionne sur les arguments passés aux fonctions (voir l'Exemple 36.16, « Astuce de valeur de retour »).
Mais, qu'en est-t'il des arguments en ligne de commande passés au script ? Une fonction les voit-elle ? Il est temps de dissiper toute confusion.
Exemple 24.3. Fonctions et arguments en ligne de commande passés au script
#!/bin/bash # func-cmdlinearg.sh # Call this script with a command-line argument, #+ something like $0 arg1. func () { echo "$1" } echo "First call to function: no arg passed." echo "See if command-line arg is seen." func # No! Command-line arg not seen. echo "============================================================" echo echo "Second call to function: command-line arg passed explicitly." func $1 # Now it's seen! exit 0
Contrairement à d'autres langages de programmation, normalement, les scripts shell passent seulement des paramètres par valeur aux fonctions. Les noms de variable (qui sont réellement des pointeurs), s'ils sont passés en tant que paramètres aux fonctions, seront traités comme des chaînes littérales. Les fonctions interprètent leurs arguments littéralement.
Les références de variables indirectes (voir l'Exemple 37.2, « Références de variables indirectes - la nouvelle façon ») apportent une espèce de mécanisme peu pratique pour passer des pointeurs aux fonctions.
Exemple 24.4. Passer une référence indirecte à une fonction
#!/bin/bash # ind-func.sh: Passing an indirect reference to a function. echo_var () { echo "$1" } message=Hello Hello=Goodbye echo_var "$message" # Hello # Now, let's pass an indirect reference to the function. echo_var "${!message}" # Goodbye echo "-------------" # What happens if we change the contents of "hello" variable? Hello="Hello, again!" echo_var "$message" # Hello echo_var "${!message}" # Hello, again! exit 0
La prochaine question logique est de savoir si les paramètres peuvent être déréférencés après avoir été passé à une fonction.
Exemple 24.5. Déréférencer un paramètre passé à une fonction
#!/bin/bash # dereference.sh # Dereferencing parameter passed to a function. # Script by Bruce W. Clare. dereference () { y=\$"$1" # Name of variable. echo $y # $Junk x=`eval "expr \"$y\" "` echo $1=$x eval "$1=\"Some Different Text \"" # Assign new value. } Junk="Some Text" echo $Junk "before" # Some Text before dereference Junk echo $Junk "after" # Some Different Text after exit 0
Exemple 24.6. De nouveau, déréférencer un paramètre passé à une fonction
#!/bin/bash # ref-params.sh : Déréférencer un paramètre passé à une fonction. # (exemple complexe) ITERATIONS=3 # Combien de fois obtenir une entrée. icompteur=1 ma_lecture () { # Appelé avec ma_lecture nomvariable, # Affiche la précédente valeur entre crochets comme valeur par défaut, # et demande une nouvelle valeur. local var_locale echo -n "Saisissez une valeur " eval 'echo -n "[$'$1'] "' # Valeur précédente. # eval echo -n "[\$$1] " # Plus facile à comprendre, #+ mais perd l'espace de fin à l'invite de l'utilisateur. read var_locale [ -n "$var_locale" ] && eval $1=\$var_locale # "liste-ET" : si "var_locale", alors l'initialiser à "$1". } echo while [ "$icompteur" -le "$ITERATIONS" ] do ma_lecture var echo "Entrée #$icompteur = $var" let "icompteur += 1" echo done # Merci à Stephane Chazelas pour nous avoir apporté cet exemple instructif. exit 0
Sortie et retour
Les fonctions renvoient une valeur, appelée code (ou état) de sortie, qui est l'analogue du code de sortie renvoyé par une commande. Le code de sortie peut être explicitement spécifié par une instruction return, ou par défaut ce sera le code de sortie de la dernière commande de la fonction (0 en cas de succès, une valeur non nulle sinon). Ce code de sortie peut être utilisé dans le script, il est référencé par la variable $?. Ce mécanisme est un moyen efficace pour que les fonctions aient une « valeur de retour » similaire à celle des fonctions C.
Termine une fonction. Une commande return [105] prend optionnellement un argument de type entier, qui est renvoyé au script appelant comme « code de sortie » de la fonction, et ce code de sortie est affecté à la variable $?.
Exemple 24.7. Maximum de deux nombres
#!/bin/bash # max.sh : Maximum de deux entiers. E_PARAM_ERR=250 # Si moins de deux paramètres passés à la fonction. EGAL=251 # Code de retour si les deux paramètres sont égaux. # Valeurs de l'erreur en dehors de la plage de tout paramètre #+ qui pourrait être fourni à la fonction. max2 () # Envoie le plus important des deux entiers. { # Note: les nombres comparés doivent être plus petits que 257. if [ -z "$2" ] then return $E_PARAM_ERR fi if [ "$1" -eq "$2" ] then return $EGAL else if [ "$1" -gt "$2" ] then return $1 else return $2 fi fi } max2 33 34 return_val=$? if [ "$return_val" -eq $E_PARAM_ERR ] then echo "Vous devez donner deux arguments à la fonction." elif [ "$return_val" -eq $EGAL ] then echo "Les deux nombres sont identiques." else echo "Le plus grand des deux nombres est $return_val." fi exit 0 # Exercice (facile) : # ------------------ # Convertir ce script en une version interactive, #+ c'est-à-dire que le script vous demande les entrées (les deux nombres).
Pour qu'une fonction renvoie une chaîne de caractères ou un tableau, utilisez une variable dédiée.
compte_lignes_dans_etc_passwd() { [[ -r /etc/passwd ]] && REPONSE=$(echo $(wc -l /etc/passwd)) # Si /etc/passwd est lisible, met dans REPONSE le nombre de lignes. # Renvoie une valeur et un statut. # Le 'echo' ne semble pas nécessaire mais... # il supprime les espaces blancs excessifs de la sortie. } if compte_ligne_dans_etc_passwd then echo "Il y a $REPONSE lignes dans /etc/passwd." else echo "Ne peut pas compter les lignes dans /etc/passwd." fi # Merci, S.C.
Exemple 24.8. Convertir des nombres en chiffres romains
#!/bin/bash # Conversion d'un nombre arabe en nombre romain # Échelle : 0 - 200 # C'est brut, mais cela fonctionne. # Étendre l'échelle et améliorer autrement le script est laissé en exercice. # Usage: romain nombre-a-convertir LIMITE=200 E_ERR_ARG=65 E_HORS_ECHELLE=66 if [ -z "$1" ] then echo "Usage: `basename $0` nombre-a-convertir" exit $E_ERR_ARG fi num=$1 if [ "$num" -gt $LIMITE ] then echo "En dehors de l'échelle !" exit $E_HORS_ECHELLE fi vers_romain () # Doit déclarer la fonction avant son premier appel. { nombre=$1 facteur=$2 rchar=$3 let "reste = nombre - facteur" while [ "$reste" -ge 0 ] do echo -n $rchar let "nombre -= facteur" let "reste = nombre - facteur" done return $nombre # Exercice : # --------- # Expliquer comment fonctionne cette fonction. # Astuce : division par une soustraction successive. } vers_romain $nombre 100 C nombre=$? vers_romain $nombre 90 XC nombre=$? vers_romain $nombre 50 L nombre=$? vers_romain $nombre 40 XL nombre=$? vers_romain $nombre 10 X nombre=$? vers_romain $nombre 9 IX nombre=$? vers_romain $nombre 5 V nombre=$? vers_romain $nombre 4 IV nombre=$? vers_romain $nombre 1 I echo exit 0
Voir aussi l'Exemple 11.28, « Vérification d'une entrée alphabétique ».
L'entier positif le plus grand qu'une fonction peut renvoyer est 255. La commande return est très liée au code de sortie, qui tient compte de cette limite particulière. Heureusement, il existe quelques astuces pour ces situations réclamant une valeur de retour sur un grand entier.
Exemple 24.9. Tester les valeurs de retour importantes dans une fonction
#!/bin/bash # return-test.sh # La plus grande valeur positive qu'une fonction peut renvoyer est 255. test_retour () # Renvoie ce qui lui est passé. { return $1 } test_retour 27 # OK. echo $? # Renvoie 27. test_retour 255 # Toujours OK. echo $? # Renvoie 255. test_retour 257 # Erreur! echo $? # Renvoie 1 (code d'erreur divers). test_retour -151896 # Néanmoins, les valeurs négatives peuvent être plus #+ importantes. echo $? # Renvoie -151896. # ====================================================== test_retour -151896 # Est-ce que les grands nombres négatifs vont #+ fonctionner ? echo $? # Est-ce que ceci va renvoyer -151896? # Non! Il renvoie 168. # Les versions de Bash antérieures à la 2.05b permettaient les codes de retour #+ au format d'un grand entier négatif. # Les nouvelles versions ont corrigées cette faille. # Ceci peut casser les anciens scripts. # Attention ! # ====================================================== exit 0
Un contournement pour obtenir des « codes de retour » au format entier long est de tout simplement affecter le « code de retour » à une variable globale.
Val_Retour= # Variable globale pour recevoir une valeur de retour #+ d'une taille trop importante. alt_return_test () { fvar=$1 Val_Retour=$fvar return # Renvoie 0 (succès). } alt_return_test 1 echo $? # 0 echo "valeur de retour = $Val_Retour" # 1 alt_return_test 256 echo "valeur de retour = $Val_Retour" # 256 alt_return_test 257 echo "valeur de retour = $Val_Retour" # 257 alt_return_test 25701 echo "valeur de retour = $Val_Retour" #25701
Une méthode plus élégante est de demander à la fonction d'afficher (via echo) son « code de retour » sur stdout et de le capturer par substitution de commandes. Voir la discussion de ceci dans la Section 36.7, « Astuces assorties ».
Exemple 24.10. Comparer deux grands entiers
#!/bin/bash # max2.sh : Maximum de deux GRANDS entiers. # Ceci correspond au précédent exemple "max.sh", modifié pour permettre la #+ comparaison de grands entiers. EGAL=0 # Code de retour si les deux paramètres sont égaux. E_PARAM_ERR=99999 # Pas assez de paramètres fournis à la fonction. # ^^^^^^ En dehors de la plage de tout paramètre fourni max2 () # Renvoie le plus gros des deux nombres. { if [ -z "$2" ] then echo $E_PARAM_ERR return fi if [ "$1" -eq "$2" ] then echo $EGAL return else if [ "$1" -gt "$2" ] then retval=$1 else retval=$2 fi fi echo $retval # Affiche (sur stdout) plutôt que de retourner la valeur. # Pourquoi ? } valeur_retour=$(max2 33001 33997) # ^^^^ nom de la fonction # ^^^^^ ^^^^^ paramètres fournis # C'est en fait une forme de substitution de commandes : #+ traiter une fonction comme s'il s'agissait d'une commande #+ et affecter la sortie de la fonction à la variable "valeur_retour". # ========================= SORTIE ======================== if [ "$valeur_retour" -eq "$E_PARAM_ERR" ] then echo "Erreur : Pas assez de paramètres passés à la fonction de comparaison." elif [ "$valeur_retour" -eq "$EGAL" ] then echo "Les deux nombres sont égaux." else echo "Le plus grand des deux nombres est $valeur_retour." fi exit 0 # ========================================================= # Exercices : # ---------- # 1) Trouvez un moyen plus élégant pour tester les paramètres passés à la #+ fonction. # 2) Simplifiez la structure du if/then à partir de "SORTIE". # 3) Réécrire le script pour prendre en entrée des paramètres de la ligne de # commande.
Voici un autre exemple de capture de la « valeur de retour » d'une fonction. Le comprendre requiert quelques connaissances d'awk.
longueur_mois () # Prend le numéro du mois en argument. { # renvoie le nombre de jours dans ce mois. moisJ="31 28 31 30 31 30 31 31 30 31 30 31" # Déclaré en tant que local ? echo "$moisJ" | awk '{ print $'"${1}"' }' # Astuce. # ^^^^^^^^^ # Paramètre passé à la fonction ($1 -- numéro du mois), puis à awk. # Awk voit ceci comme "print $1 . . . print $12" (suivant le numéro du mois) # Modèle pour passer un paramètre à un script awl embarqué : # $'"${script_parametre}"' # Besoin d'une vérification d'erreurs pour les paramètres de l'échelle (1-12) #+ et pour l'année bissextile avec février. } # ---------------------------------------------- # Exemple d'utilisation : mois=4 # avril, par exemple (4è mois). journees=$(longueur_mois $mois) echo $journees # 30 # ----------------------------------------------
Voir aussi l'Exemple A.7, « days-between : Calculer le nombre de jours entre deux dates » et l'Exemple A.37, « Écart-type ».
Exercice : Utiliser ce que nous venons d'apprendre, étendre l'exemple précédent sur les nombres romains pour accepter une entrée arbitrairement grande.
Redirection
Une fonction est essentiellement un bloc de code, ce qui signifie que stdin peut être redirigé (comme dans l'Exemple 3.1, « Blocs de code et redirection d'entrées/sorties »).
Exemple 24.11. Vrai nom pour un utilisateur
#!/bin/bash # realname.sh # À partir du nom utilisateur, obtenir le "vrai nom" dans /etc/passwd. NBARGS=1 # Attend un arg. E_MAUVAISARGS=65 fichier=/etc/passwd motif=$1 if [ $# -ne "$NBARGS" ] then echo "Usage : `basename $0` NOMUTILISATEUR" exit $E_MAUVAISARGS fi partie_fichier () # Parcours le fichier pour trouver le motif, #+ la portion pertinente des caractères de la ligne. { while read ligne # "while" n'a pas nécessairement besoin d'une "[ condition]" do echo "$ligne" | grep $1 | awk -F":" '{ print $5 }' # awk utilise le délimiteur ":". done } <$fichier # Redirige dans le stdin de la fonction. partie_fichier $motif # Oui, le script entier peut être réduit en # grep MOTIF /etc/passwd | awk -F":" '{ print $5 }' # ou # awk -F: '/MOTIF/ {print $5}' # ou # awk -F: '($1 == "nomutilisateur") { print $5 }' # vrai nom à partir du nom utilisateur # Néanmoins, ce n'est pas aussi instructif. exit 0
Il existe une autre méthode, certainement moins compliquée, de rediriger le stdin d'une fonction. Celle-ci fait intervenir la redirection de stdin vers un bloc de code entre accolades contenu à l'intérieur d'une fonction.
# Au lieu de : Fonction () { ... } < fichier # Essayez ceci : Fonction () { { ... } < fichier } # De façon similaire, Fonction () # Ceci fonctionne. { { echo $* } | tr a b } Fonction () # Ceci ne fonctionne pas. { echo $* } | tr a b # Un bloc de code intégré est obligatoire ici. # Merci, S.C.
Le fichier exemple bashrc envoyé par Emmanuel Rouat contient quelques exemples intéressants de fonctions.