docs: add bash progress bar article
This commit is contained in:
parent
d73dd9d0fa
commit
97b9cb24ad
14 changed files with 1160 additions and 0 deletions
618
content/articles/2024/bash_printf/index.md
Normal file
618
content/articles/2024/bash_printf/index.md
Normal file
|
@ -0,0 +1,618 @@
|
|||
Title: Bash avancé: barre de progression
|
||||
Category: sysadmin
|
||||
Tags: bash, script, printf, substitution
|
||||
Date: 2024-06-20 0:04
|
||||
Cover: assets/backgrounds/turris_ssh_cle.jpg
|
||||
|
||||
Après [les
|
||||
messages]({filename}../../2022/bash_gerer_les_messages_avance/index.md) et [les
|
||||
signaux]({filename}../../2022/bash_les_pieges/index.md), voici enfin un nouvel
|
||||
article dans la série **Bash avancé**. Avec presque deux ans de retard, il
|
||||
serait temps me direz-vous! Mais mieux vaut tard que jamais non?
|
||||
|
||||
Cette article me servira de prétexte pour utiliser massivement la commande
|
||||
interne `printf` et vous montrer quelques cas d'usages. Nous verrons aussi
|
||||
la *substitution de processus*, la *substitution de paramètre* et d'autres
|
||||
mécanismes offerts par *Bash*.
|
||||
|
||||
## Un peu de théorie
|
||||
|
||||
Il faut d'abord définir ce que j'entends par *barre de progression*: un élément
|
||||
affiché dans notre terminal représentant **la progression** d'une tâche exécutée
|
||||
par un script. Voici un exemple:
|
||||
|
||||
```
|
||||
Task 2/10 ████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ 20%
|
||||
```
|
||||
|
||||
Cette barre se compose donc:
|
||||
|
||||
* d'une légende / titre;
|
||||
* de la barre de progression représentant graphiquement l'avancée de notre
|
||||
tâche;
|
||||
* de la progression en pourcentage.
|
||||
|
||||
### Découpage en segments
|
||||
|
||||
Afin de préparer notre implémentation, découpons notre barre de chargement en
|
||||
segments. Ce découpage nous permettra de faciliter le passage au code.
|
||||
|
||||

