Aller au contenu

Administration Unix - CM séance 08

9. Approfondissements du langage bash

9.1. Documentation

On a déjà vu les commandes help, man et info.

Il existe de nombreux cours, tutoriels, livres en ligne, par ex. le wikibook.

⚠ Évitez les documents qui mettent en avant des syntaxes "à l'ancienne" : utilisation immodérée de ` `, de sed, de awk, etc.

La documentation officielle est sur le site de GNU.

Il existe une documentation très poussée : Advanced Bash Scripting Guide (ABSG), existe en version française (il y a des passages qui ne sont pas à jour).

On peut aussi consulter la page du projet de Chet Ramey, mainteneur de bash, son historique des versions, et la liste des changements notables dans la FAQ de bash.


Voici un outil très pratique de débogage : ShellCheck.

Utilisation : copier-coller votre script (y compris le shebang) dans la boîte "Editor" ; les erreurs seront affichées dessous dans la boîte "ShellCheck output".

Exemple :

#! /bin/bash
bonjour()
{
    echo "salut $nom"
}

Affichera

Line 4:
    echo "salut $nom"
                ^-- SC2154: nom is referenced but not assigned.

(signifie : variable non initialisée)

9.2. Sortie et résultat de fonction

9.2.1. Sortie avec return

On a vu que le code de terminaison d'une fonction est celui de la dernière commande exécutée :

$ hello() {
    echo "bonjour"
    false
  }
$ hello ; echo $?
bonjour
1

On peut interrompre le déroulement d'une fonction avec :

  • return   le code de terminaison de la fonction sera celui de la dernière commande exécuté,
  • return n   le code de terminaison de la fonction sera n.
$ hello() {
    echo "bonjour"
    return 42
  }
$ hello ; echo $?
bonjour
42

Important :

  1. Ne pas confondre return avec exit !
    • return sort de la fonction,
    • exit sort du script (il termine le shell).
  2. Dans d'autres langages, return renvoie une valeur. Pas en bash !

9.2.2. Résultat par substitution de commandes

La "bonne" façon en bash pour qu'une fonction fournisse une valeur est par affichage + substitution de commande $() :

ma_fonction() # des arguments
{
    ...
    echo "le résultat"                  # affiche sur la sortie standard
}

resultat=$(ma_fonction arguments)       # récupère la sortie standard

La fonction ne doit afficher rien d'autre que le résultat sur la sortie standard ; si elle a besoin d'afficher d'autres choses, elle peut le faire par exemple sur la sortie d'erreurs.

Exemple :

calculer_division() # a b
{
    local a=$1 b=$2
    if ((b == 0)); then
        echo "Erreur : division par 0" >&2
        return 2
    fi
    echo "Calcul en cours ..." >&2          # Essayez sans >&2 pour voir !
    echo $((a/b))                           # affiche le résultat
}

r=$(calculer_division 17 3)                 # essayez avec 17 0
if (($? == 0)); then
    echo "Le résultat est $r"
fi

# On peut même écrire :
if r=$(calculer_division 17 3); then
    echo "Le résultat est $r"
fi

9.2.3. Récursivité

on dit qu'une fonction est récursive lorsqu'elle s'appelle elle-même.

Exemple : calcul de factorielle : \(n! = n*(n-1)*...*1\)

On utilise le fait que \(n! = n * (n-1)!\)

factorielle() # n
{
    local n=$1
    if ((n <= 1)); then     # cas terminal
        echo 1
        return
    fi
    echo $((n * $( factorielle $((n-1)) ) ))
}
$ factorielle 6
720
$ factorielle 26
-1569523520172457984        # dépassement de capacité

Si on fait beaucoup de calculs, il faut mieux utiliser des outils mieux adaptés (par exemple python).

La récursivité peut être très utile quand on parcourt des structures hiérarchiques, telles que des répertoires.

Exemple de recherche récursive de fichiers (au lieu de find) :

