---
jupytext:
  text_representation:
    extension: .md
    format_name: myst
    format_version: 0.13
    jupytext_version: 1.16.0
kernelspec:
  name: python3
  display_name: Python 3
---

# Bash et les autres langages

```{code-cell} python
:tags: [hide-input]

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import numpy as np
import seaborn as sns

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
```

Bash et Python ne sont pas en concurrence — ils sont complémentaires. L'erreur fréquente est de choisir l'un ou l'autre dogmatiquement, alors que la réponse juste dépend de la nature de la tâche. Un script de déploiement qui coordonne cinq commandes système est parfaitement à sa place en Bash ; un parser de JSON imbriqué qui valide des données et envoie des requêtes HTTP l'est en Python. Ce chapitre trace la frontière entre les deux mondes, explore les mécanismes de communication entre eux, et présente `jq` comme outil de traitement JSON en ligne de commande.

## Quand Bash est le bon outil

Bash excelle dans un domaine précis et bien défini : **coordonner des processus Unix**. Sa force n'est pas le traitement de données complexes ni la logique applicative — elle réside dans la facilité avec laquelle il compose des commandes existantes via des pipes, gère des fichiers et des répertoires, et interagit avec le système d'exploitation.

```{prf:definition} Domaine de prédilection de Bash
:label: definition-19-01
Bash est le bon outil lorsque :

- La tâche consiste principalement à **coordonner des programmes existants** : compiler, copier, renommer, déplacer, archiver.
- Le traitement de données se fait par **flux de texte** simple : filtrage de lignes, extraction de colonnes, comptage — avec `grep`, `awk`, `sed`, `cut`, `sort`, `uniq`.
- Les **interactions système** dominent : variables d'environnement, signaux, processus, permissions, chemins de fichiers.
- Le script doit tourner dans un environnement **minimal sans dépendances** : un serveur neuf sans Python ou Ruby installé.
- La tâche est un **glue code** entre des outils : lancer un backup, vérifier l'espace disque, envoyer une alerte.
```

```bash
# Bash excelle : déploiement simple
#!/usr/bin/env bash
set -euo pipefail

echo "Déploiement de l'application..."
git pull origin main
npm run build
systemctl restart mon-app
systemctl is-active mon-app || { echo "ÉCHEC du démarrage" >&2; exit 1; }
echo "Déploiement réussi."

# Bash excelle : pipeline d'analyse de logs
grep "ERROR" /var/log/app.log \
    | awk '{ print $5 }' \
    | sort | uniq -c \
    | sort -rn \
    | head -10

# Bash excelle : surveillance périodique
#!/usr/bin/env bash
seuil_disk=90
utilisation=$(df / | awk 'NR==2 { gsub(/%/, ""); print $5 }')
if [ "$utilisation" -gt "$seuil_disk" ]; then
    echo "ALERTE : disque root à ${utilisation}%" | mail -s "Disque plein" admin@exemple.fr
fi
```

## Quand passer à Python

La frontière se franchit dès que la complexité de la logique augmente ou que les structures de données deviennent non triviales.

```{prf:remark}
:label: remark-19-01
Privilégier Python lorsque :

- Le traitement de données est **complexe** : tris multi-critères, jointures, agrégations, fenêtres glissantes.
- Les données sont structurées en **JSON, XML, YAML, CSV avec en-têtes** ou tout autre format sérialisé.
- Des **requêtes réseau** (HTTP, WebSocket, SSH) ou des interactions avec des **APIs** sont nécessaires.
- Le script doit être **testé** avec des tests unitaires ou d'intégration.
- La **gestion d'erreurs** est importante et granulaire : exceptions typées, retry avec backoff, logging structuré.
- Des **bibliothèques tierces** sont nécessaires : cryptographie, PDF, images, bases de données, machine learning.
- La **lisibilité** et la **maintenabilité** à long terme sont prioritaires pour un code partagé en équipe.
```

