minishell · 42 // TECHNICAL GUIDE
SYS:ONLINE REC // 05
Projet 42 · Branche Système · Version 6

minishell

// As beautiful as a shell — recoder bash, un processus à la fois.

Recoder un shell, c'est affronter quatre monstres d'un coup : un tokenizer qui découpe l'input en tokens, un expander qui résout les variables et les quotes, un parser qui construit l'arbre des commandes, et un executor qui fork, pipe et execve. Ce guide décortique chaque étape avec le code réel du projet, du readline() au execve().

Difficulté
★★★★★
Temps estimé
80 – 150 h
Fonctions
readline · fork · pipe · execve · dup2
Sortie
minishell
// Contrainte clé

Le shell doit afficher un prompt, gérer l'historique, rechercher les exécutables via PATH, implémenter 7 builtins (echo, cd, pwd, export, unset, env, exit), gérer les pipes |, les redirections < > >> <<, les variables $VAR et $?, les quotes ' et ", ctrl-C/D/\. Bonus : &&, ||, wildcard *. Maximum une variable globale. Norminette exit 0. Pas de leak.

« Un shell n'est qu'une boucle : lire, découper, étendre, exécuter, répéter. Mais dans cette boucle se cachent tous les pièges de la programmation système — forks, pipes, descripteurs de fichiers, signaux, et l'éternelle question : qui ferme quoi, et quand ? »
— Manuel de terrain YoRHa, module 153
01

Le projet

minishell est le projet le plus ambitieux de la branche système 42. L'objectif : recoder un shell fonctionnel — pas aussi complet que bash, mais suffisamment pour lancer des commandes, gérer les pipes et redirections, étendre les variables d'environnement, et implémenter les builtins essentiels. Le sujet (version 6) autorise readline pour le prompt et l'historique, et la libft.

