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 seran
.
$ hello() {
echo "bonjour"
return 42
}
$ hello ; echo $?
bonjour
42
Important :
- Ne pas confondre
return
avecexit
!return
sort de la fonction,exit
sort du script (il termine le shell).
- 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
$ 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