```python
# Python excelle : parsing et transformation de données complexes
import json
from pathlib import Path
from datetime import datetime

def analyser_logs(chemin_log: Path) -> dict:
    """Parser des logs JSON structurés et produire un rapport."""
    erreurs_par_service = {}

    with chemin_log.open() as f:
        for ligne in f:
            try:
                entree = json.loads(ligne)
            except json.JSONDecodeError:
                continue

            if entree.get('niveau') == 'ERROR':
                service = entree.get('service', 'inconnu')
                erreurs_par_service.setdefault(service, []).append({
                    'timestamp': entree['timestamp'],
                    'message': entree['message'],
                    'trace': entree.get('stacktrace', ''),
                })

    return erreurs_par_service
```

## Appeler Python depuis Bash

Il existe plusieurs façons d'intégrer du code Python dans un script Bash.

### `python3 -c` : expressions inline

Pour des transformations simples qui ne justifient pas un fichier Python séparé :

```bash
# Calculer une date dans N jours (Bash n'a pas de gestion de dates avancée)
date_cible=$(python3 -c "
from datetime import datetime, timedelta
d = datetime.now() + timedelta(days=30)
print(d.strftime('%Y-%m-%d'))
")
echo "Date dans 30 jours : $date_cible"

# Convertir des unités
taille_mo=$(python3 -c "print(${taille_octets} / 1048576)")

# Calculer un pourcentage avec précision
pct=$(python3 -c "print(f'{${compteur}/${total}*100:.1f}')")

# Parser du JSON simplement
valeur=$(echo '{"timeout": 30, "retries": 3}' \
    | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['timeout'])")
```

### Scripts Python comme sous-commandes

Pour une logique plus complexe, on écrit un script Python autonome et on l'appelle depuis Bash :

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

# Le script Python est dans le même répertoire
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Appeler le script Python avec des arguments
rapport=$(python3 "$SCRIPT_DIR/generer_rapport.py" \
    --debut "$(date -d '7 days ago' +%Y-%m-%d)" \
    --fin "$(date +%Y-%m-%d)" \
    --format json)

# Utiliser la sortie
echo "$rapport" | jq '.total_erreurs'
```

```{prf:example} Intégration Bash ↔ Python par shebang
:label: example-19-01
Un script Python peut être rendu exécutable directement et s'intégrer dans un pipeline Bash comme n'importe quelle commande Unix :

```python
#!/usr/bin/env python3
"""Transforme un CSV en JSON sur stdout."""
import sys
import csv
import json

lecteur = csv.DictReader(sys.stdin)
json.dump(list(lecteur), sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write('\n')
```

```bash
# Utilisation dans un pipeline Bash
cat donnees.csv | ./csv_vers_json.py | jq '.[] | select(.statut == "actif")'
```

## Passer des données entre Bash et Python

La communication entre Bash et Python peut emprunter plusieurs canaux, chacun avec ses avantages et ses limites.

### Variables d'environnement

Le moyen le plus simple de passer des valeurs simples (chaînes, nombres) d'un script Bash à un programme Python :

```bash
#!/usr/bin/env bash
export DB_HOST="localhost"
export DB_PORT="5432"
export APP_ENV="production"
export MAX_CONNEXIONS="100"

python3 mon_script.py
```

```python
# Dans mon_script.py
import os

db_host = os.environ['DB_HOST']
db_port = int(os.environ['DB_PORT'])
app_env = os.environ.get('APP_ENV', 'development')  # avec valeur par défaut
max_conn = int(os.environ.get('MAX_CONNEXIONS', '10'))
```

```{prf:remark}
:label: remark-19-02
Les variables d'environnement sont idéales pour la **configuration** (URLs, tokens, noms d'hôtes), mais ne conviennent pas aux données volumineuses ou aux structures imbriquées. Pour ces cas, privilégier stdin/stdout ou les fichiers temporaires. De plus, les variables d'environnement sont visibles par tous les processus enfants et par les outils de monitoring — ne jamais y placer de mots de passe ou de secrets en clair.
```

### Stdin, stdout et pipes

Le modèle Unix par excellence : un programme lit sur stdin, traite, et écrit sur stdout.

```bash
# Bash produit des données, Python les transforme
cat donnees.tsv \
    | awk -F'\t' '{ print $1, $3, $5 }' \
    | python3 normaliser.py \
    | sort -k2 \
    > resultat.txt

# Python produit du JSON, Bash le consomme avec jq
python3 extraire_metriques.py \
    | jq '.[] | select(.valeur > 100) | .nom'
```

