Aller au contenu

Administration Unix - CM séance 06

7. Structures de contrôle du shell (suite)

7.3. Les boucles

7.3.1. La boucle while

La syntaxe prend cette forme :

while cmd1 ; do bloc1 ; done

On peut remplacer les ; par des retours chariots, par exemple :

while cmd1 ; do
    bloc1
done
Principe :
(1) la commande cmd1 est exécutée ; si elle réussit, le bloc1 est exécuté, puis on recommence en (1),
sinon la boucle est terminée.

Il existe une variante :

until cmd2 ; do bloc2 ; done

cette boucle continue tant que la commande cmd2 échoue, et s'arrête lorsqu'elle a réussi. Elle est donc équivalente à

while ! cmd2 ; do bloc2 ; done

Longueur d'une variable

la longueur d'une variable est obtenue par ${#variable} :

$ ville="Marseille"
$ echo ${#ville}
9

Exemple 1 : boucler sur la longueur d'une variable qui change

Voici un extrait de code avec un while ; tracer avec set -x.

cri=""
while test ${#cri} -lt 10 ; do
    cri="A$cri"                 # ceci rallonge la variable
done
echo "$cri !"

Saisie au clavier

On peut effectuer une saisie au clavier avec la commande read :

read variable 

read lit une ligne au clavier, puis la stocke dans la variable.

$ read nom1
Paul                # l'utilisateur tape un nom
$ echo "$nom1"
Paul

Exemple 2 : boucler sur une réponse saisie au clavier

Extrait de code avec un while et un read :

reponse=""
while test "$reponse" != "oui" ; do
    echo -n "Entrez une réponse : "
    read reponse
done
echo "Vous avez donné la bonne réponse (oui)."

7.3.2. La boucle for

La syntaxe générale est :

for nom1 in liste1 ; do bloc1 ; done

liste1 est expansé et découpé en mots sur les blancs (espace ou tabulation) ; la variable nom1 prend successivement la valeur de chaque mot de liste1, et chaque fois bloc1 est exécuté.

Exemples de boucles :

  • Sur des mots :

    for note in do  mi ; do
        echo "Je joue la note $note"
    done
    
  • Sur une liste de mots dans une variable :

    liste="pomme poire banane"
    for item in $liste ; do
        echo "Je mange une $item"
    done
    

Attention, ne pas mettre de "" autour de $liste pour qu'elle soit découpée.

  • Sur des fichiers existants :

    for file in *.txt ; do
        echo -n "la taille de $file est "
        wc -c < "$file"
    done
    
  • Sur les arguments : faut-il boucler sur $* ou $@ ?

    Essais : on donne 3 arguments, le deuxième avec un espace

    $ set aa "bb cc" dd
    $ echo $#
    3
    $ echo $2
    bb cc
    $ echo $*
    aa bb cc dd
    $ echo $@
    aa bb cc dd
    

    Le but est donc de faire ici exactement 3 itérations :

    $ for s in  $*  ; do echo "$s" ; done       # affiche 4 lignes
    $ for s in "$*" ; do echo "$s" ; done       # 1
    $ for s in  $@  ; do echo "$s" ; done       # 4
    $ for s in "$@" ; do echo "$s" ; done       # 3, ok
    

C'est donc la boucle sur "$@" qui est la bonne forme pour boucler sur des arguments sans les redécouper :

for arg in "$@" ; do bloc1 ; done

Il existe d'ailleurs un raccourci équivalent (voir : help for) :

for arg ; do bloc1 ; done

7.3.3. Contrôle de boucles

Dans le bloc d'une boucle (while, until, for) on peut utiliser les mots clés suivants :

  • break   sort immédiatement de la boucle
  • break n   sort de n boucles imbriquées
  • continue   passe immédiatement à l'itération suivante
  • continue n   passe à l'itération suivante de la n-ième boucle supérieure

Exemple 1 : recherche du premier mot commençant par un "p" dans une liste

alcanes="méthane éthane propane butane pentane hexane"
for mot in $alcanes ; do
    if [[ "$mot" == p* ]]; then         # sans "" à droite autour du motif
        echo "$mot"
        break
    fi
done

Exemple 2 : affichage de la taille des fichiers qui ne sont pas des liens

for file in * ; do
    if test -L "$file" ; then continue ; fi
    echo -n "la taille de $file est "
    wc -c < "$file"
done

7.4. Le branchement case

7.4.1. case sur des mots

La syntaxe est

case mot in
    choix1) bloc1 ;;          # notez ")" et ";;"
    choix2) bloc2 ;;
    ...
esac

mot est expansé (il s'agit souvent d'une variable), puis sa valeur est comparée aux chaînes choix1, choix2, etc (dans cet ordre).

Dès que mot correspond à l'un des choix, le bloc correspondant est exécuté jusqu'au ;; (règle du "premier qui gagne").

Ceci est équivalent à :

if test mot = "choix1" ; then
    bloc1
elif test mot = "choix2" ; then
    bloc2
elif ...
    ...
fi

