Title: Bash avancé: Gérer les messages Category: sysadmin Tags: bash, script, pl-fr Date: 2022-01-30 1:30 Modified: 2022-01-30 13:56 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 types de messages dans un fichier 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 **la sortie d'erreur**. Le `printf` de notre fonction `msg` utilise `%b` pour afficher le contenu de la variable `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 s'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 # \e[34m permet de choisit la couleur bleu pour afficher notre message # \e[0m permet de revenir à la normale [ -n "$message" ] && >&2 msg "\e[34mDEBUG: $message\e[0m" } error() { local message="$*" # \e[31m permet de choisir le rouge [ -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 dans notre fonction `msg`: **`printf` interprètera nos commandes é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=1 ./test.sh >error.txt 2>&1 $ vim error.txt 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`. Il est aussi intéressant de ne pas inclure les informations de couleur lors de la redirection de la sortie vers un fichier, le problème évoqué dans la partie précédente est réglé. Le code prend un peu de poids : * On ajoute une condition dans chacune de nos trois fonctions 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*](l_w_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 de *"sourcer"* 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ésent 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). [l_w_timestamp]:https://fr.wikipedia.org/wiki/Heure_Unix