```python
# normaliser.py : traiter stdin ligne par ligne
import sys

for ligne in sys.stdin:
    champs = ligne.rstrip('\n').split(' ')
    # ... transformation ...
    print('\t'.join(champs_traites))
```

### Fichiers temporaires avec `mktemp`

Pour les données volumineuses ou lorsque plusieurs programmes doivent accéder aux mêmes données intermédiaires, les fichiers temporaires sont la solution :

```{prf:definition} `mktemp` : créer des fichiers temporaires sûrs
:label: definition-19-02
`mktemp` crée un fichier temporaire avec un nom **unique et imprévisible** dans `/tmp` (ou le répertoire spécifié). Cette unicité est cruciale pour la sécurité : utiliser un nom prévisible permettrait à un attaquant de créer un lien symbolique à l'avance et de faire écrire le script dans un fichier arbitraire du système.
```

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

# Créer un fichier temporaire
TMPFICHIER=$(mktemp)
TMPDIR_TRAVAIL=$(mktemp -d)

# Nettoyage automatique à la sortie
trap 'rm -f "$TMPFICHIER"; rm -rf "$TMPDIR_TRAVAIL"' EXIT

# Étape 1 : Python génère des données intermédiaires
python3 etape1_extraire.py > "$TMPFICHIER"

# Étape 2 : Bash traite les données
grep -v "^#" "$TMPFICHIER" | sort -k2 > "$TMPDIR_TRAVAIL/trie.txt"

# Étape 3 : Python traite le résultat
python3 etape2_analyser.py "$TMPDIR_TRAVAIL/trie.txt" > rapport_final.txt

echo "Traitement terminé. Résultat dans rapport_final.txt"
```

## Appeler Bash depuis Python

L'inverse est tout aussi courant : un script Python qui a besoin de lancer des commandes système.

### `subprocess.run` : l'interface recommandée

```{prf:definition} `subprocess.run`
:label: definition-19-03
`subprocess.run()` est la fonction recommandée depuis Python 3.5 pour lancer un processus externe et attendre sa terminaison. Elle remplace les anciennes fonctions `os.system()`, `subprocess.call()` et `subprocess.check_call()`. Son paramètre `capture_output=True` capture stdout et stderr ; `check=True` lève une exception `CalledProcessError` si le code de retour est non nul.
```

```python
import subprocess
import shlex

# Cas simple : exécuter une commande et vérifier le succès
resultat = subprocess.run(
    ['git', 'pull', 'origin', 'main'],
    capture_output=True,
    text=True,       # décoder stdout/stderr en str (UTF-8 par défaut)
    check=True       # lever CalledProcessError si code != 0
)
print(resultat.stdout)

# Récupérer la sortie d'une commande
sortie = subprocess.run(
    ['df', '-h', '/'],
    capture_output=True,
    text=True,
    check=True
).stdout
print(sortie)

# Passer une chaîne à stdin
proc = subprocess.run(
    ['grep', 'erreur'],
    input="ligne 1 : erreur critique\nligne 2 : OK\n",
    capture_output=True,
    text=True
)
print(proc.stdout)  # "ligne 1 : erreur critique\n"

# Gestion d'erreur explicite
try:
    subprocess.run(['commande_inexistante'], check=True, capture_output=True)
except subprocess.CalledProcessError as e:
    print(f"Échec (code {e.returncode}) : {e.stderr}")
except FileNotFoundError:
    print("Commande introuvable dans le PATH")
```

### `shlex.split` : découper une commande en liste

```{prf:definition} `shlex.split`
:label: definition-19-04
`shlex.split()` découpe une chaîne de commande shell en liste d'arguments en respectant les règles de quoting du shell. C'est la façon sûre de construire la liste d'arguments pour `subprocess.run` quand la commande est fournie sous forme de chaîne.
```

```python
import shlex
import subprocess

# Éviter de construire la commande comme une chaîne avec format()
# (risque d'injection si les arguments contiennent des espaces ou des caractères spéciaux)
chemin = "/chemin/avec espaces/fichier.txt"