|
||||
|
||||
Voici la légende:
|
||||
|
||||
1. segment d'informations pour la légende;
|
||||
2. segment d'actions effectuées;
|
||||
3. segment d'actions en attentes;
|
||||
4. segment complémentaire qui peut être le pourcentage de progression, ou toute
|
||||
autre information annexe.
|
||||
|
||||
## Une commande comme base
|
||||
|
||||
Pour concevoir notre barre de progression il nous faut analyser la sortie d'une
|
||||
commande. Pour notre tutoriel, je vous propose d'utiliser la commande `ping`.
|
||||
|
||||
```
|
||||
ping -c4 aquilenet.fr
|
||||
PING aquilenet.fr (185.233.100.8) 56(84) bytes of data.
|
||||
64 bytes from hestia.aquilenet.fr (185.233.100.8): icmp_seq=1 ttl=60 time=29.1 ms
|
||||
64 bytes from hestia.aquilenet.fr (185.233.100.8): icmp_seq=2 ttl=60 time=56.3 ms
|
||||
64 bytes from hestia.aquilenet.fr (185.233.100.8): icmp_seq=3 ttl=60 time=52.8 ms
|
||||
64 bytes from hestia.aquilenet.fr (185.233.100.8): icmp_seq=4 ttl=60 time=29.1 ms
|
||||
|
||||
--- aquilenet.fr ping statistics ---
|
||||
4 packets transmitted, 4 received, 0% packet loss, time 3003ms
|
||||
rtt min/avg/max/mdev = 29.055/41.806/56.258/12.764 ms
|
||||
```
|
||||
|
||||
Ci-dessus, la commande a été lancée en indiquant le nombre de requêtes à envoyer
|
||||
via le paramètre `-c4` et chacune de ces requêtes affiche le numéro d'ordre
|
||||
(`icmp_seq=`).
|
||||
|
||||
Maintenant que nous savons où aller, il est temps de commencer à écrire notre
|
||||
script:
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script1_ping.sh !}
|
||||
```
|
||||
|
||||
[Télécharger le script]({attach}./files/script1_ping.sh)
|
||||
|
||||
Il se compose d'une fonction `command` qui nécessite deux paramètres : un hôte
|
||||
et un nombre d'itérations. Attardons-nous un instant sur deux points importants.
|
||||
|
||||
* Dans la mesure du possible il faut utiliser des variables locales dans les
|
||||
fonctions et en lecture seule pour s'arrurer que la variable ne sera pas
|
||||
modifiée. Exemple: `local -r mavariable` déclare la variable locale
|
||||
`mavariable` en lecture seule avec le paramètre `-r`.
|
||||
* La commande et ses arguments sont enregistrés dans un tableau. Avec cette
|
||||
méthodem plus besoin de gérer avec le gobbing et l'interprétation des
|
||||
paramètres lors de son appel.
|
||||
|
||||
Lors de son lancement, nous pouvons observer l'affichage des différentes
|
||||
requêtes:
|
||||
|
||||
```text
|
||||
./bash script1_ping.sh
|
||||
PING aquilenet.fr (185.233.100.8) 56(84) bytes of data.
|
||||
64 bytes from hestia.aquilenet.fr (185.233.100.8): icmp_seq=1 ttl=60 time=29.1 ms
|
||||
64 bytes from hestia.aquilenet.fr (185.233.100.8): icmp_seq=2 ttl=60 time=56.3 ms
|
||||
# [...]
|
||||
```
|
||||
|
||||
## Capturer les messages de sortie
|
||||
|
||||
Il faut maintenant capturer la sortie de la commande ping et en extraire
|
||||
l'information pertinente : **le numéro de séquence**. [Télécharger le script
|
||||
complet]({attach}./files/script2_getoutput.sh)
|
||||
|
||||
### Récupérer la sortie standard ...
|
||||
|
||||
Il est tout à fait possible de rediriger la sortie de la fonction `command` vers
|
||||
une autre fonction qui traitera le flux de données. C'est exactement le même
|
||||
principe utilisé par l'enchaînement de commande avec un *pipe* comme dans
|
||||
`dmesg | grep 'failure'`. Plusieurs techniques existent pour faire ceci en
|
||||
*Bash*, ici nous allons utiliser la **substitution de processus**.
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script2_getoutput.sh!lines=13-21 }
|
||||
```
|
||||
|
||||
Ici la sortie standard de notre fonction `command` est envoyée vers la fonction
|
||||
`parse_output`.
|
||||
|
||||
Notez que cette substitution peut aussi se faire dans l'autre sens avec la
|
||||
notation `<(commande)`, la sortie standard de la commande substituée sera
|
||||
envoyée vers l'entrée de la commande à gauche comme par exemple:
|
||||
|
||||
```bash
|
||||
# Affiche les éléments trouvés par la commande `ls` en les préfixants de
|
||||
# 'éléments: ', la sortie de notre substitution (ls) sera traitée par la boucle
|
||||
while read -r line; do
|
||||
printf "élément: %s\n" "$line"
|
||||
done < <(ls)
|
||||
```
|
||||
|
||||
Sous le capot, le flux entre la sortie standard de `command` et l'entrée de
|
||||
`parse_output` se fait grâce à un descripteur de fichier.
|
||||
|
||||
### ... et la traiter
|
||||
|
||||
Pour traiter le flux, `parse_output` place chaque ligne qui arrive dans la
|
||||
variable `$line` grâce à la bocle `while` couplée à la commande `read`.
|
||||
|
||||
Ensuite `$line` est affichée en y ajoutant `out:`. Voici ce que donne
|
||||
l'exécution de ce script:
|
||||
|
||||
```text
|
||||
bash script2_getoutput.sh
|
||||
out: PING aquilenet.fr (2a0c:e300::8) 56 data bytes
|
||||
out: 64 bytes from hestia.aquilenet.fr (2a0c:e300::8): icmp_seq=1 ttl=55 time=20.0 ms
|
||||
out: 64 bytes from hestia.aquilenet.fr (2a0c:e300::8): icmp_seq=2 ttl=55 time=19.8 ms
|
||||
[...]
|
||||
```
|
||||
|
||||
## Récupérer le numéro de séquence
|
||||
|
||||
Maintenant que le flux arrive dans `parse_output`, essayons de récupérer le
|
||||
numéro de séquence. Nous pourrons alors déterminer l'état d'avancement de notre
|
||||
commande. [Télécharger le script complet]({attach}./files/script3_rematch.sh)
|
||||
|
||||
Pour l'extraire du reste de la ligne, utilisons l'opérateur de comparaison `=~`
|
||||
qui permet de vérifier la présence d'un motif dans une chaîne de caractères via
|
||||
**une expression rationnelle** comme par exemple:
|
||||
|
||||
```bash
|
||||
if [[ "$nom" =~ ^(Y|y)orick$ ]]; then
|
||||
printf "oui c'est moi!\n"
|
||||
fi
|
||||
```
|
||||
|
||||
Il est possible de récupérer le motif capturé entre parenthèses ayant "matché"
|
||||
grâce à la variable `$BASH_REMATCH`, utilisée dans `parse_output`:
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script3_rematch.sh!lines=13-22 }
|
||||
```
|
||||
|
||||
En plus du numéro de séquence, le temps est aussi capturé afin d'illustrer le
|
||||
fonctionnement de la variable `$BASH_REMATCH` qui est en fait **un tableau**:
|
||||
|
||||
* l'indice `0` permet de récupérer entièrement la chaîne qui correspond au
|
||||
motif;
|
||||
* l'indice `1` au premier élément capturé, ici le numéro de séquence qui se
|
||||
trouve après `icmp_seq=`
|
||||
* et l'indice `2` à tout ce qui se trouve après `time=`.
|
||||
|
||||
Voici ce que donne l'exécution de ce script:
|
||||
|
||||
```text
|
||||
bash script3_rematch.sh
|
||||
séquence: 2 sur 16 temps:20.2 ms
|
||||
séquence: 3 sur 16 temps:20.5 ms
|
||||
séquence: 4 sur 16 temps:19.7 ms
|
||||
[...]
|
||||
```
|
||||
|
||||
Nous avons maintenant tout les éléments utiles pour la construction de notre
|
||||
barre de progression.
|
||||
|
||||
## Première barre de chargement
|
||||
|
||||
Il faut commencer doucement, par une barre de chargement aussi simple que
|
||||
possible, sans fioriture. Nous allons ajouté la fonction `draw_progressbar` qui
|
||||
se charge du dessin à l'écran. [Télécharger le script
|
||||
complet]({attach}./files/script4_progress.sh)
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script4_progress.sh!lines=14-26 }
|
||||
```
|
||||
|
||||
`draw_progressbar` attend deux paramètres: le nombre total d'éléments et celui
|
||||
en cours. Cette fonction est appelée depuis `parse_output` et elle utilise
|
||||
`printf` pour dessiner les deux segments nécessaires (mais pas que...).
|
||||
|
||||
### `printf` vers une variable
|
||||
|
||||
Ne nous attardons pas pas sur les deux premières variables locales de notre
|
||||
fonction `draw_progressbar`. Les deux suivantes -- `progress_segment` et
|
||||
`todo_segment` sont par contre plus intéressantes.
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script4_progress.sh!lines=42 17-21 42 }
|
||||
```
|
||||
|
||||
`printf -v progress_segment` permet **d'affecter le résultat de la commande
|
||||
`printf`** à la variable locale `progress_segment` au lieu de l'afficher sur la
|
||||
sortie standard.
|
||||
|
||||
#### `printf`: format, arguments, spécification
|
||||
|
||||
Le premier argument d'une commande `printf` est le *format*, les suivants
|
||||
sont les *paramètres*. Les arguments sont affichés par le format par
|
||||
l'intermédiaire de *spécifications*.
|
||||
|
||||
```bash
|
||||
number=2
|
||||
drink="water"
|
||||
printf "I have %d bottle of %s.\n" "$number" "$drink"
|
||||
```
|
||||
|
||||
Dans l'exemple suivant:
|
||||
|
||||
* `"I have %d bottle of %s.\n"` est le format,
|
||||
* `$number` et `$drink` sont les arguments
|
||||
* `%d` et `%s` sont des spécifications:
|
||||
* `%d` indique que le premier argument doit être affiché comme
|
||||
un nombre entier;
|
||||
* `%s` indique que le second argument doit être affiché comme une chaîne
|
||||
de caractère.
|
||||
|
||||
Il est possible de définit la taille **minimale d'une spécification**, les
|
||||
caractères manquants seront remplacés par des espaces, comme par exemple:
|
||||
|
||||
```text
|
||||
printf "bonjour %10s, bienvenue\n" "monsieur" "madame" "Linus Torvalds"
|
||||
bonjour monsieur, bienvenue
|
||||
bonjour madame, bienvenue
|
||||
bonjour Linus Torvalds, bienvenue
|
||||
```
|
||||
|
||||
Vous remarquerez que la taille de 10 caractères a bien été réservée pour les
|
||||
deux premiers éléments. Le dernier par contre contient plus de 10 caractères: il
|
||||
dépasse de cet espace.
|
||||
|
||||
Vous remarquez aussi une caractéristique particulière de `printf`. Une seule
|
||||
spécification est utilisée pour trois arguments, le format est donc répété trois
|
||||
fois.
|
||||
|
||||
#### Et dans notre script...
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script4_progress.sh!lines=42 20 21 }
|
||||
```
|
||||
|
||||
Dans le script la taille des deux segments est définie:
|
||||
|
||||
* par **la variable** `progress` pour le *segment des actions effectuées*
|
||||
* par le résultat d'une soustraction pour le *segment des actions en attente*.
|
||||
|
||||
*Bash* réalise les expansions avant de passer dans la commande `printf`,
|
||||
pratique n'est ce pas!
|
||||
|
||||
Pour ces deux `printf` le formats a une spécification avec une taille minimale
|
||||
égale à la taille du segment que nous voulons obtenir (car les arguments sont
|
||||
vides). Nous obtenons **deux variables qui contiennent un nombre d'espace égal
|
||||
à la taille des segments représentés**, il ne reste plus qu'à assembler.
|
||||
|
||||
### On passe au dressage
|
||||
|
||||
La commande `printf` suivante sert à afficher effectivement la barre de
|
||||
progression. D'abord elle est redirigée sur `stderr`, cette sortie est utilisée
|
||||
pour les erreurs, mais aussi pour afficher les informations de diagnostic.
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script4_progress.sh!lines=42 22-24 }
|
||||
```
|
||||
|
||||
Son format contient deux spécifications de type chaîne de caractère et **un
|
||||
retour chariot**. Ce dernier permet au curseur de revenir au début de la ligne
|
||||
une fois dessinée. Lors du prochain passage dans la fonction `draw_progressbar`:
|
||||
la barre sera alors réécrite, **pas besoin de gérer l'effacement de la ligne**.
|
||||
|
||||
Pour les deux arguments, nous utilisons *l'expansion de paramètre* afin de
|
||||
remplacer les espaces par les caractères choisis pour symboliser les deux
|
||||
segments de notre barre.
|
||||
|
||||
Comment fonctionne le remplacement de motif avec *l'expansion de paramètre*?
|
||||
Voici deux exemples:
|
||||
|
||||
```bash
|
||||
variable="voici une plante, jolie plante non?"
|
||||
|
||||
# remplacer plante par fleur dans `variable`
|
||||
# remplace la première occurrence trouvée
|
||||
echo "${variable/plante/fleur}"
|
||||
# affiche "voici une fleur, joli plante non?"
|
||||
|
||||
#remplace toutes les occurrences
|
||||
echo "${variable//plante/fleur}"
|
||||
# affiche "voici une fleur, joli fleur non?"
|
||||
```
|
||||
|
||||
Les caractères choisis pour remplacer les espaces sont affecté au tableau
|
||||
`PROGRESS_BAR_CHAR` initialisé en tout début de script.
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script4_progress.sh!lines=4 }
|
||||
```
|
||||
|
||||
C'est la fonction `parse_output` qui fait appel à `draw_progressbar`
|
||||
|
||||
```bash
|
||||
{! content/articles/2024/bash_printf/files/script4_progress.sh!lines=28-34 }
|
||||
```
|
||||
|
||||
### Exécution
|
||||
|
||||
Maintenant que vous avez compris comment le script est construit, il est temps
|
||||
de passer à l'exécution:
|
||||
|
||||
```
|
||||
bash script4_progress.sh
|
||||
█████▒▒▒▒▒▒▒▒▒▒▒
|
||||
```
|
||||
|
||||
Notre barre de progression fonctionne, mais elle ne prend qu'une petite part de
|
||||
l'écran (les 16 caractères qui correspondent aux nombres de requêtes totales).
|
||||
Mais surtout, **aucune information n'est affichée**.
|
||||
|
||||
## Ajouter des informations
|
||||
|
||||
Elle est bien belle notre barre de progression, mais là on ne sais pas du tout
|
||||
ce que fait le script. L'angle d'attaque: modifier les fonctions `parse_output`
|
||||
et `draw_progressbar` pour ajouter des informations sur la sortie standard.
|
||||
[Télécharger le script complet]({attach}./files/script5_informations.sh)
|
||||
|
||||
### La fonction `parse_output`
|
||||
|
||||
Elle contient 2 conditions de plus qui vérifient la ligne en cours envoyée par
|
||||
`command`.
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script5_informations.sh!lines=30-46}
|
||||
```
|
||||
|
||||
#### Afficher des information en introduction ...
|
||||
|
||||
La première condition ajoutée récupère, dans la sortie de la commande `ping`,
|
||||
la ligne qui commence par `PING` comme dans l'exemple ci-dessous:
|
||||
|
||||
```
|
||||
PING aquilenet.fr (2a0c:e300::8) 56 data bytes
|
||||
```
|
||||
|
||||
De cette ligne est récupérée **le nom d'hôte** et **l'adresse IP associée** afin
|
||||
de l'afficher avant notre barre de progression.
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script5_informations.sh!lines=37}
|
||||
```
|
||||
|
||||
L'information utile est capturée et réutilisée via la variable `BASH_REMATCH`.
|
||||
|
||||
#### ... une conclusion ...
|
||||
|
||||
La seconde condition ajoutée permet de récupérer la ligne qui contient le
|
||||
récapitulatif de ce qui a été fait par commande `ping` et de la renvoyer tel
|
||||
quelle sur la sortie standard.
|
||||
|
||||
#### ... et ajouter le segment d'information
|
||||
|
||||
Enfin un paramètre de plus est passé à la fonction `draw_progressbar`: une
|
||||
chaîne de caractère pour le *segment d'information* contenant la dernière mesure
|
||||
de temps retournée par `ping` (histoire d'utiliser `${BASH_REMATCH[2]}`)
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script5_informations.sh!lines=32-36}
|
||||
```
|
||||
|
||||
### la fonction `draw_progress`
|
||||
|
||||
Le troisième paramètre passé à cette fonction est affecté à la variable
|
||||
`info_segment`. Son contenu est ajouté à l'affichage avant la barre de
|
||||
chargement.
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script5_informations.sh!lines=14-28}
|
||||
```
|
||||
|
||||
### lancement de notre script
|
||||
|
||||
Notre barre de progression est toujours là mais avec un peu de contexte.
|
||||
|
||||
```text
|
||||
bash script5_informations.sh
|
||||
Launch ping command to aquilenet.fr (2a0c:e300::8) with 16 iterations
|
||||
Ping in progress (time: 20.7 ms) ███████▒▒▒▒▒▒▒▒▒
|
||||
```
|
||||
|
||||
Et une fois terminé la barre de progression disparait et laisse place au
|
||||
récapitulatif:
|
||||
|
||||
```text
|
||||
bash script5_informations.sh
|
||||
Launch ping command to aquilenet.fr (2a0c:e300::8) with 16 iterations
|
||||
16 packets transmitted, 16 received, 0% packet loss, time 15019ms
|
||||
```
|
||||
|
||||
## Responsive design inside
|
||||
|
||||
Maintenant que l'affichage des informations est en place, attardons nous sur le
|
||||
côté *responsive design* en adaptant la taille de notre barre à la largeur de
|
||||
l'écran.[Télécharger le script complet]({attach}./files/script2_getoutput.sh)
|
||||
|
||||
La première question est bien évidement comment obtenir la largeur de la fenêtre
|
||||
de terminal? Lorque *Bash* est lancé en **mode interactif** cette largeur --
|
||||
exprimée en nombre de colonnes [^colonne] -- est disponible via la variable
|
||||
`$COLUMNS`. Mais dans le cadre d'un script, cette variable n'est pas disponible
|
||||
si vous n'utilisez pas *Bash* comme interpréteur de commande.
|
||||
|
||||
Il est possible d'utiliser la command interne `shopt` pour activer l'option
|
||||
`checkwinsize` mais elle pose deux problèmes:
|
||||
|
||||
1. Je n'ai pas réussi à activer l'option avec la commande
|
||||
`shopt -s checkwinsize` dans mes tests (mais un simple `shopt` rend les
|
||||
variables `COLUMNS` et `LINES` disponible);
|
||||
2. la récupération des ces deux variables ne se fait **qu'après exécution d'une
|
||||
commande externe**, or il y en a peu dans notre script (`ping`), ce qui
|
||||
posera problème un peu plus loin dans cet article.
|
||||
|
||||
[^colonne]: une colonne correspond à la largeur d'un caractère
|
||||
|
||||
la commande `tput`, qui permet de récupérer des informations sur le terminal,
|
||||
fait très bien le job comme vous pouvez le voir dans ce petit bout de code
|
||||
ajouté dans la fonction `main()`:
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script6_responsive.sh!lines=54-57}
|
||||
```
|
||||
|
||||
### le dessin de la barre de progression
|
||||
|
||||
Maintenant que nous avons le nombre de colonnes, passons au dessin de notre
|
||||
barre de progression. Voici la nouvelle version de `draw_progressbar`:
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script6_responsive.sh!lines=14-34}
|
||||
```
|
||||
|
||||
Afin de déterminer la place disponible pour la barre de chargement (`$bar_size`)
|
||||
-- qui sera la partie responsive de notre ligne -- il faut soustaire la taille
|
||||
de notre segment d'information (`$info_segment`) à la largeur (`COLUMNS`).
|
||||
|
||||
La progression doit maintenant s'exprimer par le ratio (`$progress_ratio`) entre
|
||||
les éléments traités (`progress`) et le nombre total d'éléments (`total`).
|
||||
|
||||
L'expansion arithmétique de ***Bash* ne prend pas en charge les nombres
|
||||
flottants**, alors ce ratio s'exprime en pourcentage qui sera arrondi à
|
||||
l'entier le plus proche.
|
||||
|
||||
Il suffit ensuite de calculer le nombre de colonnes occupées par le *segment
|
||||
d'actions effectuées* (`$progress_segment_size`) avec les deux valeurs obtenues
|
||||
précédemment (`$bar_size` et `$progress_ratio`).
|
||||
|
||||
Pour obtenir le nombre de colonnes occupées par le *segment d'actions en
|
||||
attentes*. il suffit de soustraire la taille du segment d'actions effectuées
|
||||
(`$progress_segment_size`) à la taille de la barre (`$bar_size`).
|
||||
|
||||
La suite est simple, dans les deux instructions `printf -v` qui permettent
|
||||
d'affecter le bon nombre d'espaces, il suffit d'utiliser `progress_segment_size`
|
||||
et `todo_segment_size` et le tour est joué.
|
||||
|
||||
### Exécution du script
|
||||
|
||||
Il est temps de passer à l'exécution de cette version de notre script:
|
||||
|
||||
```
|
||||
bash script6_responsive.sh
|
||||
Launch ping command to aquilenet.fr (2a0c:e300::8) with 16 iterations
|
||||
Ping in progress (time: 23.1 ms) █████████████████████████████▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
|
||||
```
|
||||
|
||||
La ligne prend maintenant **toute la place disponible**, la barre de progression
|
||||
ne fait plus juste 16 colonnes. Mais un problème subsiste sur cette version.
|
||||
|
||||
Changer la taille de la fenêtre de terminal ne change pas la taille de la barre.
|
||||
Si agrandir la fenêtre ne pose pas trop de problèmes -- la barre ne prend pas
|
||||
toute la place disponible -- la réduction de la taille mène à ceci:
|
||||
|
||||

