Le journal de MahirMinishell : ce que j'ai appris en recodant un shell en C
A Epitech, on m'a demande de recoder un shell en C. Pas un jouet : un vrai shell qui imite le comportement de TCSH, avec ses pipes, ses redirections, ses variables d'environnement, son historique et son globbing. Le projet s'appelle 42 SH, mais dans ma tete il restera toujours "minishell".
Avant ce projet, le terminal etait une boite noire. Je tapais ls -l | grep .c > liste.txt sans jamais me demander ce qui se passait vraiment. Apres, je ne l'ai plus jamais regarde de la meme facon.
Tout commence par un parseur
Avant d'executer quoi que ce soit, il faut comprendre ce que l'utilisateur a tape. Et une ligne de commande, c'est beaucoup plus piege qu'il n'y parait.
Les espaces multiples, les guillemets, les caracteres speciaux, les pipes qui s'enchainent : chaque cas particulier est un trou potentiel. J'ai passe plus de temps sur le parsing que sur l'execution. La lecon : la moitie d'un interpreteur, c'est comprendre l'entree correctement.
fork, execve et le saut dans l'inconnu
Le coeur d'un shell, c'est ce trio : creer un processus enfant, remplacer son image par le programme demande, attendre qu'il finisse.
pid_t pid = fork();
if (pid == 0) {
// processus enfant : on devient la commande
execve(path, argv, envp);
// si execve reussit, cette ligne n'est jamais atteinte
}
// processus parent : on attend l'enfant
waitpid(pid, &status, 0);
La premiere fois que fork() a marche, ca m'a fait bizarre. Un seul appel, et soudain le programme existe en double. Comprendre que l'enfant herite de tout l'etat du parent, puis se fait ecraser par execve, c'est le declic du projet.
Les pipes, ou l'art de rediriger des tuyaux
Une commande comme cat fichier | grep mot, c'est deux processus relies par un tuyau. La sortie du premier devient l'entree du second.
Concretement, il faut creer un pipe, puis rediriger les bons descripteurs de fichiers avec dup2 dans chaque enfant, et surtout fermer ceux qu'on n'utilise pas. C'est la que j'ai compris pourquoi les fuites de descripteurs sont si vicieuses : ca marche sur trois commandes, ca bloque sur trente, et le bug ne ressemble a rien.
La memoire qui ne pardonne pas
En C, personne ne ramasse vos miettes. Chaque malloc doit avoir son free, sinon la memoire fuit. Sur un shell qui tourne en boucle et lance des centaines de commandes, la moindre fuite finit par se voir.
valgrind est devenu mon meilleur ami et mon pire juge. Voir "All heap blocks were freed -- no leaks are possible" apres une session de traque, c'est une satisfaction que seuls les developpeurs C comprennent vraiment.
Les signaux, ou comment gerer Ctrl-C proprement
Quand on fait Ctrl-C dans un terminal, on s'attend a ce que ca tue la commande en cours, pas le shell lui-meme. Gerer ca correctement demande de bien comprendre quel processus recoit le signal et comment y reagir sans tout casser.
C'est un detail invisible quand ca marche, et insupportable quand ca rate. Beaucoup de finition d'un shell vit dans ces comportements qu'on remarque seulement quand ils sont absents.
Ce que je retiens
Recoder un shell, c'est faire un saut dans la couche que la plupart des developpeurs ne touchent jamais. J'en sors avec une intuition concrete de ce qu'est un processus, un descripteur de fichier, un signal.
Mais la vraie lecon est ailleurs : les outils qu'on utilise tous les jours sont faits de decisions. Quelqu'un a choisi comment le shell parse une ligne, gere une erreur, ferme un tuyau. Maintenant, ce quelqu'un, ca a aussi ete moi.
Le projet vit dans la section projets de ce site, avec sa demo et sa documentation. Si vous aimez le C qui ne fuit pas, vous devriez y jeter un oeil.