# DANGEREUX : shell=True avec données non contrôlées
# subprocess.run(f"cat {chemin}", shell=True)  # MAUVAIS

# CORRECT : liste d'arguments
subprocess.run(['cat', chemin])  # Les espaces dans chemin sont gérés correctement

# shlex.split pour les commandes dynamiques
commande_str = 'rsync -avz --delete /source/ user@host:/dest/'
args = shlex.split(commande_str)
# args = ['rsync', '-avz', '--delete', '/source/', 'user@host:/dest/']
subprocess.run(args, check=True)
```

### `subprocess.Popen` : contrôle fin du processus

Pour les cas où l'on a besoin d'un contrôle plus précis (lecture en temps réel, communication bidirectionnelle) :

```python
import subprocess

# Lire la sortie en temps réel (ligne par ligne)
with subprocess.Popen(
    ['tail', '-f', '/var/log/app.log'],
    stdout=subprocess.PIPE,
    text=True
) as proc:
    for ligne in proc.stdout:
        if 'CRITICAL' in ligne:
            envoyer_alerte(ligne)
        print(ligne, end='')

# Pipeline Python : équivalent de cmd1 | cmd2
proc1 = subprocess.Popen(['ps', 'aux'], stdout=subprocess.PIPE)
proc2 = subprocess.Popen(
    ['grep', 'python'],
    stdin=proc1.stdout,
    stdout=subprocess.PIPE,
    text=True
)
proc1.stdout.close()  # Permettre à proc1 de recevoir SIGPIPE si proc2 se termine
sortie, _ = proc2.communicate()
print(sortie)
```

## Traitement de JSON avec `jq`

`jq` est un processeur JSON en ligne de commande, comparable à ce que `awk` et `sed` font pour le texte. Il est indispensable dans les scripts Bash modernes qui interagissent avec des APIs REST.

```{prf:definition} `jq` : processeur JSON en ligne de commande
:label: definition-19-05
`jq` est un outil de transformation et d'interrogation de documents JSON. Il accepte du JSON sur stdin (ou depuis un fichier), applique un **filtre** exprimé dans son propre langage, et produit du JSON (ou des valeurs scalaires) sur stdout. Son filtre le plus simple, `.`, retourne le document formaté (pretty-print).
```

### Filtres de base

```bash
# pretty-print (indentation et couleurs)
echo '{"nom":"alice","age":30}' | jq '.'

# Accéder à un champ
echo '{"nom":"alice","age":30}' | jq '.nom'
# Sortie : "alice"

# Sans guillemets (raw output)
echo '{"nom":"alice"}' | jq -r '.nom'
# Sortie : alice

# Accès imbriqué
echo '{"user":{"email":"a@b.fr","role":"admin"}}' | jq '.user.email'

# Accès à un tableau par index
echo '[10, 20, 30, 40]' | jq '.[2]'
# Sortie : 30

# Tous les éléments d'un tableau
echo '[{"id":1},{"id":2}]' | jq '.[]'

# Longueur d'un tableau ou d'un objet
echo '[1,2,3,4,5]' | jq 'length'
```

### Filtres de transformation

```bash
# Créer un nouvel objet
echo '{"prenom":"Alice","nom":"Martin","age":30}' \
    | jq '{ nom_complet: (.prenom + " " + .nom), age }'

# Transformer un tableau
echo '[1,2,3,4,5]' | jq 'map(. * 2)'

# Filtrer un tableau avec select()
echo '[{"statut":"actif"},{"statut":"inactif"},{"statut":"actif"}]' \
    | jq '[.[] | select(.statut == "actif")]'

# Extraire un champ de chaque élément d'un tableau
echo '[{"nom":"a","val":1},{"nom":"b","val":2}]' \
    | jq '[.[] | .nom]'
# Équivalent : jq '[.[].nom]'

# Trier un tableau d'objets
echo '[{"n":"c"},{"n":"a"},{"n":"b"}]' \
    | jq 'sort_by(.n)'

# Regrouper par un champ
echo '[{"type":"A","val":1},{"type":"B","val":2},{"type":"A","val":3}]' \
    | jq 'group_by(.type)'
