Minishell
Interpréteur de commandes UNIX (shell) inspiré de bash, développé en C.
Présentation
Un interpréteur de commandes UNIX qui exécute des commandes, gère les pipes et réagit aux signaux comme le ferait un vrai terminal.
Contexte
Projet du tronc commun de l'école 42, réalisé en binôme avec mon camarade, en 2023.
L'objectif : comprendre ce qui se passe réellement quand on tape une commande dans un terminal : les processus, les descripteurs de fichiers et les signaux.
Mon rôle
Projet réalisé en binôme : j'ai conçu le cœur de l'interpréteur, mon binôme a apporté le découpage des arguments et l'expansion des variables.
- Conception du REPL (boucle lecture-évaluation-exécution) avec
readlinepour le prompt et l'historique - Parsing de la ligne de commande : substitution des variables, espacement des pipes, découpage en arguments et nettoyage des quotes
- Implémentation des builtins et de l'exécution des commandes externes via
fork/exec/wait - Pipes (simples et multiples) et gestion des signaux avec
readline
Architecture
Le REPL (Read - Eval - Print - Loop) est le cycle fondamental de tout shell interactif :
| Étape | Rôle | Dans le code |
|---|---|---|
| Read - Lecture | Le shell lit la commande saisie au clavier | readline() + add_history() |
| Eval - Évaluation | La commande est parsée puis exécutée | parsing() → fork() / execve() |
| Print - Affichage | La sortie s'affiche sur le terminal | dup2() vers STDOUT_FILENO |
| Loop - Boucle | Le shell réaffiche le prompt et attend la saisie suivante | retour au while(1) avec free() |
La boucle tourne jusqu'à ce qu'un signal d'arrêt mette fin au programme : commande exit ou fin de fichier (Ctrl+D).
L'étape Eval correspond au pipeline de parsing, qui enchaîne quatre transformations sur la chaîne lue :
- Expansion des variables (
$VAR,$?) hors zones entre quotes simples - Espacement des caractères
|pour les isoler comme tokens - Découpage en arguments avec un splitter qui ignore les séparateurs entre quotes
- Nettoyage des quotes résiduelles de chaque argument
L'exécution distingue ensuite trois cas : une commande simple, une redirection (>), et un pipe - chaque commande d'un pipe étant lancée dans un processus fork relié à son voisin via dup2, avec une gestion dédiée du dernier maillon de la chaîne.
Technologies
C
Bas-niveau, gestion mémoire manuelle, compilation native
Linux/UNIX
Appels système POSIX : fork, dup2, execve, pipe
LIBFT
bibliothèque personnalisée réimplémentant libc
READLINE
édition de ligne interactive
Makefile
build automatisé
Clang
compilateur LLVM, stricte conformité
Fonctionnalités
- Commandes builtins (cd, echo, pwd, export, unset, env, exit)
- Commandes externes via
PATHet exécutables locaux./programme - Pipes simples et multiples (
|) - Redirection de sortie (
>) - Variables d'environnement (
$VAR) et code de retour ($?) - Gestion des signaux :
Ctrl+C(interrompt la commande, réaffiche le prompt),Ctrl+D(quitte) - Gestion des erreurs
Problème | Solution
| Problème | Solution |
|---|---|
Distinguer un \| dans une chaîne echo "a \| b" d'une vraie pipe | Découpage avec skip_quotes : les séparateurs entre quotes simples ou doubles sont ignorés |
| Interrompre une commande qui tourne sans tuer le shell | Gestion des signaux ciblée : Ctrl+C transmet l'interruption à l'enfant via un handler readline, le shell reste actif |
| Enchaîner plusieurs commandes avec des pipes | Une boucle de fork qui chaîne les descripteurs : dup2 relie la sortie d'un enfant à l'entrée du suivant, dernier maillon géré séparément |
| Empêcher les variables d'être développées dans une chaîne simple-quoted | L'expansion saute explicitement les zones délimitées par '...' |
Résultats
- REPL complet : l'utilisateur tape, le shell exécute
- Builtins fonctionnelles :
cd,echo,pwd,export,unset,env,exit - Signaux correctement gérés :
Ctrl+Cinterrompt, le shell reste vivant - Pipes simples et chaînes multi-commandes
Ce que j'ai appris
J'ai compris le pipeline fork → exec → wait : je vois désormais chaque ls comme un processus qui naît, vit et meurt.
La gestion des signaux m'a appris qu'un programme est défini autant par ce qu'il fait que par ce qu'il refuse de faire, et que le parsing d'entrées utilisateur est un terrain miné, une citation mal fermée fait tout exploser.