chercher_fichier () # rep nomf
{
    local rep="$1" nomf="$2" p

    for p in "$rep"/* ; do
        if test -d "$p" ; then
            chercher_fichier "$p" "$nomf"       # appel récursif
        fi
        if [[ "$p" == "$rep"/$nomf ]] ; then    # motif à droite ⟶ sans ""
            echo "$p"
        fi
    done
}

chercher_fichier /usr/share/gimp "gimp-*.png"

9.3. Documents en ligne

9.3.1. Here-document avec <<

Il s'agit d'une redirection spéciale sur l'entrée standard, qui permet d'embarquer un fichier dans le script :

<< mot_final                # déclaration du marqueur final
...                         # lignes à recopier
...                         # y compris les blancs et retours chariots
mot_final                   # marqueur final atteint

On choisit un marqueur de fin (mot_final), qu'on indique après << ; tout ce qui suit sera recopié sur l'entrée standard de la commande, jusqu'à la ligne "mot_final" (exclue).

Utilité :
affichage de texte, génération de fichiers (scripts, pages web, ...), automatisation de commandes interactives, ...

Exemple :

$ cat << FIN
La durée de cuisson d'un œuf dur
est de 9 minutes dans de l'eau bouillante.
FIN

Les espaces à gauches, s'il y en a, sont recopiés ; le mot_final doit être en début de ligne et tout seul.

Si on veut indenter dans un script le here-document et le mot final, il faut le faire avec des tabulations, et utiliser <<- :

    cat <<- FIN
	La durée de cuisson d'un œuf dur
	est de 9 minutes dans de l'eau bouillante.
	FIN

Si on veut rediriger la commande en sortie, il faut le faire avant << :

$ cat >| recette.txt << FIN
La durée de cuisson d'un œuf dur
est de 9 minutes dans de l'eau bouillante.
FIN

Les expansions sont réalisées dans le here-document :

$ cd /tmp
$ cat << STOP
Le répertoire courant est $PWD
La taille est $(du -sh "$PWD" 2> /dev/null | cut -f 1)
STOP

affiche :

Le répertoire courant est /tmp
La taille est 1,1M

sauf si le mot final est entre simples ou double-quotes (peu connu) :

$ cat << "STOP"
Le répertoire courant est $PWD
La taille est $(du -sh "$PWD" 2> /dev/null | cut -f 1)
STOP

affiche :

Le répertoire courant est $PWD
La taille est $(du -sh "$PWD" 2> /dev/null | cut -f 1)

9.3.2. Here-string avec <<<

On peut remplacer

commande << mot_final
...
mot_final

par

commande <<< "..."

C'est très pratique lorsque le texte à remplacer "..." ne fait qu'une ligne.

Exemple :

tr a-z A-Z << FIN
Le PID courant est $BASHPID
FIN

devient

tr a-z A-Z <<< "Le PID courant est $BASHPID"

Et cela remplace avantageusement des constructions telles que

echo "Le PID courant est $BASHPID" | tr a-z A-Z

9.3.3. Exemples classiques d'utilisation

a) Génération de script

Très utile ; ne pas oublier de protéger des substitutions si besoin.

On veut par exemple générer ce script :

#! /bin/bash
# Script généré le ...                  # suivi de la date de génération
echo "Il y a $# paramètres"             # en préservant "$#"
exit 0

On peut le générer avec ce here-document :

cat >| monscript.sh << FINSCRIPT
#! /bin/bash
# Script généré le $(date '+%A %d %B %Y')
echo "Il y a \$# paramètres"
exit 0
FINSCRIPT
Après exécution on obtient :

$ cat monscript.sh
#! /bin/bash
# Script généré le mardi 10 novembre 2020
echo "Il y a $# paramètres"
exit 0

$ chmod +x monscript.sh
$ ./monscript.sh ga bu
Il y a 2 paramètres
b) Génération de pages web statiques

Cela permet de visualiser des données dans un navigateur, de construire des petits sites web sans utiliser de frameworks compliqués...

On veut par exemple générer cette page webmapage.html, qui présente dans une liste non numérotée <ul>...</ul> un lien <a href="fichier">fichier</a> vers chaque fichier du répertoire :

<!DOCTYPE html>
<html lang="fr">
  <head>
    <title>Ma page web</title>
  </head>
  <body>
    <h1>Ma page web</h1>
    <p>Le répertoire /home/thiel/Bureau/tmp contient :</p>
    <ul>
      <li><a href="fichier1">fichier1</a></li>
      <li><a href="fichier2">fichier2</a></li>
      <li>...</li>
    </ul>
  </body>
</html>

On peut générer cette page avec plusieurs here-documents :

page="mapage.html"
titre="Ma page web"

# Première partie jusqu'à <ul> :
cat >| "$page" << FINPAGE
<!DOCTYPE html>
<html lang="fr">
  <head>
    <title>$titre</title>
  </head>
  <body>
    <h1>$titre</h1>
    <p>Le répertoire $(pwd) contient :</p>
    <ul>
FINPAGE

# affichage des liens pour chaque fichier du répertoire courant
for f in * ; do
    echo "      <li><a href=\"$f\">$f</a></li>"
done >> "$page"

# Deuxième partie de </ul> à la fin
cat >> "$page" << FINPAGE
    </ul>
  </body>
</html>
FINPAGE

On peut également générer toute la page en un seul here-document, en générant les liens (le for f in *) dans une substitution de commandes $(...) :

page="mapage.html"
titre="Ma page web"

cat >| "$page" << FINPAGE
<!DOCTYPE html>
<html lang="fr">
  <head>
    <title>$titre</title>
  </head>
  <body>
    <h1>$titre</h1>
    <p>Le répertoire $(pwd) contient :</p>
    <ul>
$(
  for f in * ; do
    echo "      <li><a href=\"$f\">$f</a></li>"
  done
)
    </ul>
  </body>
</html>
FINPAGE
c) Automatisation de saisie

On a parfois besoin d'automatiser la saisie pour des commandes interactives (ça ne marchera pas si elles vérifient que l'entrée standard est un terminal, par exemple pour la saisie d'un mot de passe).

Considérons le script interactif saisie_date.sh :

#! /bin/bash
echo -n "Jour ? " ; read jour
echo -n "mois ? " ; read mois
echo -n "An   ? " ; read an
echo "Date saisie : $jour/$mois/$an"

On peut l'appeler ainsi :

./saisie_date.sh << FIN
10
11
2020
FIN

Ou encore, en utilisant des variables :

j=10 ; m=11 ; a=2020

./saisie_date.sh << FIN
$j
$m
$a
FIN