Bash et les autres langages#

Hide code cell source

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.

Définition 64 (Domaine de prédilection de Bash)

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 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.

Remarque 52

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 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é :

# 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 :

#!/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'

Exemple 33 (Intégration Bash ↔ Python par shebang)

Un script Python peut être rendu exécutable directement et s’intégrer dans un pipeline Bash comme n’importe quelle commande Unix :

#!/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')
# 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 :

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

python3 mon_script.py
# 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'))

Remarque 53

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 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'
# 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) :

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#

# 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#

#!/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"

Remarque 54

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#

# 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

Hide code cell source

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()
_images/831ce876b5ddda20465cf1e03de6a5460ae6078febf0305a38675e988dcba056.png

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.