Add bash messages article
This commit is contained in:
parent
80eff1df92
commit
6c8b7307dc
2 changed files with 398 additions and 0 deletions
Binary file not shown.
398
content/articles/2022/bash_gerer_les_messages_avance/index.md
Normal file
398
content/articles/2022/bash_gerer_les_messages_avance/index.md
Normal 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
|
||||
[34mDEBUG: We will display a message[0m
|
||||
Test Message
|
||||
[34mDEBUG: We will display an error[0m
|
||||
[31mERROR: This is an error[0m
|
||||
```
|
||||
|
||||
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).
|
Loading…
Add table
Add a link
Reference in a new issue