23. Fonctions

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 plaira aux programmeurs C (et est 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...
}

[Note]

Note

Une fonction peut être « réduite » à une seule ligne.

fun () { echo "C'est une fonction"; echo; }

Néanmoins, dans ce cas, un point-virgule doit suivre la dernière commande de la fonction.

fun () { echo "This is a function"; echo } # Error!

Les fonctions sont appelées, lancées, simplement en invoquant leur noms.

Exemple 23.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.

Il est même possible d'intégrer une fonction dans une autre fonction bien que cela 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
[Note]

Note

Qu'arrive-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é.

23.1. Fonctions complexes et complexité des fonctions

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 23.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

[Important]

Important

La commande shift fonctionne sur les arguments passés aux fonctions (voir l'Exemple 33.15, « 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 23.3. Fonctions et arguments en ligne de commande passés au script

#!/bin/bash
# func-cmdlinearg.sh
#  Appelez ce script avec un argument en ligne de commande,
#+ quelque chose comme $0 arg1.


fonction ()

{
echo "$1"
}

echo "premier appel à la fonction : aucun argument passé."
echo "Vérifie si la ligne de commande a été vue."
fonction
# Non ! Argument en ligne de commande non vu.

echo "============================================================"
echo
echo "Second appel à la fonction : argument en ligne de commande passé"
echo "explicitement."
fonction $1
# Maintenant, il est vu !

exit 0

[Note]

Note

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 34.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 23.4. Passer une référence indirecte à une fonction

#!/bin/bash
# ind-func.sh : Passer une référence indirecte à une fonction.

echo_var ()
{
echo "$1"
}

message=Bonjour
Bonjour=Aurevoir

echo_var "$message"        # Bonjour
# Maintenant, passons une référence indirecte à la fonction.
echo_var "${!message}"     # Aurevoir

echo "-------------"

# Qu'arrive-t'il si nous changeons le contenu de la variable "Bonjour" ?
Bonjour="Bonjour, de nouveau !"
echo_var "$message"        # Bonjour
echo_var "${!message}"     # Bonjour, de nouveau !

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 23.5. Déréférencer un paramètre passé à une fonction

#!/bin/bash
# dereference.sh
# Déréférence un paramètre passé à une fonction.
# Script de Bruce W. Clare.

dereference ()
{
     y=\$"$1"   # Nom de la variable.
     echo $y    # $Crotte

     x=`eval "expr \"$y\" "`
     echo $1=$x
     eval "$1=\"Un texte différent \""  # Affecte une nouvelle valeur.
}

Crotte="Un texte"
echo $Crotte "avant"   # Un texte avant

dereference Crotte
echo $Junk "après"     # Un texte différent après

exit 0

Exemple 23.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

code de sortie

Les fonctions renvoient une valeur, appelée un code (ou état) de sortie. Le code de sortie peut être explicitement spécifié par une instruction return, sinon, il s'agit du code de sortie de la dernière commande de la fonction (0 en cas de succès et une valeur non nulle sinon). Ce status de sortie peut être utilisé dans le script en le référençant à l'aide de la variable $?. Ce mécanisme permet effectivement aux fonctions des scripts d'avoir une « valeur de retour » similaire à celle des fonctions C.

return

Termine une fonction. Une commande return [86] 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 23.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).

[Astuce]

Astuce

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 &nbsp; /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 23.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 10.28, « Vérification d'une entrée alphabétique ».

[Important]

Important

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 23.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 33.8, « Astuces assorties ».

Exemple 23.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 ».

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

Rediriger le stdin d'une fonction

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 23.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
modele=$1

if [ $# -ne "$NBARGS" ]
then
  echo "Usage : `basename $0` NOMUTILISATEUR"
  exit $E_MAUVAISARGS
fi  

partie_fichier ()  # Parcours le fichier pour trouver le modèle,
                   #+ 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 $modèle

# Oui, le script entier peut être réduit en
#       grep MODELE /etc/passwd | awk -F":" '{ print $5 }'
# ou
#       awk -F: '/MODELE/ {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.


[86] La commande return est une commande intégrée Bash.