Le projet suit une architecture classique en 4 étapes : tokenizer (découper l'input en tokens), expander (résoudre $VAR, $?, supprimer les quotes), parser (construire une liste chaînée de t_cmd), et executor (fork, pipe, execve, builtins). Chaque étape a ses propres pièges que ce guide décortique.

Fonctions autorisées

// Liste complète (sujet v6)

readline, rl_clear_history, rl_on_new_line, rl_replace_line, rl_redisplay, add_history, printf, malloc, free, write, access, open, read, close, fork, wait, waitpid, wait3, wait4, signal, sigaction, sigemptyset, sigaddset, kill, exit, getcwd, chdir, stat, lstat, fstat, unlink, execve, dup, dup2, pipe, opendir, readdir, closedir, strerror, perror, isatty, ttyname, ttyslot, ioctl, getenv, tcsetattr, tcgetattr, tgetent, tgetflag, tgetnum, tgetstr, tgoto, tputs

// readline et les leaks

Le sujet dit explicitement : "The readline() function can cause memory leaks. You don't have to fix them." Mais votre propre code ne peut pas avoir de leaks. Utilisez rl_clear_history() à la fin pour nettoyer l'historique, et free() chaque ligne lue par readline().

02

Architecture du code

Le projet suit une architecture modulaire en 4 phases. Chaque phase transforme les données et les passe à la suivante. Cette séparation permet de tester chaque étape indépendamment et de respecter la limite norminette de 5 fonctions par fichier.

// Flux d'exécution : input → tokens → expansion → cmds → execution
INPUT (readline) │ ▼ TOKENIZER ────▶ découpe en tokens (T_WORD, T_PIPE, T_REDIR_*, T_AND, T_OR) │ gère quotes '...' et "..." ▼ SYNTAX CHECK ─▶ vérifie pipes/redirs au mauvais endroit → exit 2 │ ▼ EXPANDER ────▶ résout $VAR, $?, ${VAR} │ supprime quotes (single = no expand, double = expand $) │ wildcard * expansion ▼ PARSER ──────▶ construit t_cmd list depuis tokens │ op_before = T_AND/T_OR/T_PIPE pour &&, ||, | ▼ EXECUTOR ────▶ fork + pipe + execve pour commandes externes │ builtins exécutés dans le parent (pas de fork) │ && : skip si exit_status != 0 │ || : skip si exit_status == 0 ▼ OUTPUT + EXIT STATUS

Structures de données

includes/minishell.h// structures
typedef enum e_token_type
{
	T_WORD, T_PIPE, T_REDIR_IN, T_REDIR_OUT,
	T_REDIR_APPEND, T_REDIR_HEREDOC,
	T_AND, T_OR, T_LPAREN, T_RPAREN, T_EOF
}				t_token_type;

typedef struct s_token      /* un token de l'input */
{
	char			*value;     /* texte du token (NULL pour opérateurs) */
	t_token_type	type;       /* type du token */
	struct s_token	*next;
}				t_token;

typedef struct s_redir      /* une redirection */
{
	t_token_type	type;       /* < > >> << */
	char			*file;      /* nom du fichier / délimiteur */
	struct s_redir	*next;
}				t_redir;

typedef struct s_cmd        /* une commande dans le pipeline */
{
	char			**argv;     /* arguments (argv[0] = commande) */
	t_redir		*redirs;    /* liste chaînée de redirections */
	int			op_before;  /* opérateur AVANT cette commande (T_AND/T_OR/T_PIPE) */
	struct s_cmd	*next;
}				t_cmd;

typedef struct s_shell      /* état global du shell */
{
	char	**env;              /* copie de l'environnement */
	int	exit_status;        /* $? — statut de la dernière commande */
	int	running;            /* 1 = shell actif, 0 = exit demandé */
}				t_shell;

Le champ op_before dans t_cmd est crucial : il stocke l'opérateur qui précède cette commande dans l'input. Par exemple, dans true && echo yes, la commande echo a op_before = T_AND. Cela permet à l'executor de savoir s'il doit skipper cette commande (si && et exit_status != 0).

Flux principal

srcs/main.c// boucle principale
while (shell.running)
{
	input = readline(PROMPT);       /* affiche le prompt */
	if (input == NULL)               /* ctrl-D = EOF */
	{
		write(1, "exit\n", 5);
		break ;
	}
	if (input[0] != '\0')
		ms_process_input(input, &shell);
	free(input);                     /* pas de leak */
}
// isatty pour les testers

Les testers (zstenger, kichkiro) envoient les commandes via pipe. Dans ce cas, isatty(STDIN_FILENO) retourne 0. On utilise alors get_next_line au lieu de readline (pas de prompt, pas d'echo) pour matcher le comportement de bash en mode pipé.

03

Tokenizer

Le tokenizer découpe l'input en une liste chaînée de tokens. Il doit reconnaître les mots, les pipes |, les redirections < > >> <<, les opérateurs logiques && ||, les parenthèses, et gérer les quotes simples et doubles (qui empêchent la découpe sur les espaces).

Quotes et opérateurs

srcs/tokenizer_helpers.c// read_word
int	read_word(t_token **tokens, char *input, int *i)
{
	int		start;
	char	*word;

	start = *i;
	while (input[*i] != '\0' && input[*i] != ' ' && input[*i] != '\t'
		&& input[*i] != '|' && input[*i] != '<' && input[*i] != '>'
		&& input[*i] != '&' && input[*i] != '(' && input[*i] != ')')
	{
		if (input[*i] == '\'' || input[*i] == '"')
		{
			if (read_quoted(input, i, input[*i]) != 0)
				return (1);
		}
		else
			(*i)++;
	}
	if (*i > start)
	{
		word = ft_strndup(input + start, *i - start);
		add_token(tokens, new_token(word, T_WORD));
	}
	return (0);
}

La fonction read_quoted avance jusqu'à trouver la quote fermante. Si la quote n'est pas fermée, c'est une erreur. Les quotes sont conservées dans la valeur du token — c'est l'expander qui les supprimera plus tard, car il doit savoir si le contenu était en single ou double quote pour décider d'étendre ou non les $VAR.

Syntax errors

Le sujet (version 6) ne demande pas explicitement de gérer les syntax errors, mais bash le fait et les testers vérifient. Notre implémentation détecte les cas suivants et retourne exit code 2 (comme bash) :

InputErreur bashNotre comportement
|syntax error near |✅ exit 2
||syntax error near ||✅ exit 2
echo |syntax error near newline✅ exit 2
>syntax error near newline✅ exit 2
echo >syntax error near newline✅ exit 2
echo > <syntax error near <✅ exit 2
srcs/syntax_check.c// ms_check_syntax
static int	is_operator(t_token_type type)
{
	if (type == T_PIPE || type == T_AND || type == T_OR)
		return (1);
	return (0);
}

static int	is_redir(t_token_type type)
{
	if (type == T_REDIR_IN || type == T_REDIR_OUT
		|| type == T_REDIR_APPEND || type == T_REDIR_HEREDOC)
		return (1);
	return (0);
}

int	ms_check_syntax(t_token *tokens)
{
	t_token	*cur;
	t_token	*prev;

	if (tokens == NULL)
		return (0);
	cur = tokens;
	prev = NULL;
	while (cur != NULL)
	{
		if (is_operator(cur->type) && (prev == NULL
				|| is_operator(prev->type) || is_redir(prev->type)))
			return (ms_syntax_error_msg(...));
		if (is_redir(cur->type) && (cur->next == NULL
				|| cur->next->type != T_WORD))
			return (ms_syntax_error_msg(...));
		prev = cur;
		cur = cur->next;
	}
	if (prev != NULL && is_operator(prev->type))
		return (ms_syntax_error_msg("newline"));
	return (0);
}
// Le message d'erreur

Le message suit le format bash : minishell: syntax error near unexpected token `X' sur stderr, avec exit code 2. Le tester zstenger compare ce message avec celui de bash — il doit être identique (ou très proche).

04

Expander

L'expander parcourt les tokens de type T_WORD et résout les variables d'environnement. C'est l'étape la plus subtile du projet : il faut respecter les règles de quotes de bash — les single quotes empêchent toute expansion, les double quotes permettent l'expansion de $ mais pas des autres métacaractères.

$VAR et $?

srcs/expander.c// expand_dollar
char	*expand_dollar(char *str, int *i, t_shell *shell)
{
	char	*name;
	int		start;
	int		braces;

	braces = 0;
	(*i)++;
	if (str[*i] == '{' && (*i)++)   /* ${VAR} syntax */
		braces = 1;
	if (str[*i] == '?')            /* $? = exit status */
	{
		(*i)++;
		return (ft_itoa(shell->exit_status));
	}
	start = *i;
	while (str[*i] != '\0' && (ft_isalnum(str[*i]) || str[*i] == '_'))
		(*i)++;
	if (braces && str[*i] == '}')
		(*i)++;
	if (*i == start)
		return (ft_strdup("$"));    /* $ seul = littéral */
	name = ft_strndup(str + start, *i - start - braces);
	return (get_var_value(name, shell));
}

Quote-aware expansion

Le cœur de l'expander est process_token_expansion qui parcourt la valeur du token caractère par caractère, en trackant le contexte de quote. Si on est inside single quotes, $ n'est pas expansé. Si on est inside double quotes ou hors quotes, $ est expansé.

// Règles d'expansion selon le contexte de quote
echo '$HOME'$HOME (single quote: PAS d'expansion) echo "$HOME"/home/z (double quote: expansion $ activée) echo $HOME/home/z (hors quote: expansion $ activée) echo ${HOME}/home/z (accolades: expansion $ activée) echo $?0 (exit status de la dernière commande) echo "'$USER'"'z' (double outside, single inside: $USER expansé) echo '"$USER"'"$USER" (single outside, double inside: PAS d'expansion) Règle: seul le contexte de la quote la plus externe compte. Une single quote à l'intérieur d'une double quote n'a aucun effet sur $.

Wildcard *

Le wildcard * est un bonus. Il doit étendre les noms de fichiers dans le répertoire courant, triés alphabétiquement (comme bash). On utilise opendir/readdir pour lister les fichiers, et un pattern matching récursif pour vérifier si chaque fichier correspond au pattern.

srcs/wildcard_utils.c// match_pattern
int	match_pattern(char *pattern, char *str)
{
	if (*pattern == '\0' && *str == '\0')
		return (1);
	if (*pattern == '*')
	{
		while (*pattern == '*')
			pattern++;
		if (*pattern == '\0')
			return (1);           /* * à la fin = match tout */
		while (*str != '\0')
		{
			if (match_pattern(pattern, str))
				return (1);
			str++;
		}
		return (match_pattern(pattern, str));
	}
	if (*pattern == '\0' || *str == '\0')
		return (0);
	if (*pattern == *str)
		return (match_pattern(pattern + 1, str + 1));
	return (0);
}
// Fichiers cachés

Bash n'étend pas * vers les fichiers cachés (commençant par .) sauf si le pattern commence explicitement par .. Notre implémentation respecte cette règle : if (entry->d_name[0] != '.' || pattern[0] == '.').

05

Parser

Le parser transforme la liste de tokens en une liste chaînée de t_cmd. Chaque t_cmd représente une commande avec ses arguments, ses redirections, et l'opérateur qui la précède (op_before). Les tokens de type opérateur (T_PIPE, T_AND, T_OR) ne sont pas stockés dans la commande mais dans op_before de la commande suivante.

srcs/parser.c// ms_parse
int	ms_parse(t_cmd **cmds, t_token *tokens)
{
	t_cmd	*cur;
	t_cmd	*new;
	int		op_before;

	*cmds = NULL;
	cur = NULL;
	op_before = 0;
	while (tokens != NULL)
	{
		if (parse_command(&new, &tokens, op_before) != 0)
			return (1);
		if (new->argv == NULL && new->redirs == NULL)
		{
			free(new);
			break ;
		}
		if (*cmds == NULL)
			*cmds = new;
		else
			cur->next = new;
		cur = new;
		advance_op(&tokens, &op_before);  /* stocke T_AND/T_OR/T_PIPE */
	}
	return (0);
}
// op_before = opérateur AVANT la commande

Dans true && echo yes, la commande true a op_before = 0 (première commande), et la commande echo a op_before = T_AND. L'executor utilise cette valeur pour décider de skipper ou non la commande selon exit_status.

06

Executor

L'executor est le cœur du shell. Il parcourt la liste de commandes, fork pour les commandes externes, gère les pipes entre commandes, et exécute les builtins dans le processus parent (sans fork) quand il n'y a pas de pipe. Le pattern classique : fork → dup2 (redir) → execve pour chaque commande, avec pipe pour connecter stdout d'une commande au stdin de la suivante.

Pipelines

// Pipeline : echo hello | cat | wc -c
Processus parent : fork() → child 1 (echo hello) fork() → child 2 (cat) fork() → child 3 (wc -c) wait() × 3 Descripteurs de fichiers : child 1: stdin=tty, stdout=pipe1[1] ──▶ "hello\n" ──▶ child 2: stdin=pipe1[0], stdout=pipe2[1] ──▶ "hello\n" ──▶ child 3: stdin=pipe2[0], stdout=tty ──▶ "6\n" Règle: chaque child ferme TOUS les fd de pipe qu'il n'utilise pas. Le parent ferme TOUS les fd de pipe après les forks.
srcs/executor.c// run_pipeline_loop
int	run_pipeline_loop(t_cmd **cmds, t_shell *shell, int count)
{
	t_cmd	*cur;
	int		prev_fd;
	int		pipefd[2];
	int		i;

	cur = *cmds;
	prev_fd = -1;
	i = -1;
	while (++i < count)
	{
		if (i < count - 1 && do_fork(cur, shell, &prev_fd, pipefd))
			return (1);
		if (i >= count - 1 && do_fork(cur, shell, &prev_fd, NULL))
			return (1);
		if (i < count - 1)
			prev_fd = pipefd[0];
		cur = cur->next;
	}
	if (prev_fd >= 0)
		close(prev_fd);
	*cmds = cur;
	return (0);
}

Builtins dans le parent

Quand il n'y a qu'une seule commande et que c'est un builtin, on l'exécute directement dans le processus parent — sans fork. C'est essentiel pour cd (qui doit modifier le répertoire du parent) et export/ unset (qui doivent modifier l'environnement du parent). On sauvegarde stdin/stdout, on applique les redirections, on exécute le builtin, puis on restaure stdin/stdout.

srcs/executor_helpers.c// exec_builtin_in_parent
int	exec_builtin_in_parent(t_cmd *cmd, t_shell *shell)
{
	int	saved_in;
	int	saved_out;
	int	ret;

	saved_in = dup(STDIN_FILENO);
	saved_out = dup(STDOUT_FILENO);
	if (ms_redirect(cmd->redirs) != 0)
		ret = 1;
	else
		ret = ms_exec_builtin(cmd, shell);
	dup2(saved_in, STDIN_FILENO);
	dup2(saved_out, STDOUT_FILENO);
	close(saved_in);
	close(saved_out);
	return (ret);
}

Court-circuit && et ||

srcs/executor_helpers.c// ms_execute
int	ms_execute(t_cmd *cmds, t_shell *shell)
{
	t_cmd	*cur;
	int		skip;

	cur = cmds;
	while (cur != NULL)
	{
		skip = 0;
		if (cur->op_before == T_AND && shell->exit_status != 0)
			skip = 1;              /* && : skip si échec */
		else if (cur->op_before == T_OR && shell->exit_status == 0)
			skip = 1;              /* || : skip si succès */
		if (!skip)
			exec_pipeline(&cur, shell);
		else
			cur = cur->next;
	}
	return (0);
}
07

Builtins

Les 7 builtins obligatoires : echo (avec option -n), cd (chemin relatif ou absolu), pwd (sans option), export (sans option), unset (sans option), env (sans option ni argument), exit (sans option). Chaque builtin retourne un exit status qui devient $?.

BuiltinComportementExit status
echoAffiche ses arguments séparés par des espaces. -n supprime le newline final.0
cdChange le répertoire courant. Sans argument, va vers $HOME.0 si OK, 1 si erreur
pwdAffiche le répertoire courant via getcwd.0
exportAjoute/modifie une variable d'environnement. Sans argument, affiche toutes les variables triées.0
unsetSupprime une variable d'environnement.0
envAffiche toutes les variables d'environnement.0
exitQuitte le shell avec le code donné (ou $? si sans argument).code donné
// cd doit modifier le parent

cd est le builtin qui justifie l'exécution dans le parent : si on fork, le chdir ne modifie que le processus enfant, et le shell reste dans son répertoire courant. C'est pourquoi les builtins sont exécutés sans fork quand ils sont seuls (pas de pipe). Dans un pipeline cd /tmp | echo ok, cd est exécuté dans un child et n'affecte pas le parent — c'est le comportement de bash.

08

Redirections

Les 4 redirections obligatoires : < (input), > (output truncate), >> (output append), << (heredoc). Elles sont appliquées via dup2 après le fork, avant l'execve.

RedirectionFlag opendup2 cible
< fileO_RDONLYdup2(fd, STDIN)
> fileO_WRONLY | O_CREAT | O_TRUNCdup2(fd, STDOUT)
>> fileO_WRONLY | O_CREAT | O_APPENDdup2(fd, STDOUT)
<< delimpipe internedup2(fd, STDIN)

Le heredoc << est particulier : il crée un pipe interne, lit l'input ligne par ligne avec readline("> ") jusqu'à trouver le délimiteur, écrit tout dans le pipe, puis redirige stdin vers l'autre bout du pipe.

srcs/redirections.c// ms_heredoc
int	ms_heredoc(char *delimiter)
{
	char	*line;
	int		fd[2];

	if (pipe(fd) < 0)
		return (ms_perror("pipe"));
	while (1)
	{
		line = readline("> ");
		if (line == NULL)
			break ;
		if (ft_strcmp(line, delimiter) == 0)
		{
			free(line);
			break ;
		}
		write(fd[1], line, ft_strlen(line));
		write(fd[1], "\n", 1);
		free(line);
	}
	close(fd[1]);
	return (fd[0]);   /* retourne le fd de lecture */
}
09

Libft & compatibilité

Le projet utilise la libft de l'utilisateur (auteur : rakrouna, 111 fichiers source). Comme cette libft a une API légèrement différente de la libft standard 42, un fichier de compatibilité ms_libft_compat.c fournit des wrappers pour les fonctions manquantes ou avec signatures différentes.

Fonction requiseLibft utilisateurWrapper
ft_split(s, c)ft_strsplit(s, c)Wrapper direct
ft_substr(s, start, len)ft_strsub(s, start, len)Wrapper avec cast
ft_calloc(count, size)ft_memalloc(size) + ft_bzeroCompose 2 fonctions
ft_atol(str)Non présenteImplémentation propre
get_next_line(fd, line)get_next_line(fd, line, pitcher) (3 args)ms_gnl.c — GNL propre (2 args)
ft_strtrim(s, set)ft_strtrim(s) (1 arg, trim whitespace)trim_newline() dans read_input.c
// Pourquoi ms_gnl.c ?

La libft de l'utilisateur a un get_next_line avec 3 arguments (fd, line, pitcher) — une API non-standard. Notre code utilise l'API standard à 2 arguments (fd, line). Plutôt que de modifier la libft de l'utilisateur, on crée ms_gnl.c avec notre propre GNL, et on commente la déclaration dans libft.h pour éviter le conflit de types. Le Makefile de la libft est modifié pour ne pas compiler get_next_line.c.

10

Audit fiche d'évaluation

Critère ficheStatutDétail
Compilation -Wall -Wextra -Werrormake -n montre les flags
Pas de re-linkmake 2× → "Nothing to be done"
Commande simple /bin/lsRecherche via PATH + chemin absolu
Commande vide / espacesPas de crash
0 variable globale (ressources)Tout en structures + paramètres
echo avec/sans -n
exit avec/sans argumentAffiche "exit" en interactif seulement
cd (chemin absolu/relatif)Maj de $PWD
pwdgetcwd
envAffiche shell->env
export (avec/sans args)Sans args : affiche trié avec declare -x
unsetSupprime de shell->env
Pipes |fork + pipe + dup2
Redirections < > >>open + dup2
Heredoc <<Pipe interne + readline
$VAR expansionHors single quotes
$? exit statusshell->exit_status
Single quotes (no expand)echo '$HOME'$HOME
Double quotes (expand $)echo "$HOME"/home/z
ctrl-C (new prompt)sigaction + rl_on_new_line
ctrl-D (exit)readline retourne NULL
ctrl-\ (nothing)SIG_IGN
Surprise: echo "'$USER'"'z'
Surprise: echo '"$USER"'"$USER"
Bonus: && ||Court-circuit via op_before
Bonus: wildcard *opendir/readdir + tri
Norminette exit 0Tous les fichiers srcs + header
Libft de l'utilisateurAuteur: rakrouna, compilée via son Makefile
11

Makefile

Le Makefile compile la libft via son propre Makefile, puis compile les sources de minishell avec les flags -Wall -Wextra -Werror et linke avec libreadline. Il contient les règles $(NAME), all, clean, fclean, re.

Makefile// extraits clés
NAME        = minishell
CC          = cc
CFLAGS      = -Wall -Wextra -Werror -g
LDFLAGS     = /usr/lib/x86_64-linux-gnu/libreadline.so.8

LIBFT_DIR   = libft
LIBFT       = $(LIBFT_DIR)/libft.a
INCLUDES    = -I includes -I $(LIBFT_DIR)

# Compilation par .o (incrémental, pas de re-link)
%.o:        %.c $(HEADERS)
            $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@

# Link final avec libft + readline
$(NAME):    $(OBJS) $(LIBFT) $(HEADERS)
            $(CC) $(CFLAGS) $(INCLUDES) $(OBJS) $(LIBFT) $(LDFLAGS) -o $(NAME)

# Compile la libft via son propre Makefile
$(LIBFT):
            make -C $(LIBFT_DIR)
// readline linking

Sur les machines 42, libreadline-dev est installé et -lreadline suffit. Sur notre environnement, nous avons dû télécharger les headers et linker directement vers libreadline.so.8. Sur une machine 42 standard, remplacez LDFLAGS = /usr/lib/.../libreadline.so.8 par LDFLAGS = -lreadline.

12

Pièges & edge cases

PiègeSymptômeSolution
readline corrompt stdin pipéPremier caractère mangéUtiliser GNL en mode non-interactif (isatty)
Builtin dans un pipe ne modifie pas le parentcd /tmp | echo ok ne change pas le dirComportement bash : builtin forké dans un pipe. C'est normal.
Single quote expand $VARecho '$HOME' affiche /home/z au lieu de $HOMETracker le contexte de quote dans l'expander
Syntax errors sans exit code 2Tester zstenger : KO sur |, > seulms_check_syntax retourne 2 + message bash
"exit" affiché en mode pipéDiff avec bash (bash n'affiche pas "exit" en pipé)if (isatty(STDIN_FILENO)) avant write("exit")
fd leak dans les pipesProcessus zombie, pipe bloquéFermer tous les fd non utilisés dans parent ET children
Command not found exit codeBash retourne 127exit(127) dans exec_external_child
${VAR} non supportéAffiche ${HOME} littéralementDétecter { après $ et skip à }
Wildcard ne trie pasOrdre non-alphabétiquesort_arr (bubble sort) avant insertion
GNL de la libft a 3 argsConflit de typesms_gnl.c avec GNL 2-args, commenter déclaration dans libft.h
Heredoc sans délimiteurBoucle infiniereadline retourne NULL sur EOF → break
Norminette > 25 lignes/fonctionErreur normExtraire helpers statiques, utiliser j++ inline, ft_calloc au lieu de malloc
« Un shell n'est jamais fini. Il y a toujours un edge case, un pipe mal fermé, une quote non équilibrée, un signal mal géré. Mais c'est dans cette quête d'exhaustivité que l'on apprend vraiment comment fonctionne un système UNIX. »
— Commandant White, briefing pré-mission