<!--
  canonical-url: https://www.avonture.be/fr/blog/bash-progression-bar
  published:     2024-10-07
  This is a static plain-Markdown mirror generated at build time.
  Visit the canonical URL above for the fully rendered page, with images and interactive components.
-->

# Linux - Utiliser une barre de progression dans vos scripts

> Ajoutez une barre de progression visuelle à vos scripts Bash sous Linux. Ce guide vous montre comment implémenter facilement une barre de progression pour suivre des jobs concurrents, visualiser l'avancement et améliorer votre interface dans le terminal.

---
![Linux - Utiliser une barre de progression dans vos scripts](/img/v2/linux_progress_bar.webp)

**TL;DR**

Cet article de suivi ajoute une barre de progression visuelle aux scripts Bash (dans la continuité de l'article précédent sur les jobs parallèles) à l'aide d'un helper réutilisable `progress_bar.sh` : sourcez-le, faites passer la sortie de votre boucle dans `progress_bar::process "Doing some stuff" 50`, et affichez `Progress=N` depuis l'intérieur de la boucle pour piloter la barre — pratique pour les traitements par lots longs comme des uploads vers une API pilotés par un CSV.

Dans mon article précédent, [Linux - Take advantage of the number of CPUs you have; start concurrent jobs](/blog/bash-parallel-task), nous avons vu comment lancer des jobs en parallèle.

La prochaine chose sympa, c'est d'afficher une barre de progression dans votre console. Ça présente plusieurs avantages : une vue claire de ce qui est fait et de ce qui reste à faire, et une interface agréable.

*Quand le nombre d'éléments n'est pas connu à l'avance, un compteur en direct est plus adapté ; voyez [Keep running and count the number of files in a folder using inotifywait](/blog/linux-inotifywait). Et pour garder une trace de ce qui s'est passé une fois la barre disparue, [Bash - Script to add logging features to your script](/blog/bash-logging).*

Il y a quelques mois, j'ai trouvé cet article de blog français dans mes flux RSS : <https://xieme-art.org/post/bash-avance-barre-de-progression/> et, juste, **wow !!!**

Amusons-nous avec ça.

## Résultat {#result}

![Barre de progression](./images/progression_bar.gif)

C'est une simple boucle Bash, sans aucun outil externe, qui pilote une barre de progression en direct dans la console.

## Pourquoi ça fonctionne {#why-it-works}

Un script doit faire trois choses pour obtenir cette barre :

1. Sourcer le script `progress_bar.sh` ([merci à ephase](https://xieme-art.org/post/bash-avance-barre-de-progression/)) ;
2. Lancer la fonction qui fait le vrai travail d'une manière particulière : pas juste `main` mais `main > >(progress_bar::process "Doing some stuff" 50)`. Le chiffre `50` doit correspondre au nombre d'itérations de la boucle ;
3. Depuis l'intérieur de cette fonction, afficher une seule chose dans la console avec `echo` : `Progress=` suivi d'un nombre (de 1 à 50) — c'est le signal que la barre lit pour avancer.

Pas si compliqué, non ?

## Créer demo.sh {#create-demosh}

D'abord, créez un nouveau dossier sur votre disque et déplacez-vous dedans : `mkdir /tmp/progress && cd $_`.

Ensuite, créez un fichier `demo.sh` dans ce dossier et copiez/collez le script bash ci-dessous :

**demo.sh**

```bash
#!/bin/bash
set -euo pipefail

function demo {
    echo "Progress=$1"
    sleep 0.1
    return 0
}

function main {
    for i in {1..50}; do
        demo "$i"
    done
}

source "progress_bar.sh"

main > >(progress_bar::process "Doing some stuff" 50)
```

Rendez le script exécutable en lançant `chmod +x ./demo.sh`.

## Créer progress\_bar.sh {#create-progress\_barsh}

Voici le contenu à copier/coller dans un fichier appelé `progress_bar.sh`, créé dans le même dossier que `demo.sh`.

Créez donc un nouveau fichier sur votre disque dur ; nommez-le `progress_bar.sh` et copiez/collez le contenu ci-dessous dedans.

**progress\_bar.sh**

````bash
#!/usr/bin/env bash

# region - Intro block -----------------------------------------------
#
# Progress bar
#
# ## How to use this script?
#
# Just include this script in your own Bash like this `source progress_bar.sh`
#
# Now, you can use any public functions defined here like `progress_bar::draw``.
#
# ## Example
#
# The example below will just loop for 1 till 50 and the most important part is the "Progress=${i}"
# echo statement. The progression of the bar depends on a regex defined in the progress_bar::process() function.
#
# ```bash
# function main() {
#     for i in {1..50}
#     do
#         echo "Progress=${i}"
#         sleep 0.1
#     done
# }
#
# main > >(progress_bar::process "Doing some stuff" 50)
# ```
#
# ## Namespace
#
# `progress_bar::`
#
# cspell:ignore endregion
#
# shellcheck disable=SC1090,SC1091
#
# ## Source
#
# The source comes from a French blog post:
# https://xieme-art.org/post/bash-avance-barre-de-progression/
#
# shellcheck disable=SC2059
#
# endregion - Intro block --------------------------------------------

# let script exit if an unset variable is used or if a command fails
set -o nounset
set -o errexit

# The different bar to display (done)
PROGRESS_BAR_CHAR=(█ ▒)

# Show or not the introduction text (f.i. "Doing some stuff")
PROGRESS_BAR_DISPLAY_INFO=1

PROGRESS_BAR_DISPLAY_COMP=1

# Display template
PROGRESS_BAR_INFO_TEMPLATE=' %s [ %3d/%3d ] '
PROGRESS_BAR_COMP_TEMPLATE=' %3d%% '
PROGRESS_BAR_TEMPLATE='\033[1m%s%s\033[0m%s%s\r'

# region - private function progress_bar::__change_column_size
#
# ## Description
#
# This function will capture de WINCH signal. That one is fired by Linux when
# the size of the window (i.e. the console) is updated so allow us to retrieve the
# max. number of columns that can fit in the window.
#
# endregion
function progress_bar::__change_column_size() {
    printf >&2 "%${COLUMNS}s" ""
    printf >&2 "\033[0K\r"
    COLUMNS=$(tput cols)
}

# region - private function progress_bar::__draw
#
# ## Description
#
# Draw the progress bar / progression
#
# endregion
function progress_bar::__draw() {
    # function parameters
    local -r progress=${1?"progress is mandatory"}
    local -r total=${2?"total elements is mandatory"}
    local -r info=${3:-"In progress"}

    # function local variables
    local progress_segment
    local todo_segment
    local info_segment=""
    local comp_segment=""

    if [[ ${PROGRESS_BAR_DISPLAY_INFO:-1} -eq 1 ]]; then
        printf -v info_segment "${PROGRESS_BAR_INFO_TEMPLATE}" \
            "$info" "$progress" "$total"
    fi

    local -r progress_ratio=$((progress * 100 / total))
    if [[ ${PROGRESS_BAR_DISPLAY_COMP:-0} -eq 1 ]]; then
        printf -v comp_segment "${PROGRESS_BAR_COMP_TEMPLATE}" \
            "$progress_ratio"
    fi

    # progress bar construction
    # calculate each element sizes, bar must fit in ou screen
    local -r bar_size=$((COLUMNS - ${#info_segment} - ${#comp_segment}))
    local -r progress_segment_size=$((bar_size * progress_ratio / 100))
    local -r todo_segment_size=$((bar_size - progress_segment_size))
    printf -v progress_segment "%${progress_segment_size}s" ""
    printf -v todo_segment "%${todo_segment_size}s" ""

    printf >&2 "$PROGRESS_BAR_TEMPLATE" \
        "$info_segment" \
        "${progress_segment// /${PROGRESS_BAR_CHAR[0]}}" \
        "${todo_segment// /${PROGRESS_BAR_CHAR[1]}}" \
        "$comp_segment"
}

# region - public function progress_bar::process
#
# ## Description
#
# Display a progress bar
#
# ## Example
#
# ```bash
# function main() {
#     for i in {1..50}
#     do
#         echo "Progress=${i}"
#         sleep 0.1
#     done
# }
#
# main > >(progress_bar::process "Doing some stuff" 50)
# ```
#
# ## Parameters
#
# * @arg $1 The text to display like "Upload in progress"
# * @arg $2 The number of positions in the bar (f.i. if there are 4 rows to proceed, it has to be initialize to 4)
#
# endregion
function progress_bar::process() {
    local -r action="${1:-Upload in progress}"
    local -r max=${2:-100}

    # Capture the resize of the window so the progress bar becomes responsive
    trap progress_bar::__change_column_size WINCH

    while read -r line; do
        # Each line with "Progress=(digit)" will increase the progress bar
        if [[ $line =~ Progress=([[:digit:]]{1,}) ]]; then
            progress_bar::__draw "${BASH_REMATCH[1]}" "${max}" "${action}"
        fi
    done
}

# This script didn't contains executable code; only helpers.
sourced=0
(return 0 2> /dev/null) && sourced=1 || sourced=0
if [ "${sourced}" -eq "0" ]; then
    printf "\033[1;31m%s\033[0m\n" "ERROR, the $0 script is meant to be sourced. Try 'source $0' and use public functions." >&2 # Write on stderr
    exit 1
fi

return 0
````

## Place à l'exécution {#time-to-run-the-code}

Retournez simplement dans la console, lancez `./demo.sh` et... wow, c'est vraiment chouette — vous obtenez la barre montrée plus haut.

Dans un cas réel, j'ai implémenté cette fonctionnalité dans un script Bash où je dois lire un fichier CSV et, pour chaque enregistrement récupéré dans le fichier, lancer une commande `curl` pour appeler une API RESTful en POST et uploader un PDF.

Et, quand le script est lancé de manière interactive, la sortie à l'écran est vraiment pratique et terriblement efficace.

Bien sûr, j'utilise aussi un fichier de log où je redirige un maximum d'informations.
