Passing the Gate. Minishell is widely seen as the main threshold of 42: the first big systems project where you must design, test, and debug a real UNIX program under strict constraints (no leaks, precise signals, exact exit codes, brittle specs). Many students struggle not because it’s “hard C,” but because small behavioral details matter and are tested. Written in a group together with https://github.com/grignetta
- Goal: Re‑implement a tiny Bash‑like shell.
- You must get right: parsing, quoting, environment/expansion, redirections, pipelines, builtins, signals (Ctrl‑C/D/), heredocs, and exit codes.
- Why this project matters: It forces you to build a clean architecture (lexer → parser → executor) and to develop production habits: test harnesses, robust error handling, and clean resource management.
- Prompt loop using readline with history.
- Lexer/Parser producing a parse tree (or equivalent) that supports:
- Words, operators (
|,<,>,<<,>>), and correct precedence. - Quotes: single quotes = literal, double quotes = expand
$&$?.
- Words, operators (
- Expansion:
$VAR,$?(last exit status), rules with quotes. - Redirections:
>,>>,<,<<(heredoc with optional expansion). - Pipelines:
cmd1 | cmd2 | ...with correct FD wiring. - Builtins implemented in‑process when appropriate:
echo(with-n),cd,pwd,export,unset,env,exit.
- Signals & termios behavior matching Bash‐like expectations.
- No memory leaks over long interactive sessions; correct cleanup for all code paths
include/ # headers
src/
main/ # entry, init, signals, errors
env/ # env storage & manipulation
lexer/ # tokenization
parser/ # grammar -> AST
expand/ # quotes & $ expansion
exec/ # executor, redirs, pipes
heredoc/ # heredoc management
builtins/ # builtins implementations
path/ # PATH resolution
free/ # cleanup helpers
printf_fd/ # libprintf_fd.a (ft_printf_fd & helpers)
libft/ # standard library with additional functions for this project (libft.a)
tools/suppress/ # sanitizer suppression files (readline, etc.)
- Executor rule of thumb:
- Single builtin with redirs → run in parent (so it can change shell state).
- Pipelines / external commands → run in children.
# clone
git clone https://github.com/michaela811/minishell.git minishell
cd minishell
# build
make
# run
./minishellmake– buildminishell,libft.a, andlibprintf_fd.a.make clean|fclean|re– standard hygiene.
Getting Ctrl keys right is a common failure point. The parent shell and its children must behave differently.
- At prompt (no child running):
- Effect: print a newline, redisplay prompt, don’t exit.
- Exit status becomes
130for$?. - Implementation: handle SIGINT in the parent (reset current line via readline hooks), do not terminate the shell.
- While a child runs:
- The child receives SIGINT with default disposition and terminates.
- The parent prints a newline and sets last status to
130(128 + SIGINT). The shell itself stays alive and shows the next prompt.
What this means in practice
- The terminal sends SIGINT to the foreground process group (your shell + its children).
- To avoid killing the shell, parent installs a custom handler (or temporarily ignores SIGINT) while it waits for the child.
- The child must have
SIGINT/SIGQUITrestored to default beforeexecve()so external programs behave normally. - After
waitpid(), if the child died from SIGINT, set$? = 130and print a newline to keep the prompt on a fresh line.
Pipelines
- All children in the pipeline receive SIGINT. Wait for each PID and set
$?to the last command’s status (commonly130if the pipeline was interrupted).
- Ctrl‑C at prompt:
- Run
./minishell. - Press Ctrl‑C once: you should see a newline + fresh prompt.
- Type
echo $?→ expect130.
- Run
- Ctrl‑C while child runs:
- Type
sleep 5and press Enter. - Press Ctrl‑C → the sleep ends immediately.
- Type
echo $?→ expect130.
- Type
- Ctrl‑\ at prompt:
- Press *Ctrl‑* → nothing should print, prompt remains.
echo ok→ printsok.$?unchanged.
- Ctrl‑\ while child runs:
- Run
catand press Enter (it waits on stdin). - Press *Ctrl‑* → you should see
Quit: 3and return to prompt. echo $?→ expect131.
- Run
- Ctrl‑D at prompt (EOF):
- From a fresh
./minishell, press Ctrl‑D on an empty line. - Shell prints
exitand terminates. - In your outer shell, run
echo $?→ expect previous minishell status (often0).
- From a fresh
- Run:
sleep 5 | cat→ press Ctrl‑C. - Expect all children to terminate and
$?to reflect the last command (typically130).
-
Unquoted delimiter (expands):
cat <<EOF $USER $? EOFShould expand
$USER/$?inside heredoc. -
Quoted delimiter (no expand):
cat <<'EOF' $USER $? EOFShould print literal
$USER $?.
- Delimiter quoting controls expansion:
- Unquoted delimiter → expand
$VARand$?inside heredoc. - Quoted delimiter → no expansion.
- Unquoted delimiter → expand
0– success.126– found but not executable (permissions / directory exec attempt).127– command not found.130– terminated by SIGINT.131– terminated by SIGQUIT.
Also ensure exit builtin accepts: exit, exit <n>, error on too many args, numeric parsing as Bash.
- Redirect errors (e.g.,
cat < nofile) must not create processes needlessly; print error and set correct status. - Mix of redirs & pipelines: last command’s exit status propagates to
$?. cdwith redirs (e.g.,cd > file) must still run in parent when possible; handle errors before state changes.
- Single quotes
'...'→ literal. - Double quotes
"..."→ expand$VARand$?; keep spaces as literal characters. - Unquoted words are subject to splitting by the parser before expansion is resolved into argv; design your lexer/parser so that quotes affect tokenization, not post‑processing hacks.
readline and other libs often keep allocations until process exit, which show up as leaks unless you filter them. The goal here is to prove your code is leak‑free while ignoring 3rd‑party internals.
Typical command you can copy‑paste:
valgrind \
--suppressions=tools/suppress/suppress_lnx \
--leak-check=full \
--show-leak-kinds=all \
--track-origins=yes \
--trace-children=yes \
--track-fds=yes \
./minishellWhat each flag does:
--suppressions=FILE– ignore known leaks from libraries (e.g., readline). Put your rules intools/suppress/suppress_lnx.--leak-check=full– show a full backtrace for each leak.--show-leak-kinds=all– report definite / indirect / possible / reachable (see below).--track-origins=yes– trace uninitialized values to their origin (slower but super useful).--trace-children=yes– follow fork/exec children. Good for minishell because you spawn processes (pipes, heredoc helpers). If you only want to check the parent shell, set this tono.--track-fds=yes– report file descriptors not closed at exit.
Understanding leak kinds:
- definitely lost – real leaks. Fix these.
- indirectly lost – leaked via a leaked parent block. Usually fix.
- possibly lost – suspicious pointer arithmetic; investigate.
- still reachable – memory held until exit (often okay for libs like readline).
Rule of thumb: your grade depends on fixing definite/indirect leaks and FD leaks in your code paths (normal run, errors, heredoc abort). Suppress (or tolerate as reachable) what comes from libraries you don’t control.
- REPL skeleton with readline, history, and clean Ctrl‑C/D behavior at prompt.
- Lexer producing tokens (words, ops). Handle quotes during tokenization.
- Parser building an AST/commands list; validate grammar, produce friendly errors.
- Executor skeleton (no pipes/redirs yet): spawn external cmd, run builtins in parent.
- PATH resolution + clear error messages (126/127).
- Redirections (>, >>, <) with FD lifecycle and error propagation.
- Pipelines: connect FDs, collect last status.
- Expansion:
$VAR,$?, quoting rules. - Builtins complete & consistent (env table, export/unset behavior).
- Heredoc with delimiter rules + SIGINT abort.
- Signals in children: restore defaults; print correct messages.
- Cleanups: sweeping frees on all early‑return/error paths.
- Test matrix: scripts to cover signals, pipes, redirs, heredoc, big env, weird filenames.
Written by Michaela and https://github.com/grignetta.