Add bash messages article

This commit is contained in:
Yorick Barbanneau 2022-01-30 01:53:08 +01:00
parent 80eff1df92
commit 6c8b7307dc
2 changed files with 398 additions and 0 deletions

View file

@ -0,0 +1,398 @@
Title: Bash avancé: Gérer les messages
Category: sysadmin
Tags: bash, script, pl-fr
Date: 2022-01-30 1:30
Cover: assets/backgrounds/article_bash_messages.jpg
Dans ce premier article de l'année 2022, nous allons voir comment gérer les
messages de sorties de nos scripts Bash. L'idée ici est de proposer trois type
de messages dans un fichiers que nous pourrons ensuite inclure dans nos scripts
à l'aide de la commande `source`.
Ces messages seront de 3 types différents:
* **messages standards** envoyés sur la sortie standard
* **messages de débogage** affichés si une variable `DEBUG` est positionnée et
envoyés vers la sortie d'erreur
* **message d'erreur** envoyés sur la sortie d'erreur.
Nous utiliserons quelques spécificité de bash nous permettant d'agrémenter nos
sorties.
## Une première version
Cette première ébauche de cette librairie contient 3 fonctions répondant à la
demande formulée en introduction. Appelons ce fichier `messages.sh`.
```bash
#!/bin/env bash
msg() {
local message="$*"
# Si la fonction est appelée sans paramètres, on la quitte
[ -z "$message" ] && return
printf "%b\n" "$message"
}
debug() {
local message="$*"
# si la variable $DEBUG n'est pas définie ou si sa valeur
# est différente de 1, on quite notre fonction.
[ -z "$DEBUG" || $DEBUG -ne 1 ] && return
[ -z "$message" ] && return
>&2 msg "DEBUG: $message"
}
error() {
local message="$*"
[ -z "$message" ] && return
>&2 msg "ERROR: $message" }
```
Vous remarquez que les fonctions `error` et `debug` utilisent la fonction
`message` mais son appel est précédé de `>&2` afin que la sortie se fasse sur
`SRDERR`.
Le `printf` de notre fonction `msg` utilise `%b` pour afficher le contenu de la
`message`. Ainsi les séquences échappées par un antislash seront interprétées.
Nous aborderons le sujets dans la partie suivante.
Pour les tester, créons un script `test.sh` dans le même répertoire
que notre librairie avec le code suivant:
```bash
#!/usr/bin/env bash
# inclusion de notre librairie de message, il ne faut pas oublier
# d'afficher une erreur et teminer notre script si il y a un problème
# lors de son chargement.
source message.sh || { >&2 printf "Can't load message.sh"; exit 1; }
debug "We will display a message"
msg "Test Message"
debug "We will display an error"
error "This is an error"
exit 0
```
Pour tester il suffit de lancer la commande :
```shell
./test.sh
Test Message
ERROR: This is an error
```
Comme il n'y a pas de variable `$DEBUG` de définie, les message de débogage ne
sont pas affichés, pour tester ces messages il suffit de faire:
```none
$ DEBUG=1 ./test.sh
DEBUG: We will display a message
Test Message
DEBUG: We will display an error
ERROR: This is an error
```
## Ajouter un peu de couleur
Personnellement, **j'aime avoir un peu de couleur dans mon terminal**, les
choses apparaissent souvent plus claire. Pour nos messages, nous pouvons faire
de même.
La commande `printf` permet d'insérer des code couleur (entre autres), utilisons
le rouge pour les messages d'erreur (logique non?) et le bleu pour les messages
de débogages
```bash
debug() {
local message="$*"
[[ -z $DEBUG || $DEBUG -ne 1 ]] && return
[ -n "$message" ] && >&2 msg "\e[34mDEBUG: $message\e[0m"
}
error() {
local message="$*"
[ -n message ] && >&2 msg "\e[31mERROR: $message\e[0m"
}
```
Ici la commande `\e[34m` permet de choisir la couleur bleue et `\e[0m` de
revenir à la normale. C'est ici que le choix de `%b` pour le formatage de la
variable `$message` est important: **`printf` interprètera nos commande
échappées avec l'antislash**.
La couleur c'est bien, mais si on décide de rediriger une sortie (ou les deux)
de notre script dans un fichier voici son contenu ouvert dans `vim`:
```none
DEBUG: We will display a message
Test Message
DEBUG: We will display an error
ERROR: This is an error
```
Nous allons justement régler ce problème dans le paragraphe suivant.
## Sortie vers un fichier
Il est souvent nécessaire de **rediriger les sorties** d'un de nos script vers
un fichier. Surtout lorsqu'il est exécuté en dehors d'une session interactive --
dans une tâche *cron* par exemple.
Dans ce cas il peut être intéressant d'ajouter un horodatage en début de ligne
comme dans la plupart des applications qui effectuent de la journalisation. Bash
dispose d'un opérateur de test permettant de savoir si la sortie demandée est un
terminal interactif ou non: `-t`.
Le code prend un peu de poids :
* On ajoute une condition dans chacune de nos trois fonction d'origine afin de
tester le type de sortie.
* On y ajoute une fonction `log` qui se charge de traiter notre sortie
lorsque elle est redirigée vers un fichier en ajoutant une information de
date / heure au début de la ligne.
* Le format de cette date est paramétrable à l'aide de la variable `$DATE_FMT`,
dans l'exemple un *timestamp*.
Voici le nouveau code :
```bash
#!/usr/bin/env bash
DATE_FMT="+%s"
msg() {
local message="$*"
[ -z "$message" ] && return
if [ -t 1 ]
then
printf "%b\n" "$message"
else
log "$message"
fi
}
log() {
local message="$*"
[ -z "$message" ] && return
# On veux conserver les sauts de ligne et les tabulation du
# message, utilisons alors %b ...
printf "%s %b\n" "$(date $DATE_FMT)" "$message"
}
debug() {
local message="$*"
[[ -z $DEBUG || $DEBUG -ne 1 ]] && return
[ -z "$message" ] && return
message="DEBUG: $message"
if [ -t 2 ]
then
>&2 msg "\e[34m$message\e[0m"
else
>&2 log "$message"
fi
}
error() {
local message="$*"
[ -z message ] && return
message="ERROR: $message"
if [ -t 2 ]
then
>&2 msg "\e[31m$message\e[0m"
else
>&2 log $message
fi
}
```
## Améliorer les informations de débogage
Lors de la sortie d'information de débogage via notre fonction `debug`, il peut
être intéressant d'afficher des **informations supplémentaires** comme par
exemple la fonction en cours et le fichier source. Ces informations peuvent être
très précieuse surtout dans le cas d'un projet conséquent répartis sur plusieurs
fichiers sources.
Ces informations sont disponible via des variables spéciales de bash :
* `BASH_SOURCE`: un tableau reprenant **la pile des fichiers** scripts
utilisés. `$BASH_SOURCE[0]` représente le fichier source de la fonction
en cours.
* `FUNCNAME`: est un tableau reprenant la **liste des fonctions appelées**, en
quelque sorte notre pile d'appel. Cette variable est liée à la précédente,
`BASH_SOURCE[n]` représente la source de la fonction `FUNCNAME[n]` et
`FUNCNAME[0]` la fonction courante.
Dans notre fonction `debug`, `FUNCNAME[0]` correspond donc à `debug`, pour
retrouver la fonction appelante, il faut chercher du côté de `FUNCNAME[1]` (et
donc de `BASH_SOURCE[1]`).
Voici donc le nouveau code de notre fonction:
```bash
debug() {
local message="$*"
[[ -z $DEBUG || $DEBUG -ne 1 ]] && return
[ -z "$message" ] && return
# On affiche les informations supplémentaires pour le débogage
message="DEBUG [${BASH_SOURCE[1]}:${FUNCNAME[1]}]: $message"
if [ -t 2 ]
then
>&2 msg "\e[34m$message\e[0m"
else
>&2 log "$message"
fi
}
```
Afin de tester le fonctionnement de notre modification, nous allons inclure un
autre fichier bash dans notre script de test. Voici notre fichier `include.sh`:
```bash
#!/usr/bin/env bash
myfunct() {
local a=10
debug "my a variable is $a"
msg "value of 'a' squared $(( a * a ))"
}
```
Et modifions notre fichier `test.sh` afin d'inclure notre fichier comme
ci-dessous :
```bash
source message.sh || { >&2 printf "Can't load message.sh"; exit 1; }
source include.sh || { >&2 printf "Can't load include.sh"; exit 1; }
# Appel de notre fonction venue de include.sh
myfunct
debug "We will display a message"
msg "Test Message"
```
Et voici sa sortie:
```none
$ DEBUG=1 ./test.sh
DEBUG [include.sh:myfunct]: my a variable is 10
value of 'a' squared: 100
DEBUG [./test.sh:main]: We will display a message
Test Message
```
Le fichier source et la fonction appelée sont bien affichés. Bash dispose
d'autre variables utiles que vous trouverez dans l'aide : `man bash`.
## Améliorer les sorties d'erreur
Avec ce que nous venons de voir, nous pouvons maintenant améliorer la sortie
d'erreur. Pourquoi nous n'afficherions pas, lorsque le mode de débogage est
activé, une sorte de *stack trace*?
Pour ce faire, nous pouvons utiliser la variable `${#FUNCNAME[@]}` qui va nous
donner le nombre de fonctions appelées. Voici le code de notre nouvelle fonction
`error`:
```bash
error() {
local message="$*"
[ -z "$message" ] && return
message="ERROR: $message"
# Nous affichons notre "stack trace si le mode débogage est activé
if [[ -n $DEBUG && $DEBUG -eq 1 ]]
then
message="$message\n\tstack trace:\n"
# Il nous suffit pour ça de parcourir notre tableau FUNCNAME et
# d'afficher le BASH_SOURCE et BASH_LINENO correspondant
for (( i=1; i<${#FUNCNAME[@]}; i++ ))
do
message="${message}\t source:${BASH_SOURCE[i]}"
message="${message} function:${FUNCNAME[$i]}"
# Attention, il faut prendre ici la valeur de n-1 pour BASH_LINENO
message="${message} line:${BASH_LINENO[$i-1]}\n"
done
fi
if [ -t 2 ]
then
>&2 msg "\e[31m$message\e[0m"
else
>&2 log "$message"
fi
}
```
Pour tester notre nouvelle fonction, rajoutons le code suivant dans le fichier
`include.sh`:
```bash
check_file() {
if [ ! -f "monfichier.txt" ]
then
display_error "File monfichier.txt not found"
fi
}
display_error() {
error "$*"
}
```
Et enfin modifions notre fichier `test.sh` comme ci-dessous:
```bash
#!/usr/bin/env bash
source message.sh || { >&2 printf "Can't load message.sh"; exit 1; }
source include.sh || { >&2 printf "Can't load include.sh"; exit 1; }
myfunct
check_file
debug "We will display a message"
msg "Test Message"
error "This is a simple error message"
exit 0
```
Lors de l'exécution de notre script de test avec le mode débogage, nous pouvons
voir que tous les éléments demandés sont présent:
```none
$ DEBUG=1 ./test.sh
DEBUG [include.sh:myfunct]: my a variable is 10
value of 100
ERROR: File monfichier.txt not found
stack trace:
source:include.sh function:display_error line:18
source:include.sh function:check_file line:13
source:./test.sh function:main line:6
DEBUG [./test.sh:main]: We will display a message
Test Message
ERROR: This is a simple error message
stack trace:
source:./test.sh function:main line:9
```
## En conclusion
Nous avons vu tout au long de cet article comment utiliser des fonctions pour
afficher vos messages, que se soit pour déboguer, afficher des erreurs ou de
simples messages.
Pensez que vous pouvez placer votre "bibliothèque" dans un endroit pr esent dans
la variable d'environnement `$PATH` et ainsi l'inclure dans n'importe quel
script.
Les fichiers d'exemple sont disponibles [ici]({attach}files/scripts.tar.gz).