A Unix shell built from scratch in C as part of the 42 Berlin core curriculum. It replicates core bash behavior: tokenization, parsing, variable expansion, redirections, pipes, heredocs, and all mandatory built-in commands.
Built by Miguel Andrade and Daniela Torretta.
- Interactive prompt with command history (via readline)
- Single and multi-command pipelines (
cmd1 | cmd2 | cmd3) - Input/output redirections (
<,>,>>) - Heredoc (
<<) - Environment variable expansion (
$VAR,$?) - Single and double quote handling
- Signal handling:
Ctrl+C,Ctrl+D,Ctrl+\ - 7 built-in commands
- PATH-based command resolution
| Command | Description |
|---|---|
echo [-n] |
Print arguments to stdout. -n suppresses the trailing newline |
cd [path] |
Change directory. Supports .., - (OLDPWD), and no-arg (HOME) |
pwd |
Print the current working directory |
export [NAME=value] |
Set or update an environment variable |
unset [NAME] |
Remove an environment variable |
env |
Print all current environment variables |
exit [code] |
Exit the shell with an optional status code |
Dependencies: readline must be installed.
# macOS (Homebrew)
brew install readline
# Ubuntu/Debian
sudo apt-get install libreadline-dev# Clone and build
git clone https://github.com/miguandr/minishell.git
cd minishell
make
# Run
./minishell# Clean object files
make clean
# Full clean (objects + binary)
make fclean
# Rebuild from scratch
make reThe Makefile automatically detects macOS vs Linux and adjusts the readline include/library paths.
The shell processes each command through a three-stage pipeline:
readline input
│
▼
┌─────────┐
│ Lexer │ Tokenizes raw input into a doubly-linked list of tokens
└────┬────┘ (WORD, PIPE, GREAT, GREAT_GREAT, LESS, HERE_DOC)
│
▼
┌──────────┐
│ Parser │ Builds a linked list of t_parser nodes (one per command).
└────┬─────┘ Separates arguments from redirections. Calls the expander.
│
▼
┌──────────────┐
│ Expander │ Resolves $VAR, $?, quote stripping before execution
└──────┬───────┘
│
▼
┌──────────────┐
│ Executor │ Dispatches to builtins or execve. Handles pipes via
└──────────────┘ fork/dup2. Manages heredoc temp files.
t_mshell — global shell state: environment, PATH array, PWD/OLDPWD, pipe count, PID array, exit code.
t_lexer — doubly-linked list node: raw string + token type + quote flags (is_single, is_double).
t_parser — doubly-linked list node per command: argument array, builtin function pointer, redirections list, heredoc info.
- Builtins are stored as function pointers in
t_parser.builtins. At parse time,builtins_handler()does a name lookup and stores the pointer directly — no string comparison at execution time. cd,exit,export,unsetrun in the parent process; all others fork. These four mutate shell state (env, PWD, exit code) so they can't run in a child.- Heredocs write to a temp file with a generated name; the executor reads from that fd.
- The expander runs during parsing, not execution. Each token in the argument array is expanded in place before being handed off to the executor.
- Error handling is centralized via
handle_error()andhandle_error2(), which print to stderr, set the exit code, and callreset_data()to bring the shell back to a clean state without exiting.
minishell/
├── includes/
│ └── minishell.h # All structs, enums, and function prototypes
├── sources/
│ ├── main.c # Entry point + main loop (lexer → parser → executor)
│ ├── lexer/ # Tokenizer: splits input into t_lexer nodes
│ ├── parser/ # Builds t_parser nodes, separates args/redirections
│ ├── expander/ # Variable and quote expansion
│ ├── executor/ # Forks, pipes, execve, heredoc
│ ├── builtins/ # echo, cd, pwd, export, unset, env, exit
│ ├── utils/ # Init, reset, signals, environment, memory
│ └── error/ # Centralized error handling
└── libft/ # Custom C standard library (ft_printf, get_next_line, libft)
I was responsible for the input processing pipeline — everything from raw user input to the structured representation the executor consumes.
Full implementation of the tokenizer:
- Scans raw input character by character, producing a doubly-linked list of
t_lexernodes - Handles all 6 token types:
WORD,PIPE,GREAT,GREAT_GREAT,LESS,HERE_DOC - Quote-aware word boundary detection: single and double quotes are respected when determining where a token ends
lexer_error_check.c: validates token sequences before parsing (catches things like| |, trailing pipes, commands starting with>>)- Quote balance check (
count_quotes) before tokenization even starts
Full implementation of the variable expansion engine:
expander.c: entry point — iterates over the argument array and dispatches to the right handlerexpander_variables.c:$VARlookup againstenvp,$?expansion (exit code), variable name extractionexpander_utils.c: double-quote expansion (variables expand inside"..."), single-quote passthrough (variables do NOT expand inside'...'), edge cases like$VARat end of string or followed by non-alphanumeric chars- Also handled
expander_heredoc— variable expansion inside heredoc content
Centralized error system used throughout the entire codebase:
handle_error(): maps 9 error codes to specific messages (syntax errors, memory failures, system call failures), sets exit code, callsreset_data()to recoverhandle_error2(): handlesexport-specific errors (invalid identifiers, event not found, non-numeric exit args)
signals.c: signal handlers forSIGINT/SIGQUITwith different behavior in parent vs child processesinit.c: shell initialization and reset loop (reset_databrings the shell back to a clean state after each command without restarting the process)utils/enviroment.c: PATH parsing and splitting, PWD/OLDPWD tracking fromenvpmain.c: entry point, argument validation, initial environment setup
My teammate Daniela Torretta built the executor (fork/pipe/dup2/execve), all built-in commands (echo, cd, pwd, export, unset, env, exit), the parser, and heredoc execution.