```

### `jq` dans des scripts Bash

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

API_URL="https://api.exemple.fr/v1"
TOKEN="${API_TOKEN:?La variable API_TOKEN doit être définie}"

# Appeler une API et extraire des données
reponse=$(curl -s -H "Authorization: Bearer $TOKEN" "$API_URL/utilisateurs")

# Vérifier le statut HTTP (avec -w pour écrire le code)
http_code=$(curl -s -o /tmp/reponse.json -w "%{http_code}" \
    -H "Authorization: Bearer $TOKEN" "$API_URL/utilisateurs")
if [ "$http_code" -ne 200 ]; then
    echo "Erreur API : HTTP $http_code" >&2
    exit 1
fi

# Traiter la réponse
nb_utilisateurs=$(jq 'length' /tmp/reponse.json)
echo "Nombre d'utilisateurs : $nb_utilisateurs"

# Extraire les emails des utilisateurs actifs
jq -r '.[] | select(.actif == true) | .email' /tmp/reponse.json

# Boucler sur les éléments avec while read
jq -r '.[] | "\(.id) \(.nom) \(.email)"' /tmp/reponse.json \
    | while IFS=' ' read -r id nom email; do
        echo "Traitement de l'utilisateur $id ($nom) <$email>"
        # ... traitement ...
    done

# Construire un objet JSON depuis des variables Bash
NOM="Alice Martin"
EMAIL="alice@exemple.fr"
ROLE="admin"

payload=$(jq -n \
    --arg nom "$NOM" \
    --arg email "$EMAIL" \
    --arg role "$ROLE" \
    '{nom: $nom, email: $email, role: $role}')

curl -s -X POST \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer $TOKEN" \
    -d "$payload" \
    "$API_URL/utilisateurs"
```

```{prf:remark}
:label: remark-19-03
**Ne jamais construire du JSON par concaténation de chaînes.** Les caractères spéciaux dans les variables (guillemets, antislashes, caractères Unicode) peuvent invalider le JSON ou introduire des injections. Utiliser `jq -n --arg nom "$variable"` ou le module `json` de Python pour construire du JSON de façon sûre.
```

### Parsing de CSV avec `awk` et Python

```bash
# CSV simple : extraire la 2e colonne (sans en-tête)
awk -F',' 'NR>1 { print $2 }' donnees.csv

# CSV avec guillemets : utiliser Python
python3 - <<'EOF'
import csv, sys

with open('donnees.csv', newline='', encoding='utf-8') as f:
    lecteur = csv.DictReader(f)
    for ligne in lecteur:
        print(ligne['email'], ligne['statut'])
EOF

# Convertir CSV en JSON avec python3
python3 -c "
import csv, json, sys
lecteur = csv.DictReader(sys.stdin)
json.dump(list(lecteur), sys.stdout, ensure_ascii=False, indent=2)
" < donnees.csv > donnees.json
```

