Redirections et pipes#
La puissance du shell Unix repose sur un concept remarquablement simple : les programmes ne connaissent pas la source de leurs données ni la destination de leurs résultats. Ils lisent sur un canal appelé entrée standard et écrivent sur deux canaux appelés sortie standard et sortie d’erreur. Le shell peut rediriger ces canaux vers des fichiers ou les connecter entre programmes à l’aide de pipes. Ce mécanisme, conçu dans les années 1970 par Douglas McIlroy aux Bell Labs, reste aujourd’hui l’un des fondements les plus élégants et les plus puissants de l’informatique en ligne de commande.
Les trois flux standard#
Chaque processus Unix hérite à sa création de trois descripteurs de fichiers ouverts, numérotés de 0 à 2 :
Définition 17 (Les trois flux standard)
Les trois flux standard sont des canaux de communication prédéfinis associés à tout processus :
stdin (descripteur 0) — Standard Input : le canal par lequel le processus lit ses données. Par défaut, il est connecté au clavier. Toute commande qui « attend » que vous tapiez quelque chose lit sur stdin.
stdout (descripteur 1) — Standard Output : le canal sur lequel le processus écrit ses résultats. Par défaut, il est connecté au terminal. C’est ce que l’on voit normalement s’afficher.
stderr (descripteur 2) — Standard Error : le canal réservé aux messages d’erreur et de diagnostic. Par défaut, lui aussi connecté au terminal, mais indépendant de stdout. Cette séparation est fondamentale : elle permet de rediriger les erreurs séparément des résultats.
Cette séparation entre stdout et stderr est un choix de conception délibéré. Elle permet par exemple d’écrire les résultats dans un fichier tout en voyant les erreurs s’afficher dans le terminal, ou à l’inverse de rediriger les erreurs dans un fichier de log sans polluer la sortie principale.
Remarque 18
Le système de descripteurs de fichiers est bien plus général : chaque processus peut ouvrir jusqu’à des milliers de descripteurs supplémentaires (3, 4, 5…) pour lire ou écrire des fichiers, des sockets réseau, des pipes, etc. Les redirections du shell manipulent ces descripteurs : elles les ferment, les rouvrent sur d’autres fichiers ou les font pointer vers d’autres descripteurs. La commande ls -la /proc/$$/fd permet de lister les descripteurs ouverts du shell courant.
Les redirections de base#
Redirection de stdout (> et >>)#
L’opérateur > redirige la sortie standard vers un fichier. Si le fichier existe, il est écrasé ; s’il n’existe pas, il est créé :
# Écrire la liste des fichiers dans un fichier
ls -la > liste_fichiers.txt
# Créer un fichier vide (ou vider un fichier existant)
> fichier_vide.txt
# Écrire un message dans un fichier
echo "Bonjour le monde" > message.txt
L’opérateur >> ajoute (append) à la fin du fichier sans l’écraser :
# Ajouter une entrée à un fichier de log
echo "$(date): Démarrage du service" >> /var/log/mon_service.log
# Accumuler des résultats
echo "Résultats du test 1 :" >> rapport.txt
./test1.sh >> rapport.txt
echo "Résultats du test 2 :" >> rapport.txt
./test2.sh >> rapport.txt
Redirection de stdin (<)#
L’opérateur < connecte le fichier spécifié à l’entrée standard de la commande :
# Trier le contenu d'un fichier
sort < données.txt
# Équivalent (les deux formes sont équivalentes)
sort données.txt
cat données.txt | sort
# Envoyer un fichier comme corps d'un email
mail -s "Rapport quotidien" destinataire@example.com < rapport.txt
# Alimenter une commande interactive avec des données préparées
mysql -u root -p ma_base < script.sql
Redirection de stderr (2>, 2>>)#
# Rediriger les erreurs vers un fichier
ls /repertoire_inexistant 2> erreurs.txt
# Ajouter les erreurs à un fichier de log existant
find / -name "*.conf" 2>> erreurs_find.log
# Supprimer les messages d'erreur (les envoyer dans le néant)
find / -name "*.conf" 2> /dev/null
# Afficher les erreurs mais pas les résultats normaux
find / -name "*.conf" 1>/dev/null
Rediriger stderr vers stdout (2>&1)#
La syntaxe 2>&1 signifie : « connecte le descripteur 2 (stderr) au même endroit que le descripteur 1 (stdout) ». L’ordre des redirections est crucial :
# Capturer stdout et stderr dans le même fichier
commande > sortie_totale.txt 2>&1
# ORDRE IMPORTANT : cette commande est différente
# Ici, stderr va vers le terminal (l'ancien stdout), stdout va dans le fichier
commande 2>&1 > fichier.txt # INCORRECT pour capturer les deux
# La forme abrégée &> (bash uniquement) redirige les deux simultanément
commande &> sortie_totale.txt
# Ajouter stdout et stderr à un fichier
commande >> journal.txt 2>&1
Remarque 19
La syntaxe 2>&1 se lit de droite à gauche : « le descripteur 2 prend comme cible ce que pointe actuellement le descripteur 1 ». C’est pourquoi l’ordre importe : dans > fichier 2>&1, au moment où 2>&1 est évalué, le descripteur 1 pointe déjà vers fichier, donc stderr ira aussi dans fichier. Dans 2>&1 > fichier, au moment où 2>&1 est évalué, le descripteur 1 pointe encore vers le terminal, donc stderr ira vers le terminal.
Le périphérique /dev/null#
/dev/null est un fichier spécial qui se comporte comme un puits sans fond : tout ce qui y est écrit est silencieusement discardé, et toute lecture sur /dev/null retourne immédiatement EOF.
# Supprimer complètement la sortie d'une commande
commande > /dev/null 2>&1
# Lancer une commande en silence total
./script_bruyant.sh &>/dev/null
# Vérifier si une commande réussit sans afficher de sortie
if grep -q "motif" fichier.txt 2>/dev/null; then
echo "Motif trouvé"
fi
Les pipes : connecter les commandes#
Le pipe | est l’opérateur qui connecte la sortie standard d’une commande à l’entrée standard de la suivante. C’est le mécanisme fondamental de la composition de commandes Unix.
Définition 18 (Pipe (tube))
Un pipe est un canal de communication unidirectionnel entre deux processus. Dans le shell, l’opérateur | crée un pipe et :
Lance les deux commandes simultanément (pas l’une après l’autre).
Connecte le stdout de la commande gauche au stdin de la commande droite.
Chaque commande s’exécute dans un sous-shell séparé.
Le pipe se ferme automatiquement quand la commande productrice termine (envoi de SIGPIPE à la commande consommatrice si elle tente d’écrire après la fermeture du pipe).
Construction de pipelines#
# Pipeline simple : lister et trier
ls -la | sort -k5,5rn
# Pipeline en trois étapes
cat /etc/passwd | cut -d':' -f1 | sort
# Les 10 fichiers les plus récemment modifiés
find . -type f -printf "%T@ %p\n" | sort -rn | head -10 | cut -d' ' -f2-
# Compter le nombre de connexions par état TCP
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c | sort -rn
# Analyser l'utilisation des commandes dans l'historique
history | awk '{print $2}' | sort | uniq -c | sort -rn | head -20
Sous-shells et variables#
Un aspect important des pipes est que chaque commande d’un pipeline s’exécute dans un sous-shell. Cela a des implications sur les variables :
# Ce code ne fonctionne PAS comme attendu
total=0
cat nombres.txt | while read n; do
total=$((total + n))
done
echo "Total : $total" # Affiche 0 ! La variable a été modifiée dans un sous-shell.
# Solution avec redirection (pas de sous-shell pour while)
total=0
while read n; do
total=$((total + n))
done < nombres.txt
echo "Total : $total" # Fonctionne correctement
# Solution avec lastpipe (bash 4.2+)
set +m # Désactiver le job control
shopt -s lastpipe
total=0
cat nombres.txt | while read n; do
total=$((total + n))
done
echo "Total : $total" # Fonctionne avec lastpipe
SIGPIPE et gestion des erreurs#
Quand la commande de droite d’un pipe arrête de lire (par exemple head -5 après avoir lu 5 lignes), le noyau envoie le signal SIGPIPE à la commande de gauche pour lui signaler qu’elle peut s’arrêter. Ce comportement est généralement transparent, mais il peut générer des messages d’erreur avec certaines commandes :
# yes génère "y" en boucle infinie, head l'interrompt proprement
yes | head -5
# Certaines commandes affichent un avertissement à la réception de SIGPIPE
# On peut ignorer le code de retour avec || true
quelque_chose | head -1 || true
Substitution de processus#
La substitution de processus est une fonctionnalité avancée de Bash qui permet d’utiliser la sortie d’une commande comme si c’était un fichier.
Définition 19 (Substitution de processus)
La substitution de processus prend deux formes :
<(commande): exécute la commande et rend sa sortie disponible comme un fichier en lecture. Bash crée un fichier spécial dans/dev/fd/(ou un tube nommé selon le système) que la commande cible peut ouvrir comme un fichier ordinaire.>(commande): de manière symétrique, crée un fichier en écriture dont le contenu est envoyé vers stdin de la commande spécifiée.
Cette fonctionnalité est particulièrement utile quand une commande attend des arguments de type fichier et ne peut pas lire sur stdin.
Comparer des sorties de commandes#
# Comparer le contenu de deux répertoires
diff <(ls répertoire1/) <(ls répertoire2/)
# Comparer deux fichiers triés sans créer de fichiers temporaires
diff <(sort fichier1.txt) <(sort fichier2.txt)
# Vérifier que deux commandes produisent le même résultat
diff <(commande1) <(commande2) && echo "Identiques" || echo "Différents"
Alimenter des commandes nécessitant des fichiers#
# wc -l peut prendre plusieurs fichiers et afficher les totaux par fichier
wc -l <(grep "ERREUR" app.log) <(grep "ERREUR" access.log)
# join nécessite des fichiers (et non stdin) pour les deux entrées
join <(sort fichier1.csv) <(sort fichier2.csv)
# Comparer la sortie de deux branches Git
diff <(git show branche1:fichier.py) <(git show branche2:fichier.py)
Substitution de processus en écriture#
# Écrire simultanément dans deux commandes
commande | tee >(gzip > sortie.gz) >(wc -l > compte.txt) > /dev/null
# Journaliser et afficher simultanément
./script.sh > >(tee -a journal.log) 2> >(tee -a erreurs.log >&2)
La commande xargs — construire des arguments#
La commande xargs lit des données sur stdin et les transforme en arguments de la commande spécifiée. C’est essentiel car beaucoup de commandes ne lisent pas sur stdin mais attendent des arguments.
```{prf:definition} Fonctionnement de xargs
:label: definition-07-04
xargs lit les éléments de stdin (séparés par défaut par des espaces et des retours à la ligne) et les passe en argument à la commande cible. Sans options, xargs passe le maximum d’arguments possible en une seule invocation de la commande.
Options importantes :
-I {}: substitution. Chaque occurrence de{}dans la commande est remplacée par l’élément lu sur stdin. La commande est invoquée une fois par élément.-n N: passe au maximum N arguments par invocation de la commande.-P N(parallel) : exécute jusqu’à N processus en parallèle.-0(null) : utilise le caractère nul (\0) comme séparateur. À utiliser avecfind -print0pour gérer les noms de fichiers contenant des espaces.-t(trace) : affiche chaque commande avant de l’exécuter.-r(no-run-if-empty) : ne rien faire si stdin est vide.
### Usages courants de `xargs`
```bash
# Supprimer tous les fichiers .tmp trouvés
find . -name "*.tmp" | xargs rm -f
# Version sûre pour les noms de fichiers avec espaces
find . -name "*.tmp" -print0 | xargs -0 rm -f
# Compter les lignes de tous les fichiers Python du projet
find . -name "*.py" | xargs wc -l
# Convertir des images en parallèle (4 processus simultanés)
find . -name "*.png" | xargs -P4 -I{} convert {} -quality 85 {}.jpg
# Télécharger une liste d'URLs
cat urls.txt | xargs -n1 -P4 wget -q
# Passer des arguments à une commande complexe
echo "alice bob carol" | xargs -n1 | xargs -I{} bash -c 'echo "Bonjour, {}!"'
xargs avec des commandes complexes#
# Rechercher un motif dans tous les fichiers d'une liste
cat fichiers_a_analyser.txt | xargs grep -l "motif_important"
# Créer des répertoires à partir d'une liste
cat liste_projets.txt | xargs -I{} mkdir -p projets/{}
# Archiver des fichiers modifiés récemment
find . -newer référence.txt -type f | xargs tar czf archive.tar.gz
# Tester des URLs en parallèle et afficher le code HTTP
cat urls.txt | xargs -P10 -I{} curl -s -o /dev/null -w "{}: %{http_code}\n" {}
La commande tee — dupliquer le flux#
La commande tee lit sur stdin et écrit simultanément sur stdout et dans un ou plusieurs fichiers. Son nom fait référence au raccord en T de la plomberie.
# Afficher la sortie ET l'enregistrer dans un fichier
commande | tee journal.txt
# Ajouter à un fichier existant (-a pour append)
commande | tee -a journal.txt
# Écrire dans plusieurs fichiers simultanément
commande | tee fichier1.txt fichier2.txt
# Utilisation classique : voir la sortie ET continuer le pipeline
find . -name "*.py" | tee liste_py.txt | wc -l
# Affiche le nombre de fichiers ET enregistre la liste dans liste_py.txt
# Journaliser stdout et stderr séparément tout en continuant le pipeline
./script.sh 2> >(tee erreurs.log >&2) | tee sortie.log | analyse_résultats
Here-documents et here-strings#
Les here-documents et here-strings sont des mécanismes permettant de fournir des données directement dans le code shell, sans fichier externe.
```{prf:definition} Here-document (<<EOF)
:label: definition-07-05
Un here-document est une forme de redirection stdin qui permet d’écrire du texte multi-lignes directement dans le script. La syntaxe est :
commande <<MARQUEUR
ligne 1
ligne 2
...
MARQUEUR
Le texte entre les deux occurrences du marqueur est envoyé sur stdin de la commande. L’interpolation des variables et des substitutions de commandes est active par défaut. Pour la désactiver et traiter le texte littéralement, on place le marqueur entre guillemets simples ou on l’échappe : <<'EOF' ou <<\EOF.
### Here-documents en pratique
```bash
# Écrire un fichier de configuration multi-lignes
cat > /etc/mon_service.conf <<EOF
# Configuration générée le $(date)
serveur=localhost
port=8080
utilisateur=$USER
mode=production
EOF
# Envoyer un email avec contenu HTML
mail -s "Rapport" -a "Content-Type: text/html" destinataire@example.com <<EOF
<h1>Rapport du $(date +%Y-%m-%d)</h1>
<p>Bonjour,</p>
<p>Veuillez trouver ci-joint le rapport quotidien.</p>
EOF
# Exécuter plusieurs commandes MySQL
mysql -u root -p ma_base <<'EOF'
SELECT COUNT(*) FROM utilisateurs;
SELECT nom, email FROM utilisateurs WHERE actif = 1;
UPDATE logs SET traité = TRUE WHERE date < NOW() - INTERVAL 30 DAY;
EOF
# Here-document avec indentation (<<- supprime les tabulations initiales)
if condition; then
cat <<-EOF
Ce texte est indenté dans le script
mais s'affiche sans les tabulations initiales
(seules les tabulations, pas les espaces, sont supprimées)
EOF
fi
Here-strings (<<<)#
# Envoyer une chaîne sur stdin d'une commande
grep "motif" <<< "texte à analyser contenant le motif"
# Équivalent de echo "texte" | commande, mais sans sous-shell
read prénom nom <<< "Alice Dupont"
echo "$prénom" # Alice
echo "$nom" # Dupont
# Tester une expression régulière
if grep -qE "^[0-9]+$" <<< "$variable"; then
echo "$variable est un entier"
fi
# Passer une chaîne à bc pour calcul
résultat=$(bc <<< "scale=4; 355/113")
echo "π ≈ $résultat"
Remarque 20
La here-string <<< a un avantage sur echo "..." | commande : elle ne crée pas de sous-shell. Cela signifie que les variables modifiées à l’intérieur d’une structure lisant depuis une here-string sont visibles dans le shell parent. De plus, avec la here-string, la commande cible peut utiliser un pseudo-fichier au lieu d’un vrai pipe, ce qui peut être plus efficace pour les petites données.
Redirections avancées#
Ouvrir et fermer des descripteurs de fichiers#
Le shell permet de manipuler directement les descripteurs de fichiers, ce qui est utile dans les scripts avancés :
# Ouvrir le descripteur 3 en lecture sur un fichier
exec 3< données.txt
# Lire une ligne depuis le descripteur 3
read -u 3 ligne
# Fermer le descripteur 3
exec 3<&-
# Ouvrir le descripteur 4 en écriture
exec 4> journal.txt
# Écrire sur le descripteur 4
echo "Entrée de journal" >&4
# Fermer le descripteur 4
exec 4>&-
# Sauvegarder et restaurer stdout
exec 5>&1 # Sauvegarder stdout dans le descripteur 5
exec > sortie.txt # Rediriger stdout vers un fichier
echo "Ceci va dans le fichier"
exec 1>&5 # Restaurer stdout depuis le descripteur 5
exec 5>&- # Fermer le descripteur 5
echo "Ceci s'affiche à nouveau dans le terminal"
Redirections dans les blocs#
# Rediriger la sortie de tout un bloc
{
echo "En-tête du rapport"
date
df -h
free -h
} > rapport_système.txt
# Rediriger l'entrée d'un bloc
while read ligne; do
echo "Lu : $ligne"
done < données.txt
# Redirections dans les fonctions
générer_rapport() {
echo "=== Rapport ==="
echo "Date : $(date)"
echo "Utilisateur : $USER"
} 2>&1 | tee rapport.txt
Visualisation : les flux stdin/stdout/stderr#
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[2], line 126
122 # Description
123 ax2.text(3.2, y - 0.22, description, ha='left', va='center',
124 fontsize=8.5, color='#555555', style='italic')
--> 126 plt.tight_layout()
127 plt.show()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/pyplot.py:2843, in tight_layout(pad, h_pad, w_pad, rect)
2835 @_copy_docstring_and_deprecators(Figure.tight_layout)
2836 def tight_layout(
2837 *,
(...) 2841 rect: tuple[float, float, float, float] | None = None,
2842 ) -> None:
-> 2843 gcf().tight_layout(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/figure.py:3640, in Figure.tight_layout(self, pad, h_pad, w_pad, rect)
3638 previous_engine = self.get_layout_engine()
3639 self.set_layout_engine(engine)
-> 3640 engine.execute(self)
3641 if previous_engine is not None and not isinstance(
3642 previous_engine, (TightLayoutEngine, PlaceHolderLayoutEngine)
3643 ):
3644 _api.warn_external('The figure layout has changed to tight')
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/layout_engine.py:188, in TightLayoutEngine.execute(self, fig)
186 renderer = fig._get_renderer()
187 with getattr(renderer, "_draw_disabled", nullcontext)():
--> 188 kwargs = get_tight_layout_figure(
189 fig, fig.axes, get_subplotspec_list(fig.axes), renderer,
190 pad=info['pad'], h_pad=info['h_pad'], w_pad=info['w_pad'],
191 rect=info['rect'])
192 if kwargs:
193 fig.subplots_adjust(**kwargs)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/_tight_layout.py:266, in get_tight_layout_figure(fig, axes_list, subplotspec_list, renderer, pad, h_pad, w_pad, rect)
261 return {}
262 span_pairs.append((
263 slice(ss.rowspan.start * div_row, ss.rowspan.stop * div_row),
264 slice(ss.colspan.start * div_col, ss.colspan.stop * div_col)))
--> 266 kwargs = _auto_adjust_subplotpars(fig, renderer,
267 shape=(max_nrows, max_ncols),
268 span_pairs=span_pairs,
269 subplot_list=subplot_list,
270 ax_bbox_list=ax_bbox_list,
271 pad=pad, h_pad=h_pad, w_pad=w_pad)
273 # kwargs can be none if tight_layout fails...
274 if rect is not None and kwargs is not None:
275 # if rect is given, the whole subplots area (including
276 # labels) will fit into the rect instead of the
(...) 280 # auto_adjust_subplotpars twice, where the second run
281 # with adjusted rect parameters.
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/_tight_layout.py:82, in _auto_adjust_subplotpars(fig, renderer, shape, span_pairs, subplot_list, ax_bbox_list, pad, h_pad, w_pad, rect)
80 for ax in subplots:
81 if ax.get_visible():
---> 82 bb += [martist._get_tightbbox_for_layout_only(ax, renderer)]
84 tight_bbox_raw = Bbox.union(bb)
85 tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:1402, in _get_tightbbox_for_layout_only(obj, *args, **kwargs)
1396 """
1397 Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
1398 *for_layout_only* kwarg; this helper tries to use the kwarg but skips it
1399 when encountering third-party subclasses that do not support it.
1400 """
1401 try:
-> 1402 return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
1403 except TypeError:
1404 return obj.get_tightbbox(*args, **kwargs)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/axes/_base.py:4587, in _AxesBase.get_tightbbox(self, renderer, call_axes_locator, bbox_extra_artists, for_layout_only)
4584 bbox_artists = self.get_default_bbox_extra_artists()
4586 for a in bbox_artists:
-> 4587 bbox = a.get_tightbbox(renderer)
4588 if (bbox is not None
4589 and 0 < bbox.width < np.inf
4590 and 0 < bbox.height < np.inf):
4591 bb.append(bbox)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:364, in Artist.get_tightbbox(self, renderer)
348 def get_tightbbox(self, renderer=None):
349 """
350 Like `.Artist.get_window_extent`, but includes any clipping.
351
(...) 362 Returns None if clipping results in no intersection.
363 """
--> 364 bbox = self.get_window_extent(renderer)
365 if self.get_clip_on():
366 clip_box = self.get_clip_box()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:969, in Text.get_window_extent(self, renderer, dpi)
964 raise RuntimeError(
965 "Cannot get window extent of text w/o renderer. You likely "
966 "want to call 'figure.draw_without_rendering()' first.")
968 with cbook._setattr_cm(fig, dpi=dpi):
--> 969 bbox, info, descent = self._get_layout(self._renderer)
970 x, y = self.get_unitless_position()
971 x, y = self.get_transform().transform((x, y))
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:382, in Text._get_layout(self, renderer)
380 clean_line, ismath = self._preprocess_math(line)
381 if clean_line:
--> 382 w, h, d = _get_text_metrics_with_cache(
383 renderer, clean_line, self._fontproperties,
384 ismath=ismath, dpi=self.get_figure(root=True).dpi)
385 else:
386 w = h = d = 0
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_agg.py:215, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
211 return super().get_text_width_height_descent(s, prop, ismath)
213 if ismath:
214 ox, oy, width, height, descent, font_image = \
--> 215 self.mathtext_parser.parse(s, self.dpi, prop)
216 return width, height, descent
218 font = self._prepare_font(prop)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/mathtext.py:86, in MathTextParser.parse(self, s, dpi, prop, antialiased)
81 from matplotlib.backends import backend_agg
82 load_glyph_flags = {
83 "vector": LoadFlags.NO_HINTING,
84 "raster": backend_agg.get_hinting_flag(),
85 }[self._output_type]
---> 86 return self._parse_cached(s, dpi, prop, antialiased, load_glyph_flags)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/mathtext.py:100, in MathTextParser._parse_cached(self, s, dpi, prop, antialiased, load_glyph_flags)
97 if self._parser is None: # Cache the parser globally.
98 self.__class__._parser = _mathtext.Parser()
--> 100 box = self._parser.parse(s, fontset, fontsize, dpi)
101 output = _mathtext.ship(box)
102 if self._output_type == "vector":
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/_mathtext.py:2167, in Parser.parse(self, s, fonts_object, fontsize, dpi)
2164 result = self._expression.parse_string(s)
2165 except ParseBaseException as err:
2166 # explain becomes a plain method on pyparsing 3 (err.explain(0)).
-> 2167 raise ValueError("\n" + ParseException.explain(err, 0)) from None
2168 self._state_stack = []
2169 self._in_subscript_or_superscript = False
ValueError:
pid = $$
^
ParseException: Expected end of text, found '$' (at char 6), (line:1, col:7)
Error in callback <function _draw_all_if_interactive at 0x7f45cb08f4c0> (for post_execute), with arguments args (),kwargs {}:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/pyplot.py:278, in _draw_all_if_interactive()
276 def _draw_all_if_interactive() -> None:
277 if matplotlib.is_interactive():
--> 278 draw_all()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/_pylab_helpers.py:131, in Gcf.draw_all(cls, force)
129 for manager in cls.get_all_fig_managers():
130 if force or manager.canvas.figure.stale:
--> 131 manager.canvas.draw_idle()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/backend_bases.py:1893, in FigureCanvasBase.draw_idle(self, *args, **kwargs)
1891 if not self._is_idle_drawing:
1892 with self._idle_draw_cntx():
-> 1893 self.draw(*args, **kwargs)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_agg.py:382, in FigureCanvasAgg.draw(self)
379 # Acquire a lock on the shared font cache.
380 with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar
381 else nullcontext()):
--> 382 self.figure.draw(self.renderer)
383 # A GUI class may be need to update a window using this draw, so
384 # don't forget to call the superclass.
385 super().draw()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
92 @wraps(draw)
93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94 result = draw(artist, renderer, *args, **kwargs)
95 if renderer._rasterizing:
96 renderer.stop_rasterizing()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/figure.py:3257, in Figure.draw(self, renderer)
3254 # ValueError can occur when resizing a window.
3256 self.patch.draw(renderer)
-> 3257 mimage._draw_list_compositing_images(
3258 renderer, self, artists, self.suppressComposite)
3260 renderer.close_group('figure')
3261 finally:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/image.py:134, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
132 if not_composite or not has_images:
133 for a in artists:
--> 134 a.draw(renderer)
135 else:
136 # Composite any adjacent images together
137 image_group = []
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/axes/_base.py:3226, in _AxesBase.draw(self, renderer)
3223 if artists_rasterized:
3224 _draw_rasterized(self.get_figure(root=True), artists_rasterized, renderer)
-> 3226 mimage._draw_list_compositing_images(
3227 renderer, self, artists, self.get_figure(root=True).suppressComposite)
3229 renderer.close_group('axes')
3230 self.stale = False
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/image.py:134, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
132 if not_composite or not has_images:
133 for a in artists:
--> 134 a.draw(renderer)
135 else:
136 # Composite any adjacent images together
137 image_group = []
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:752, in Text.draw(self, renderer)
749 renderer.open_group('text', self.get_gid())
751 with self._cm_set(text=self._get_wrapped_text()):
--> 752 bbox, info, descent = self._get_layout(renderer)
753 trans = self.get_transform()
755 # don't use self.get_position here, which refers to text
756 # position in Text:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:382, in Text._get_layout(self, renderer)
380 clean_line, ismath = self._preprocess_math(line)
381 if clean_line:
--> 382 w, h, d = _get_text_metrics_with_cache(
383 renderer, clean_line, self._fontproperties,
384 ismath=ismath, dpi=self.get_figure(root=True).dpi)
385 else:
386 w = h = d = 0
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_agg.py:215, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
211 return super().get_text_width_height_descent(s, prop, ismath)
213 if ismath:
214 ox, oy, width, height, descent, font_image = \
--> 215 self.mathtext_parser.parse(s, self.dpi, prop)
216 return width, height, descent
218 font = self._prepare_font(prop)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/mathtext.py:86, in MathTextParser.parse(self, s, dpi, prop, antialiased)
81 from matplotlib.backends import backend_agg
82 load_glyph_flags = {
83 "vector": LoadFlags.NO_HINTING,
84 "raster": backend_agg.get_hinting_flag(),
85 }[self._output_type]
---> 86 return self._parse_cached(s, dpi, prop, antialiased, load_glyph_flags)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/mathtext.py:100, in MathTextParser._parse_cached(self, s, dpi, prop, antialiased, load_glyph_flags)
97 if self._parser is None: # Cache the parser globally.
98 self.__class__._parser = _mathtext.Parser()
--> 100 box = self._parser.parse(s, fontset, fontsize, dpi)
101 output = _mathtext.ship(box)
102 if self._output_type == "vector":
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/_mathtext.py:2167, in Parser.parse(self, s, fonts_object, fontsize, dpi)
2164 result = self._expression.parse_string(s)
2165 except ParseBaseException as err:
2166 # explain becomes a plain method on pyparsing 3 (err.explain(0)).
-> 2167 raise ValueError("\n" + ParseException.explain(err, 0)) from None
2168 self._state_stack = []
2169 self._in_subscript_or_superscript = False
ValueError:
pid = $$
^
ParseException: Expected end of text, found '$' (at char 6), (line:1, col:7)
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/formatters.py:402, in BaseFormatter.__call__(self, obj)
400 pass
401 else:
--> 402 return printer(obj)
403 # Finally look for special method names
404 method = get_real_method(obj, self.print_method)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/IPython/core/pylabtools.py:170, in print_figure(fig, fmt, bbox_inches, base64, **kwargs)
167 from matplotlib.backend_bases import FigureCanvasBase
168 FigureCanvasBase(fig)
--> 170 fig.canvas.print_figure(bytes_io, **kw)
171 data = bytes_io.getvalue()
172 if fmt == 'svg':
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/backend_bases.py:2157, in FigureCanvasBase.print_figure(self, filename, dpi, facecolor, edgecolor, orientation, format, bbox_inches, pad_inches, bbox_extra_artists, backend, **kwargs)
2154 # we do this instead of `self.figure.draw_without_rendering`
2155 # so that we can inject the orientation
2156 with getattr(renderer, "_draw_disabled", nullcontext)():
-> 2157 self.figure.draw(renderer)
2158 if bbox_inches:
2159 if bbox_inches == "tight":
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:94, in _finalize_rasterization.<locals>.draw_wrapper(artist, renderer, *args, **kwargs)
92 @wraps(draw)
93 def draw_wrapper(artist, renderer, *args, **kwargs):
---> 94 result = draw(artist, renderer, *args, **kwargs)
95 if renderer._rasterizing:
96 renderer.stop_rasterizing()
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/figure.py:3257, in Figure.draw(self, renderer)
3254 # ValueError can occur when resizing a window.
3256 self.patch.draw(renderer)
-> 3257 mimage._draw_list_compositing_images(
3258 renderer, self, artists, self.suppressComposite)
3260 renderer.close_group('figure')
3261 finally:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/image.py:134, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
132 if not_composite or not has_images:
133 for a in artists:
--> 134 a.draw(renderer)
135 else:
136 # Composite any adjacent images together
137 image_group = []
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/axes/_base.py:3226, in _AxesBase.draw(self, renderer)
3223 if artists_rasterized:
3224 _draw_rasterized(self.get_figure(root=True), artists_rasterized, renderer)
-> 3226 mimage._draw_list_compositing_images(
3227 renderer, self, artists, self.get_figure(root=True).suppressComposite)
3229 renderer.close_group('axes')
3230 self.stale = False
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/image.py:134, in _draw_list_compositing_images(renderer, parent, artists, suppress_composite)
132 if not_composite or not has_images:
133 for a in artists:
--> 134 a.draw(renderer)
135 else:
136 # Composite any adjacent images together
137 image_group = []
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/artist.py:71, in allow_rasterization.<locals>.draw_wrapper(artist, renderer)
68 if artist.get_agg_filter() is not None:
69 renderer.start_filter()
---> 71 return draw(artist, renderer)
72 finally:
73 if artist.get_agg_filter() is not None:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:752, in Text.draw(self, renderer)
749 renderer.open_group('text', self.get_gid())
751 with self._cm_set(text=self._get_wrapped_text()):
--> 752 bbox, info, descent = self._get_layout(renderer)
753 trans = self.get_transform()
755 # don't use self.get_position here, which refers to text
756 # position in Text:
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:382, in Text._get_layout(self, renderer)
380 clean_line, ismath = self._preprocess_math(line)
381 if clean_line:
--> 382 w, h, d = _get_text_metrics_with_cache(
383 renderer, clean_line, self._fontproperties,
384 ismath=ismath, dpi=self.get_figure(root=True).dpi)
385 else:
386 w = h = d = 0
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:69, in _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi)
66 """Call ``renderer.get_text_width_height_descent``, caching the results."""
67 # Cached based on a copy of fontprop so that later in-place mutations of
68 # the passed-in argument do not mess up the cache.
---> 69 return _get_text_metrics_with_cache_impl(
70 weakref.ref(renderer), text, fontprop.copy(), ismath, dpi)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/text.py:77, in _get_text_metrics_with_cache_impl(renderer_ref, text, fontprop, ismath, dpi)
73 @functools.lru_cache(4096)
74 def _get_text_metrics_with_cache_impl(
75 renderer_ref, text, fontprop, ismath, dpi):
76 # dpi is unused, but participates in cache invalidation (via the renderer).
---> 77 return renderer_ref().get_text_width_height_descent(text, fontprop, ismath)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/backends/backend_agg.py:215, in RendererAgg.get_text_width_height_descent(self, s, prop, ismath)
211 return super().get_text_width_height_descent(s, prop, ismath)
213 if ismath:
214 ox, oy, width, height, descent, font_image = \
--> 215 self.mathtext_parser.parse(s, self.dpi, prop)
216 return width, height, descent
218 font = self._prepare_font(prop)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/mathtext.py:86, in MathTextParser.parse(self, s, dpi, prop, antialiased)
81 from matplotlib.backends import backend_agg
82 load_glyph_flags = {
83 "vector": LoadFlags.NO_HINTING,
84 "raster": backend_agg.get_hinting_flag(),
85 }[self._output_type]
---> 86 return self._parse_cached(s, dpi, prop, antialiased, load_glyph_flags)
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/mathtext.py:100, in MathTextParser._parse_cached(self, s, dpi, prop, antialiased, load_glyph_flags)
97 if self._parser is None: # Cache the parser globally.
98 self.__class__._parser = _mathtext.Parser()
--> 100 box = self._parser.parse(s, fontset, fontsize, dpi)
101 output = _mathtext.ship(box)
102 if self._output_type == "vector":
File ~/legacy_workspace/notebooks/.venv/lib/python3.13/site-packages/matplotlib/_mathtext.py:2167, in Parser.parse(self, s, fonts_object, fontsize, dpi)
2164 result = self._expression.parse_string(s)
2165 except ParseBaseException as err:
2166 # explain becomes a plain method on pyparsing 3 (err.explain(0)).
-> 2167 raise ValueError("\n" + ParseException.explain(err, 0)) from None
2168 self._state_stack = []
2169 self._in_subscript_or_superscript = False
ValueError:
pid = $$
^
ParseException: Expected end of text, found '$' (at char 6), (line:1, col:7)
<Figure size 1600x800 with 2 Axes>
Exemples avancés de pipelines#
Monitoring et analyse en temps réel#
# Surveiller les nouvelles lignes d'un log en les filtrant
tail -f /var/log/syslog | grep --line-buffered "ERREUR\|WARN" | \
while read ligne; do
echo "[$(date '+%H:%M:%S')] $ligne"
done
# Calculer la taille cumulée des fichiers de log
find /var/log -name "*.log" -type f | \
xargs du -b 2>/dev/null | \
awk '{total += $1} END {printf "Total : %.2f Mo\n", total/1048576}'
# Identifier les fichiers en double (même contenu, noms différents)
find . -type f -exec md5sum {} \; | \
sort | \
uniq -d -w32 | \
awk '{print $2}'
Traitement de données structurées#
# Extraire et reformater des données JSON avec jq et awk
curl -s "https://api.example.com/users" | \
jq -r '.[] | "\(.nom),\(.email),\(.age)"' | \
sort -t',' -k3,3n | \
awk -F',' 'BEGIN {print "Nom;Email;Âge"} {print $1";"$2";"$3}' | \
tee rapport_utilisateurs.csv | \
wc -l
# Convertir un fichier de log en CSV
awk '{
match($0, /\[([^\]]+)\]/, date_arr)
match($0, /"([A-Z]+) ([^ ]+)/, méthode_arr)
print date_arr[1] "," méthode_arr[1] "," $9 "," $10
}' /var/log/apache2/access.log | \
sort -t',' -k3,3n | \
tee accès.csv | head -5
Exemple 18 (Pipeline de traitement de données de capteurs)
Supposons un fichier capteurs.txt avec des relevés de température toutes les minutes, au format timestamp,capteur_id,température :
# Calculer les statistiques par capteur
sort -t',' -k2,2 capteurs.txt | \
awk -F',' '
{
somme[$2] += $3
n[$2]++
if (!min[$2] || $3 < min[$2]) min[$2] = $3
if ($3 > max[$2]) max[$2] = $3
}
END {
printf "%-15s %8s %8s %8s %8s\n", "Capteur", "Moyenne", "Min", "Max", "N"
printf "%-15s %8s %8s %8s %8s\n", "-------", "-------", "---", "---", "-"
for (c in somme)
printf "%-15s %8.2f %8.2f %8.2f %8d\n",
c, somme[c]/n[c], min[c], max[c], n[c]
}' | sort -k2,2rn
# Détecter les anomalies (température > 80°C)
awk -F',' '$3 > 80 {
print "ALERTE:", $1, "- Capteur", $2, "- Température:", $3 "°C"
}' capteurs.txt | tee alertes.log | wc -l
## Résumé
Dans ce chapitre, nous avons maîtrisé les mécanismes de communication entre processus et le shell :
- Les **trois flux standard** — stdin (0), stdout (1) et stderr (2) — sont des descripteurs de fichiers hérités par chaque processus. Leur séparation permet un contrôle fin de la communication entre programmes.
- Les **redirections** permettent de réorienter ces flux : `>` écrase un fichier, `>>` ajoute, `<` connecte un fichier à stdin. La redirection `2>` cible stderr, `2>&1` fusionne stderr dans stdout, et `&>` redirige les deux simultanément. `/dev/null` absorbe silencieusement tout ce qui lui est envoyé.
- Les **pipes** `|` connectent le stdout d'une commande au stdin de la suivante. Les commandes s'exécutent en parallèle dans des sous-shells ; SIGPIPE gère proprement l'arrêt de la chaîne.
- La **substitution de processus** `<(cmd)` et `>(cmd)` permet d'utiliser la sortie ou l'entrée d'une commande comme un fichier, contournant la limitation des programmes qui n'acceptent que des arguments de type fichier.
- **`xargs`** transforme stdin en arguments de commande, avec `-I{}` pour la substitution, `-n` pour limiter le nombre d'arguments par appel et `-P` pour la parallélisation.
- **`tee`** duplique un flux : il écrit simultanément sur stdout et dans un fichier, permettant d'observer et d'enregistrer sans interrompre le pipeline.
- Les **here-documents** (`<<EOF`) et **here-strings** (`<<<`) permettent d'inclure des données multi-lignes directement dans les scripts sans fichiers temporaires.
Dans le chapitre suivant, nous aborderons le cœur du scripting Bash : les **variables et les types** — comment déclarer des variables, maîtriser les différents types de guillemets, gérer l'environnement et effectuer des calculs arithmétiques.