infra / linux-bash
Bash ultra-expert : coproc et exec avancé
Explication
Ce que vous allez apprendre
- Comprendre la limite d'un pipe classique (sens unique, une seule fois)
- Ouvrir une conversation bidirectionnelle avec un processus grâce à
coproc - Distinguer les deux usages du mot-clé
exec(redirection vs remplacement de processus) - Comprendre pourquoi un entrypoint Docker doit se terminer par
exec - Ouvrir une connexion réseau brute avec
/dev/tcp, sans installernetcat
Dans quel contexte ?
Tu lis le script entrypoint.sh d'une image Docker officielle et tu tombes sur une ligne qui se termine par exec "$@", sans comprendre pourquoi ce mot-clé est là plutôt qu'un simple appel de commande.
D'abord, la limite d'un pipe classique
Un pipe (|) relie deux commandes dans un seul sens, une seule fois : la sortie de la première devient l'entrée de la seconde, sans retour possible. coproc va plus loin en ouvrant un canal bidirectionnel vers un processus qui reste actif en arrière-plan.
Voici à quoi ça ressemble concrètement
coproc MONPROC { bc -l; } lance une calculatrice en arrière-plan ; echo "3.14159 * 2" >&"${MONPROC[1]}" lui envoie un calcul, et read -r resultat <&"${MONPROC[0]}" récupère sa réponse. C'est une vraie conversation continue avec le processus, pas un échange à sens unique.
Une fois coproc compris, voyons un second mécanisme : exec sans commande
exec 3< fichier_entree.txt ouvre un descripteur de fichier numéro 3 qui reste disponible pour tout le reste du script, pas seulement pour une commande isolée. exec 3<&- le referme explicitement quand il n'est plus nécessaire.
Il reste une confusion fréquente : le même mot "exec" fait deux choses très différentes
Utilisé avec une redirection, exec modifie les canaux du shell courant, comme on vient de le voir. Utilisé avec une commande, comme exec uvicorn app.main:app --port 8000, il remplace complètement le processus shell actuel par ce programme — sans créer de nouveau processus, avec le même PID.
Usage de exec | Exemple | Effet |
|---|---|---|
| Avec une redirection | exec 3< fichier.txt | Ouvre un descripteur pour tout le reste du script |
| Avec une commande | exec uvicorn app.main:app | Remplace le shell actuel, même PID |
Pourquoi cette distinction compte en pratique
Dans un script entrypoint.sh de conteneur Docker, terminer par exec évite un processus shell intermédiaire inutile qui resterait entre le conteneur et l'application réelle. Sans exec, ce shell intermédiaire complique la réception propre des signaux d'arrêt envoyés au conteneur.
Réflexe à adopter
Termine toujours un script entrypoint.sh de conteneur Docker par exec "$@" : ça évite un processus shell intermédiaire inutile et permet au conteneur de recevoir proprement les signaux d'arrêt (SIGTERM).
Un dernier tour de force : une connexion réseau sans netcat
exec 7<>/dev/tcp/exemple.com/80 ouvre une connexion TCP directement comme un descripteur de fichier bidirectionnel, sans installer aucun outil supplémentaire — une fonctionnalité propre à bash, absente du shell POSIX classique.
Le piège à connaître
Ces mécanismes sont rarement nécessaires au quotidien, mais les reconnaître évite d'être perdu face à un script professionnel qui les emploie, notamment dans les entrypoints Docker que tu retrouveras dans le cours dédié à Docker.
Commandes & code
Bash ultra-expert : coproc et exec avancé
#!/usr/bin/env bash
# --- coproc : lance une commande en arrière-plan avec des pipes bidirectionnels vers le shell ---
coproc MONPROC { bc -l; }
# MONPROC[0] = descripteur de LECTURE (stdout du process), MONPROC[1] = descripteur d'ÉCRITURE (stdin du process)
echo "3.14159 * 2" >&"${MONPROC[1]}" # envoie une commande au process bc en arrière-plan
read -r resultat <&"${MONPROC[0]}" # lit sa réponse
echo "Résultat : $resultat"
echo "quit" >&"${MONPROC[1]}"
wait "$MONPROC_PID" # attend proprement la fin du coprocess
# --- coproc anonyme (nom par défaut : COPROC) ---
coproc { tail -f /var/log/syslog; }
while read -r ligne <&"${COPROC[0]}"; do
[[ "$ligne" == *ERROR* ]] && echo "ALERTE: $ligne"
done
# --- exec sans commande : modifie les descripteurs de fichiers du SHELL COURANT, durablement ---
exec 3< fichier_entree.txt # ouvre le fd 3 en lecture, reste ouvert pour tout le reste du script
exec 4> fichier_sortie.log # ouvre le fd 4 en écriture
while read -r ligne <&3; do
echo "traité: $ligne" >&4
done
exec 3<&- # ferme explicitement le fd 3
exec 4<&- # ferme explicitement le fd 4
# --- Rediriger stdout/stderr de TOUT le reste du script en une seule ligne ---
exec > script.log 2>&1
echo "Cette ligne va dans script.log, plus dans le terminal"
# Pour revenir au terminal : sauvegarder les fd d'origine AVANT la redirection
exec 5>&1 6>&2 # sauvegarde stdout/stderr originaux sur 5 et 6
exec > script.log 2>&1
echo "dans le fichier"
exec 1>&5 2>&6 # restaure stdout/stderr d'origine
echo "de nouveau dans le terminal"
# --- exec AVEC commande : REMPLACE le process shell courant (pas de fork, même PID) ---
# Utile en fin de script wrapper : évite un process zombie intermédiaire inutile
exec uvicorn app.main:app --host 0.0.0.0 --port 8000
# Tout code après cette ligne ne s'exécute JAMAIS : le shell a été remplacé
# --- Socket TCP brut sans netcat, via un pseudo-device bash ---
exec 7<>/dev/tcp/exemple.com/80 # ouvre une connexion TCP comme un fd bidirectionnel
printf 'GET / HTTP/1.1\r\nHost: exemple.com\r\nConnection: close\r\n\r\n' >&7
cat <&7 # lit la réponse HTTP brute
exec 7<&-
# --- Here-string combiné à une substitution de process pour parser en une passe ---
donnees=$(curl -fsS https://api.exemple.com/status)
jq -r '.services[] | select(.status != "up") | .name' <<< "$donnees"
# --- Verrouillage de section critique via flock sur un descripteur dédié ---
exec 9>/var/lock/mon-script.lock
if ! flock -n 9; then
echo "Une autre instance tourne déjà" >&2
exit 1
fi
# Le verrou est libéré automatiquement à la fermeture du fd 9 (fin de script ou exec explicite)Résumé
coprocouvre un pipe bidirectionnel vers une commande d'arrière-plan (NOM[0]lecture,NOM[1]écriture).exec 3<fichier/exec 3<&-gèrent des descripteurs de fichiers custom qui survivent au-delà d'une seule commande.exec commande(sans redirection) remplace le process shell courant — zéro fork, PID identique, pratique en dernière ligne d'un entrypoint.
Exercices pratiques
Mission : comprendre un entrypoint Docker énigmatique
Objectif : Manipuler coproc et les deux usages d'exec pour comprendre un entrypoint Docker professionnel.
Contexte
Tu lis le script entrypoint.sh d'une image Docker officielle et tu tombes sur une ligne qui se termine par exec "$@", sans comprendre pourquoi ce mot-clé est là plutôt qu'un simple appel de commande. Tu dois aussi manipuler coproc pour comprendre ce qu'un pipe classique ne permet pas.