This project has been created as part of the 42 curriculum by nribakov, sancuta.
minishell is a miniature POSIX-like shell. It provides a REPL interface
with readline, a character-by-character POSIX tokenizer, an LALR(1) parser
scaffold, and arena-based memory management.
- Description
- Instructions
- Known Limitations
- Algorithm and Data Structure
- Deviation from bash
- Resources
makeormake all- compilesminishell;make clean- removes object and dependency files;make fclean- additionally removes the binary, libs, docs, and logs;make re- recompiles the entire project from scratch;make debug- compiles with-g -DDEBUGfor debugging;make run- builds and runs./minishell;make run-debug- builds and runs./minishell-debug;make test- builds both release and debug binaries, then runs the test suite;make doc- generates Doxygen man pages indocs/(requiresdoxygen);make compile_flags.txt- generates a clangd-compatible compilation database;
Options:
make [target] ARENA_SIZE=N- overrides the arena initial capacity (default64);
Dependencies:
- libft (bundled)
./minishell- readline-based REPL with colored prompt (
user@hostname:cwd[exitcode]shni$) - POSIX-conformant tokenizer (10 rules, quotes, expansions, operators)
- shift-only LALR(1) parser (reductions planned)
- builtins:
env,pwd,exit(partial) - arena-based memory management (no per-allocation tracking)
- interactive and non-interactive modes
- debug mode with
--scope=<flags>and--no_exec
- No pipelines, redirections, subshells, or chaining:
|,<,>,<<,>>,&&,||,(,)are lexed but never acted upon. - No heredoc:
<<is tokenized but the heredoc body reader is not implemented. - No signal handling:
<signal.h>is included but no handlers are registered.SIGINTandSIGQUITare ignored by default.
The program state lives in a single t_ctx struct passed by pointer through
every stage. Memory is managed through typed arenas - there is no per-allocation
tracking or explicit free for individual tokens.
t_ctx
├── t_env env - linked list of environment key/value pairs
├── t_arena[5] arena - typed arenas
├── char *read_line - readline-allocated input (freed per iteration)
├── int return_status - exit code of the last command
├── bool is_interactive - true when STDIN is a tty
└── (debug) scope, no_exec - debug flags (only with -DDEBUG)
Arenas:
| Arena | Stride | Entry | Purpose |
|---|---|---|---|
AT_NONE |
- | - | Sentinel / untyped |
AT_PROMPT |
1 | char |
Prompt string buffer. Cleared per prompt. |
AT_STRING |
1 | char |
Token content, expanded strings. Cleared per parse. |
AT_STACK |
sizeof(t_symbol) | t_symbol |
Shift-reduce parse stack. Cleared per parse. |
AT_CMD |
sizeof(t_node) | t_node |
AST node pool. Cleared per parse. |
Pipeline:
read_line
│
▼
Lex ── character-by-character POSIX lexer ──► t_token
│
▼
Classify ── operator strings → grammar terminal
│
▼
Shift (AT_STACK) + Alloc (AT_CMD) ── WORD becomes t_node(NODE_ARG)
│
▼
Reduce ── pop AT_STACK frames, wire AT_CMD t_node pointers
│
▼
Execute ── walk AT_CMD root, fork/exec, apply redirects
│
▼
JIT Expand ── flagged tokens expanded inline during exec
Arena types use different strides: AT_PROMPT and AT_STRING are character
buffers (stride=1), AT_STACK has stride=sizeof(t_symbol), AT_CMD has
stride=sizeof(t_node).
typedef struct s_ctx
{
t_env env;
t_arena arena[AT_COUNT];
char *read_line;
int return_status; // exit status of last command
bool is_interactive; // true if STDIN is a tty
# ifdef DEBUG
uint8_t scope; // debug scope flags SCOPE_*
bool no_exec; // skip execution if set to true
# endif
} t_ctx;
typedef struct s_symbol
{
uint64_t offset; // offset into AT_STRING arena
uint64_t prev_symbol; // arena index of logical predecessor
uint64_t next_frame; // subshell chain (future use)
uint64_t next_pipeline; // pipeline chain (future use)
uint64_t next_redir; // command redirect chain (future use)
uint64_t next_arg; // command arg chain (future use)
uint64_t next_cmd; // pipeline level (future use)
uint32_t entry_state; // parser state before this symbol
uint32_t flags; // LEX_HAS_EXPANSION | LEX_HAS_QUOTES
t_symbol_type type; // SYM_TOKEN, SYM_OPERATOR, ...
} t_symbol;
typedef struct s_parser_state
{
uint32_t cur_state; // current parser state (always 0 - no reductions)
uint64_t arena_idx; // physical index of most recently shifted symbol
uint64_t stack_idx; // logical index (currently == arena_idx)
t_token lookahead; // the current lookahead token
uint8_t flags; // PARSE_HERE_PENDING | PARSE_DONE
} t_parser_state;
typedef struct s_lexer_state
{
t_slice token; // position+length of token being built
uint64_t char_idx; // index into read_line
t_symbol_type type; // SYM_TOKEN or SYM_OPERATOR
uint8_t flags; // LEX_HAS_EXPANSION | LEX_HAS_QUOTES | ...
} t_lexer_state;
typedef struct s_token
{
uint64_t offset; // offset into AT_STRING arena
uint32_t flags; // same flags as lexer_state
t_symbol_type type; // SYM_TOKEN, SYM_OPERATOR, ...
} t_token;
// t_node — single AST node struct, allocated in AT_CMD.
// child = first/left child, next = sibling/right operand,
// redirs = redirect chain (commands only), offset = string offset.
typedef struct s_node
{
t_node_type type;
struct s_node *child;
struct s_node *next;
struct s_node *redirs;
uint64_t offset;
uint32_t flags;
int heredoc_fd;
} t_node;Input is handled by a readline wrapper in srcs/input.c:
-
Interactive mode:
isatty(STDIN_FILENO)is checked once at startup. The prompt is built in theAT_PROMPTarena and includes color, user, hostname, cwd, and exit status:\033[38;5;40muser@hostname:cwd[0]shni$ \033[0m. Escape sequences are wrapped in\001/\002to prevent readline redisplay corruption. [TODO](go into more detail as to why) -
Continuation prompt: when
is_continuationis true, default"> "is used (could add a properPS2implementation instead). -
Non-interactive mode (3 approaches):
readline(NULL)withrl_outstream = stderr— current. readline still mirrors input to stderr but not stdout. History is skipped when!c->is_interactive.get_next_line()line-by-line — planned preferred. Clean loop, no readline overhead in non-interactive mode.read(2)intoAT_INPUTarena — planned alternative. Bulk-read stdin into a buffer, lexer scans forward preserving\n. Execution fires percomplete_commandreduction.
-
History: in interactive mode, non-empty lines are added to readline history via
add_history(). Not persisted between sessions.
Parsing follows a two-stage pipeline: tokenization (lexer) and syntax
analysis (parser). The lexer reads characters from read_line and produces
delimited tokens. The parser shifts those tokens onto a stack as t_symbol
nodes.
The lexer (in srcs/lexer/) implements the POSIX token recognition rules
from Section 2.3 of the Shell & Utilities volume. It is a character-by-character
state machine:
Lexer state tracks:
token: t_slice {pos, len} current extent in read_line
char_idx: uint64_t current read position
type: SYM_TOKEN / SYM_OPERATOR //TODO: refined into the correct
// operator symbol during classify
flags: LEX_HAS_EXPANSION token contains $ expansion
LEX_HAS_QUOTES token contains ' or " quotes
LEX_IS_BUILDING currently accumulating token
LEX_IS_DELIMITED token is complete, ready to shift
LEX_NEEDS_INPUT consumed all input, need more
The rules are applied in order (see POSIX 2.3 Token Recognition):
- EOI/EOF — delimit current token if one exists
- Operator continuation — if
prev_char+cur_charform a longer operator (e.g.<+<→<<), extend the token - Operator break — if
prev_charwas an operator butcur_charcannot extend it, delimit the operator token - Quotes —
'or"starts a quoted region; characters are added verbatim until the matching close quote - Expansion —
$starts a variable reference; following validname_charsare consumed - Operator start — unquoted
|,<,>,&,;,(,)start a new operator token; delimit any existing token first - Blank —
or\tdelimits an existing token and is discarded - Word continuation — any other character extends a word token
- Comment —
#(when not quoted) discards until end of line (not required, optionally compiled) - Default — start a new word token
On delimiting, the token body is copied into AT_STRING arena and a t_token
is produced as the lookahead.
The parser in srcs/parser/ is a shift-only LALR(1) parser. The main loop
in parse_input():
- Create a zeroed
t_parser_stateandt_lexer_state - Set
parse.arena_idx = 1(index 0 is the sentinel guard element) - Loop:
- Call
get_lookahead()— runs the lexer until a token is delimited - When a token is ready (
LEX_IS_DELIMITED), callshift_symbol() shift_symbol()allocates at_symbolonAT_STACK, sets its offset, type, flags, entry_state (always 0), andprev_symbolpointing to the previous symbol- Update
stack_idxandarena_idx - Repeat until EOF
- Call
No reductions are performed. The grammar rules (t_rule[RULE_COUNT]) and
reduce function pointers (t_reduce) are defined in types.h but never
populated. The parser creates a flat linked list of symbols — no non-terminal
nodes are synthesized.
Planned: The parser will perform LALR(1) shifts and reductions via
reduce_by_rule(), allocating t_node entries into AT_CMD on each shift
(for WORD tokens) and wiring them together during reduce. AT_STACK entries
become lightweight t_frame with t_node *node_ref instead of bare tokens.
Shell Grammar Lexical Conventions
Lexing happens immediately following the token being delimited.
1. if
`cur_token` is `operator`
do
identify as corresponding `token_id`
2. if // we skip this
`cur_token` is only `digits`
&& `delimiter` is `<` or `>`
do
identify as `IO_NUMBER`
3. if // we skip this
`cur_token` is `{LOCATION}`
&& `delimiter` is `<` or `>`
do
identify as `IO_LOCATION`
4. do
identify as `TOKEN`
Shell Grammar Rules
1. (rule 2 - redirection to or from `filename`)
1.1. tilde expansion
1.2. parameter/variable expansion
1.3. quote removal // actually 5th expansion, but we don't handle
// command substitution or arithmetic expansion
1.4. filename expansion
1.5. word splitting // if more than one word, bash errors with:
// `bash: $var: ambiguous redirect`
2. (rule 3 - redirection from `here-doc`)
Quote removal of `WORD` after `DLESS` to determine `here-doc` `delimiter`
3. (rule 7 - assignment preceding command name)
//TODO: continue writing
documentation
Extended Backus-Naur Form (EBNF) Notation
%token WORD NEWLINE PIPE LESS GREAT DLESS DGREAT AND_IF OR_IF OPAR CPAR
%start complete_commands
%%
complete_commands : complete_commands newline_list complete_command
| complete_command
;
complete_command : list
;
separator : newline_list
;
newline_list : NEWLINE
| newline_list NEWLINE
;
linebreak : newline_list
| /* empty */
;
list : and_or
;
and_or : pipeline
| and_or AND_IF linebreak pipeline
| and_or OR_IF linebreak pipeline
;
pipeline : pipe_sequence
;
pipe_sequence : command
| pipe_sequence PIPE linebreak command
;
command : simple_command
| compound_command
| compound_command redirect_list
;
compound_command : subshell
;
subshell : OPAR compound_list CPAR
;
compound_list : linebreak term
| linebreak term separator
;
term : term separator and_or
| and_or
;
simple_command : cmd_prefix cmd_word cmd_suffix
| cmd_prefix cmd_word
| cmd_prefix
| cmd_name cmd_suffix
| cmd_name
;
cmd_name : WORD
;
cmd_word : WORD
;
cmd_prefix : io_redirect
| cmd_prefix io_redirect
;
cmd_suffix : io_redirect
| cmd_suffix io_redirect
| WORD
| cmd_suffix WORD
;
redirect_list : io_redirect
| redirect_list io_redirect
;
io_redirect : io_file
| io_here
;
io_file : LESS filename
| GREAT filename
| DGREAT filename
;
filename : WORD
;
io_here : DLESS here_end
;
here_end : WORD
;
%%bash grammar the following chars don't have an explicit token they get assigned to in the grammar.
'&' '|' '(' ')' '<' '>'
being single char tokens, they can just represent themselves?
metacharacters - see 2. Definitions - metacharacter characters that separate words when unquoted.
'|', '&', ';', '(', ')', '<', '>'
' ', '\t', '\n'
operators - see 2. Definitions - operator
operators contain at least one unquoted metacharacter
blanks - see 2. Definitions
' ', '\t'
control operators - see 2. Definitions - control operator
'||', '&&', '(', ')'
'&', ';', ';;', ';&', ';;&', '|', '|&' - not implemented
redirection operators - see 3.6 Redirections
'<', '>', '>>'
'<<' // here-doc
name - see 2. Definitions - name
words that conform to the following format: [a-zA-Z_][a-zA-Z0-9_]
TODO
currently limited to a flat walk of the symbol stack in token_processor.c.
Will walk AT_CMD AST via t_node pointers.
TODO
currently no signal handling. <signal.h> is included in minishell.h.
TODO cleanup is incomplete.
ASSIGNMENT_WORDpotentially necessary — can be handled inexportas a builtin with string parsing instead of a dedicated token typeIO_NUMBERfd-specific redirects like2>are not mentioned by the subject and not required per campus consensus;,!,\,~not required, but interesting to consider~used in the dynamic prompt#comments are implemented
AI tools were used mainly for interactive rubber ducking and searching for information.