Syntaxe alternative : rajouter une ( devant (optionnel, rarement utilisé) :

case mot in
    (choix1) bloc1 ;;
    (choix2) bloc2 ;;
    ...
esac

7.4.2. Les motifs de fichiers

L'instruction case devient très puissante avec l'utilisation des motifs de fichiers (pattern glob) dans les choix.

Syntaxe des motifs :

  • *   toute chaîne, y compris la chaîne vide
  • ?   un caractère, n'importe lequel
  • [abc]   un caractère dans cet ensemble 'a','b','c'
  • [a-z]   un caractère dans cet intervalle de 'a' à 'z'
  • [a-zA-Z_]   un caractère dans a-z ou A-Z ou '_'
  • [^..] ou [!..]   un caractère qui n'est pas dans cette liste

Cas particuliers :

  • pour inclure ] on le met en premier :   []..]
  • pour inclure - on le met en premier ou dernier :   [-..] ou [..-]
  • idem pour exclure - :   [^-..] ou [^..-]
  • pour exclure ^ ou ! on ne les met pas en premier :   [^..^!..]

7.4.3. case sur des motifs

La syntaxe du case est identique avec celle des motifs de fichiers ; de plus

  • on peut faire un ou entre plusieurs motifs avec | ;
  • les espaces ne sont pas pris en compte, ils doivent être matérialisés avec " " ou ' ' ;
  • on peut rajouter un cas par défaut À LA FIN avec *).
case mot in
    motif1)         bloc1 ;;
    motif2|motif3)  bloc2 ;;
    ...
    *)              bloc_defaut ;;       # cas par défaut, À LA FIN
esac

Ceci est équivalent à

if [[ mot == motif1 ]]; then
    bloc1
elif [[ mot == motif2 || mot == motif3 ]]; then
    bloc2
elif ...
    ...
else
    bloc_defaut
fi

Exemple :

echo -n "Démarrer l'installation ? "
read rep

case "$rep" in
    [oO] | [oO][uU][iI] )
        echo "L'installation va démarrer"
        ;;
    [nN] | [nN][oO][nN] )
        echo "Installation abandonnée"
        exit 0
        ;;
    *)
        echo "Erreur: oui ou non attendu" >&2
        exit 1
esac

7.5. Les fonctions

Une fonction est un nom (par exemple ma_fonction), associé à un morceau de code (appelé corps de la fonction).

Chaque fois qu'on invoque son nom, le corps de la fonction est exécuté par le shell courant.

7.5.1. Déclaration

Pour déclarer une fonction on écrit :

ma_fonction ()                    # ou : ma_fonction()
{
    # corps de la fonction
}

Ensuite, pour appeler (= invoquer ou exécuter) la fonction, on écrit :

ma_fonction

Exemple :

$ afficher_pid() { echo "Le PID est $BASHPID" ;}        # déclaration
$ afficher_pid                                          # appel
Le PID est 12467
$ echo $BASHPID
12467               # la fonction est bien exécutée dans le même shell

Il ne faut JAMAIS séparer les (), elles signifient : déclaration de fonction.

Il existe des variantes de déclaration (obsolètes) :

FUNCTION ma_fonction { ... ;}
FUNCTION ma_fonction() { ... ;}

7.5.2. Variables globales

Dans un script, toutes les variables sont globales ; donc une fonction peut utiliser et modifier une variable du script :

lire_reponse() {
    echo -n "Entrez votre réponse : "
    read REP
}

REP=""
lire_reponse
if test "$REP" == "oui" ; then ... ; fi

⚠  C'est une pratique qui peut être DANGEREUSE car elle risque de créer des bugs (par exemple, si plusieurs fonctions manipulent cette variable...).

7.5.3. Variables locales

On peut déclarer des variables locales :

local nom[=valeur]...

elles ne seront visibles que de la fonction...

$ i=5
$ a(){ local i=6 ; echo "$i";}
$ a
6
$ echo "$i"
5

... et des fonctions filles, c'est-à-dire appelées par la fonction :

$ i=5
$ a(){ local i=6 ; b ;}
$ b(){ echo "$i" ;}
$ a
6
$ b
5

⚠  Cette utilisation dans des fonctions fille peut AUSSI créer des bugs ! Par exemple si dans a() on oublie local, ce sera le i global qui sera modifié.

Bon usage :

  • mettre les variables qui doivent être globales en MAJUSCULES ;
  • déclarer systématiquement les variables comme locales et les mettre en minuscules.

7.5.4. Arguments

Les fonctions peuvent recevoir des arguments ; cela fonctionne exactement comme pour les scripts :

$ tata() { echo "J'ai reçu $# arguments ; le 1er est $1" ;}
$ tata foo bar baz
J'ai reçu 3 arguments ; le 1er est foo

Pour rendre le code plus clair, on décrit les paramètres en commentaire après ()

chercher_fichiers() # rep_depart extension
{
    find "$1" -name "*.$2" -print 2> /dev/null
}

chercher_fichiers /usr png

Pour rendre le code encore plus clair, on sauvegarde les paramètres dans des variables locales :

chercher_fichiers() # rep_depart extension
{
    local rep_depart=$1 extension=$2

    find "$rep_depart" -name "*.$extension" -print 2> /dev/null
}

chercher_fichiers /usr png

7.5.5. Divers

Les fonctions peuvent être redirigées, exactement comme pour une commande ou un script :

chercher_fichiers /usr png | grep icon | wc -l

Après l'appel d'une fonction, le code de terminaison $? est celui de la dernière commande exécutée.

On peut interrompre une fonction avec return :

  • return   le code de terminaison est celui de la dernière commande exécutée ;
  • return n   le code de terminaison est n (0 succès, 1..255 échec).

Comment renvoyer un résultat ? On le verra la prochaine fois, avec la substitution de commandes $().