```{code-cell} python
:tags: [hide-input]

fig, ax = plt.subplots(figsize=(14, 9))
ax.set_xlim(-0.5, 14.5)
ax.set_ylim(-0.5, 10.5)
ax.axis('off')
ax.set_title('Matrice décisionnelle : Bash vs Python par type de tâche',
             fontsize=14, fontweight='bold', pad=20)

# En-têtes colonnes
cols = ['Type de tâche', 'Bash', 'Python', 'Recommandation']
col_x = [0.0, 5.2, 7.8, 10.4]
col_w = [5.0, 2.4, 2.4, 3.8]
couleurs_col = ['#2c3e50', '#e67e22', '#2980b9', '#27ae60']

for label, x, w, c in zip(cols, col_x, col_w, couleurs_col):
    b = patches.FancyBboxPatch((x, 9.5), w - 0.15, 0.75,
                               boxstyle='round,pad=0.1', linewidth=1.5,
                               edgecolor=c, facecolor=c, alpha=0.9)
    ax.add_patch(b)
    ax.text(x + (w-0.15)/2, 9.875, label, ha='center', va='center',
            fontsize=9.5, fontweight='bold', color='white')

# Données
taches = [
    ('Coordonner des commandes système\n(cp, rsync, git, systemctl)',
     '★★★★★', '★★', 'Bash'),
    ('Pipeline de texte simple\n(grep, awk, sed, sort)',
     '★★★★★', '★★★', 'Bash'),
    ('Manipulation de JSON/XML/YAML',
     '★', '★★★★★', 'Python'),
    ('Requêtes HTTP / API REST',
     '★★ (curl)', '★★★★★', 'Python'),
    ('Scripts de déploiement',
     '★★★★★', '★★★', 'Bash (ou les deux)'),
    ('Traitement de données tabulaires',
     '★★ (awk)', '★★★★★', 'Python'),
    ('Tests unitaires et TDD',
     '★', '★★★★★', 'Python'),
    ('Gestion de fichiers simples',
     '★★★★★', '★★★★', 'Bash'),
    ('Logique conditionnelle complexe',
     '★★', '★★★★★', 'Python'),
    ('Scripts portables sans dépendances',
     '★★★★★', '★★', 'Bash'),
]

couleurs_fonds = ['#fafafa', '#f0f0f0']
couleurs_etoiles = {'Bash': '#e67e22', 'Python': '#2980b9'}

for i, (tache, score_bash, score_python, reco) in enumerate(taches):
    y = 8.8 - i * 0.88
    bg = couleurs_fonds[i % 2]

    # Fond de ligne
    b = patches.FancyBboxPatch((0.0, y - 0.38), 14.15, 0.78,
                               boxstyle='round,pad=0.05', linewidth=0,
                               edgecolor='none', facecolor=bg, alpha=0.9)
    ax.add_patch(b)

    # Texte tâche
    ax.text(2.5, y, tache, ha='center', va='center',
            fontsize=8.5, color='#2c3e50', multialignment='center')

    # Score Bash
    ax.text(6.4, y, score_bash, ha='center', va='center',
            fontsize=10, color='#e67e22', fontweight='bold')

    # Score Python
    ax.text(9.0, y, score_python, ha='center', va='center',
            fontsize=10, color='#2980b9', fontweight='bold')

    # Recommandation
    reco_c = '#e67e22' if 'Bash' in reco and 'Python' not in reco else (
             '#2980b9' if 'Python' in reco and 'Bash' not in reco else '#8e44ad')
    b_reco = patches.FancyBboxPatch((10.4, y - 0.22), 3.65, 0.44,
                                    boxstyle='round,pad=0.08', linewidth=1.2,
                                    edgecolor=reco_c, facecolor=reco_c, alpha=0.15)
    ax.add_patch(b_reco)
    ax.text(12.225, y, reco, ha='center', va='center',
            fontsize=8.5, color=reco_c, fontweight='bold')

# Légende des étoiles
ax.text(0.1, 0.1, '★ = faible aptitude   ★★★★★ = excellente aptitude',
        fontsize=9, color='#555', style='italic')

plt.tight_layout()
plt.show()
```

## Résumé

Ce chapitre a exploré la complémentarité entre Bash et Python :

- Bash excelle comme **colle entre programmes** : coordination de processus, pipelines de texte, scripts de déploiement, tâches système sans dépendances. Python excelle pour la **logique applicative complexe** : JSON, XML, APIs, tests unitaires, traitement de données.
- Depuis Bash, on peut appeler Python via `python3 -c "..."` pour des expressions inline, ou via des scripts Python autonomes avec shebang.
- La communication entre les deux mondes emprunte trois canaux : les **variables d'environnement** (configuration simple), **stdin/stdout/pipes** (flux de données), les **fichiers temporaires** avec `mktemp` (données volumineuses ou multi-étapes).
- Depuis Python, `subprocess.run()` est l'interface recommandée pour lancer des commandes système, avec `capture_output=True`, `text=True` et `check=True`. `shlex.split()` découpe proprement une chaîne de commande en liste d'arguments.
- `jq` est l'outil de choix pour le traitement JSON en ligne de commande : filtres de base (`.champ`, `.[index]`), `select()`, `map()`, `group_by()`, et construction sûre de JSON avec `jq -n --arg`.

Dans le chapitre suivant, nous clôturons ce livre par les **bonnes pratiques** et ShellCheck, l'outil d'analyse statique indispensable pour écrire du Bash fiable et maintenable.