|
||||
|
||||
L'explication est simple: la valeur de `$COLUMNS` n'est pas rafraichie! Il
|
||||
serait tout à fait possible de récupérer le nombre de colonnes au début de la
|
||||
fonction `draw_progress`, mais ce ne serait pas très optimisé pour une
|
||||
fonctionnalité utilisée rarement. **Une meilleure solution existe...** (mais
|
||||
elle n'est pas si simple).
|
||||
|
||||
## SIGWINCH à la rescousse
|
||||
|
||||
Figurez-vous qu'un signal existe pour signifier un changement de taille d'une
|
||||
fenêtre de terminal. Mais l'implémentation dans notre script est plus épineuse
|
||||
qu'il n'y parait et nécessite quelques changements sur des fonctions d'affichage
|
||||
**pour que notre code soit compatible avec le plus d'émulateurs de terminal**
|
||||
possibles. [Télécharger le script complet]({attach}./files/script7_sigwinch.sh)
|
||||
|
||||
D'abord ajoutons la fonction `change_column_size` qui se charge d'appliquer le
|
||||
changement du nombre de colonnes de la fenêtre de terminal.
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script7_sigwinch.sh!lines=56-61}
|
||||
```
|
||||
|
||||
Cette fonction se charge:
|
||||
|
||||
1. de remplir la ligne courante sur `stderr` d'espace pour l'effacer;
|
||||
2. ensuite d'effacer les caractères de la ligne entière grâce à la séquence
|
||||
`\033[0K\r` passée à `printf`;
|
||||
3. enfin de récupérer le nouveau nombre de colonnes.
|
||||
|
||||
Elle sera exécutée dès le changement de géométrie de la fenêtre de terminal
|
||||
(c'est ici que `SIGWINCH` intervient). Cependant une petite subtilité se cache
|
||||
dans la gestion des signaux en *Bash*: ils ne sont pas transmis au *sub-shell*
|
||||
et notre fonction `parse_output` est justement exécuté comme tel via la
|
||||
substitution de processus.
|
||||
|
||||
Pour notre la capture de notre signal `SIGWICH`, il est alors préférable d'ajouter la commande `trap` au début de notre fonction `parse_output`. Si vous voulez plus d'information sir les signaux en Bash, je vous coneille la lecture de mon
|
||||
[précédent article]({filename}../../2022/bash_les_pieges/index.md).
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script7_sigwinch.sh!lines=37-40}
|
||||
```
|
||||
### Exécution du script
|
||||
|
||||
Notre barre de progression se redimensionne correctement, mais deux problèmes
|
||||
persistent.
|
||||
|
||||
1. Il faut attendre le rafraichissement de la barre de progression pour qu'elle
|
||||
prenne la bonne dimension une fois la fenêtre redimensionnée.
|
||||
2. La diminution de la taille de la fenêtre de terminal crée des lignes "vides"
|
||||
|
||||

|
||||
|
||||
Un programme avec une véritable TUI [^TUI] utilise une boucle et des buffers
|
||||
pour gérer spécifiquement le rafraichissement de l'affichage. Dans le cadre d'un
|
||||
script et de cet article, nous garderons ces deux bugs mineurs et nous en
|
||||
resterons là.
|
||||
|
||||
[^TUI]: Terminal User Interface -- interface utilisateur dans le terminal
|
||||
|
||||
## Bonus stage: un script plus aboutit
|
||||
|
||||
Maintenant que tous nous avons une barre de progression fonctionnelle, il est
|
||||
temps de l'améliorer pour la rendre réutilisable et paramétrable -- et de mettre
|
||||
en place le segment manquant. [Télécharger le script complet]({attach}./files/script8_bonus.sh)
|
||||
|
||||
D'abord il est possible d'ajouter des variables globales pour paramétrer les
|
||||
différents segments de la fonction `draw_progressbar`,
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script8_bonus.sh!lines=5-18}
|
||||
```
|
||||
|
||||
Les variables finissant par `_TEMPLATE` servent de *templates* aux segments
|
||||
d'information et complémentaires. Elles contiennent des définitions de
|
||||
**formats** pour `printf` qui sont utilisée dans la fonction d'affichage comme
|
||||
par exemple la mise en gras du segment d'information.
|
||||
|
||||
```bash
|
||||
{!content/articles/2024/bash_printf/files/script8_bonus.sh!lines=28 2 40-49 2 59-63}
|
||||
```
|
||||
|
||||
Dans `draw_progressbar`, les affichages du segment *d'information* et celui
|
||||
*complémentaire* sont conditionnels. Ensuite les formats des différentes
|
||||
commandes `printf` sont aussi donnés par les variables globales définies plus
|
||||
haut.
|
||||
|
||||
Nous avons maintenant une fonction `draw_progressbar` et les variables associées
|
||||
nous permettant de personnaliser son apparence en fonction du contexte.
|
||||
|
||||
## en conclusion
|
||||
|
||||
Au final, voici ce que que donne notre barre de progression:
|
||||
|
||||

|
||||
|
||||
Nous avons abordé beaucoup de choses dans cet article un peu plus long que
|
||||
d'habitude. Principalement autour de la commande `printf` et ses nombreuses
|
||||
possibilités (mais nous n'avons pas tout vu...).
|
||||
|
||||
J'espère vous avoir montré que par rapport à la commande `echo` habituellement
|
||||
utilisées dans les script, `printf` est réellement bien plus **puissante** et
|
||||
**utile**.
|
||||
|
||||
Merci à [Heuzef][heuzef], Yishan et [RavenRamirez][alois] pour les nombreuse
|
||||
relectures, corrections et conseils.
|
||||
|
||||
[heuzef]:https://heuzef.com/
|
||||
[alois]:https://aloisbouny.fr/
|
Loading…
Add table
Add a link
Reference in a new issue