Minishell β’ C β’ Unix β’ Signals β’ Readline
A compact, Bash-like shell in C for the 42 core curriculum. It supports builtins, pipelines, redirections (incl. heredoc), quote-aware expansion, and correct signal/exit-status semantics β implemented strictly within the subject (not a full Bash).
- β Mandatory: complete, defense-ready
- β¨ Bonus (started): minimal
*wildcard expansion (ungraded in our defense) - π« Not implemented by design:
&&,||, subshells( β¦ ), command substitution, assignment words likeX=1 cmd
This repository contains the final, evaluated version. The code follows 42 Norm (β€ 25 lines per function, β€ 5 functions per file, β€ 4 params per function, formatting rules).
- Introduction
- Features & Scope
- Build & Dependencies
- Quick Start
- Architecture Overview
- Lexing, Parsing & Expansion
- Redirections & Heredoc
- Signals & Exit Status
- Environment Management
- Wildcards (Bonus β started)
- Testing & Example Sessions
- Defense Notes
- Known Limitations
- Changelog (Recent Fixes)
- Project Structure
- Build Options & Make Targets
- Evaluator Quick Checklist
- Authors
Minishell re-implements a constrained, Bash-compatible subset focusing on:
- Correct builtins in the parent process (when not piped)
- Robust redirections (
<,>,>>,<<) with right-most precedence - Pipelines
| - Expansion of
$VARand$?honoring quote rules & token adjacency - Signals for prompt / children / heredoc
- Accurate exit statuses (
126,127,130,131)
Design goal: clean, Norm-compliant, defense-ready code that behaves like Bash in the parts covered by the subject.
- Builtins (run in parent if not piped):
echo(with-n)cdpwdexportunsetenvexit
- External commands via
fork+execve - Pipelines: arbitrary length (
cmd1 | cmd2 | ...) - Redirections:
- Input
< - Output
> - Append
>> - Heredoc
<<(quoted/unquoted delimiter semantics)
- Input
- Expansion:
$VAR,$?- Single quotes block expansion
- Double quotes allow expansion
- Adjacent tokens preserved (e.g.,
"a"'b'cβabc)
- Signals
- Prompt:
Ctrl-Cclears line + sets$?=130;Ctrl-\ignored - Children:
Ctrl-Cβ 130,Ctrl-\β 131 - Heredoc:
Ctrl-Caborts immediately (status 130), skips command
- Prompt:
- Environment management
- Deep-copied
char **env exportsorted printing, strict identifier validationunsetsafe removal
- Deep-copied
- Bonus (started):
- Minimal
*wildcard expansion (no hidden files unless pattern starts with.)
- Minimal
Out of scope per subject:
- Logical operators
&&,|| - Subshells
( β¦ ) - Command substitution
`cmd`/$(cmd) - Assignment words (
X=1 cmd)
- readline library
- 42 libft (included as submodule or folder)
On Linux (Debian/Ubuntu):
sudo apt-get update
sudo apt-get install -y libreadline-dev
makeOn macOS (Homebrew):
brew install readline
# The Makefile already links with -lreadline and includes -I as needed
makemake # build minishell
make clean # remove object files
make fclean # remove objects + binary
make re # full rebuild./minishell
minishell $> echo hello | grep h
minishell $> export A=42; echo "$A"
minishell $> cat << EOF | grep hi
> hi
> EOF
minishell $> cd -typedef struct s_redirection {
int type; // enum: <, >, >>, <<
char *filename; // for << store delimiter
struct s_redirection *next;
} t_redirection;
typedef struct s_command {
char **argv; // null-terminated
t_redirection *redirection; // linked list
struct s_command *next; // pipeline chain
} t_command;
typedef struct s_bash {
char **env; // deep-copied environment
int exit_status;
t_token *tokens; // last tokens (for cleanup)
t_command *commands; // last parsed pipeline (for cleanup)
char **export_noval; // names declared via 'export NAME' (no value)
} t_bash;readline β tokenize β expand β parse β execute
-
Tokenizer: words, quotes,
$, redirects, pipe -
Expander:
$VAR,$?under quote rules; concatenates adjacent pieces -
Parser: builds
t_commandlinked list; attaches redirections; permits redirections at command start (e.g.,> out cmd) -
Executor:
- No pipe: builtin runs in parent, else
fork+execve - Pipeline: each stage forks; redirs apply in child; parent reaps and sets status from rightmost child
- No pipe: builtin runs in parent, else
-
Right-most redirection wins (
echo hi > a > bwrites tob) -
Builtins in parent (when not piped) so that
PWD,env,$?update in-shell -
In pipelines, builtins run in the child of their stage (Bash semantics)
-
cd -:- uses
$OLDPWDand prints the new cwd - updates
PWD/OLDPWD
- uses
-
Single quotes
'...': literal text, no expansion -
Double quotes
"...": expand$VAR,$? -
Bare
$inside double quotes remains$if no valid name follows -
Adjacency preserved:
"a"'b'cβabc- filenames for redirections can be built from multiple adjacent pieces without spaces; we concatenate during parse
-
$?expands to last exit status
-
<β open read-only, dup to stdin -
>β create/truncate, dup to stdout -
>>β create/append, dup to stdout -
<<(heredoc)- Quoted delimiter β no expansion in heredoc body
- Unquoted delimiter β expand
$VARand$? Ctrl-Cduring heredoc cancels immediately (status 130), command is skipped
-
Redirections are applied inside children (including pipeline stages), so each stage has its own view
-
For a chain of
>/>>, the last one takes effect (right-most precedence)
-
Prompt
SIGINT(Ctrl-C): clear line + newline, set$?=130SIGQUIT(Ctrl-\): ignored
-
Children
SIGINTβ exit code 130SIGQUITβ exit code 131
-
Exit codes of interest
127β command not found /PATHunset126β found but not executable or is a directory130β terminated bySIGINT131β terminated bySIGQUIT
-
For pipelines, the shellβs
$?is the rightmost stageβs status (with the signal mapping above)
-
We deep-copy the host environment (
char **env) at startup -
exportrules (Bash-compatible within scope)-
export(no args): print a sorted merged view of actualenvand names stored inexport_noval- Format:
declare -x KEY="VAL"(if value) ordeclare -x KEY(if no value)
- Format:
-
export NAME(no=):- Validate identifier (see below)
- If
NAMEis not already inenv, store it inexport_noval(name only)
-
export NAME=VAL:- Add/replace in
env - Remove
NAMEfromexport_novalif present
- Add/replace in
-
Invalid identifiers print to
stderrand set status 1
-
-
Identifier validation:
[A-Za-z_][A-Za-z0-9_]*- Rejects:
A-,HELLO-=123,=,123, etc.
-
unset NAMEremoves fromenvand also fromexport_novalif present
- Minimal
*expansion implemented (not graded in defense) - Uses
opendir/readdir - Expands only unquoted tokens containing
* - Hidden files are ignored unless the pattern starts with
.
We validated behavior with the public LucasKuhn/minishell_tester plus extra manual tests.
# cd -
pwd; cd ..; cd -; echo "$?" # prints new cwd; exit 0
# export (name only vs key=val)
unset A; export A; export | grep '^declare -x A$'
export A=1; export | grep '^declare -x A="1"$'
unset A
# invalid export (should set status 1 and print error)
export A-; echo $?
export HELLO-=123; echo $?
export =; echo $?
export 123; echo $?
# pipe + env mutation (env change must not leak back to parent through a pipe)
export GHOST=123 | env | grep GHOST # parent remains unchanged
# mixed quoting / adjacency
echo "a"'b'c # -> abc
echo "$" # -> $
# heredoc cancel (Ctrl-C)
cat << EOF
^C # heredoc canceled, $?=130
# signal exit statuses from children
sleep 5 & kill -QUIT %1 # observe status 131echo a | cat -e
cat << EOF | grep hi | wc -l
hi
EOFrm -f a b
echo hi > a > b; wc -c a b
# 0 a
# 3 b
# 3 total
grep hi < ./test_files/infile
echo hi > out.txt
echo again >> out.txt-
We implemented only what the subject requires, not full Bash.
-
Builtins in parent (when not piped) so
PWD,env,$?mutate the shell state. -
Pipelines spawn children per stage; redirs are applied in children; parent reaps and sets
$?from the rightmost stage. -
cd -uses$OLDPWD, prints the new cwd, and updatesPWD/OLDPWD. -
export:- Print format:
declare -x KEY="VAL"ordeclare -x KEY(no quotes if no value) - Accept
export NAMEas a declaration (recorded inexport_novalunless already inenv) - Invalid identifiers β
stderr, exit 1
- Print format:
-
126 vs 127: distinguished correctly (not executable vs not found).
-
Heredoc:
- Quoted delimiter β no expansion
- Unquoted β expands
$VAR/$? Ctrl-Ccancels heredoc and sets$?=130(command skipped)
- No logical operators (
&&,||), no subshells( β¦ ) - No command substitution or assignment words
- Minimal wildcard only (bonus, not graded)
- Message strings like βBroken pipeβ may differ from Bash; not required by subject. Exit codes match.
-
cd -parity with Bash- Fetch
$OLDPWD, change directory, print new cwd - Update
PWD/OLDPWD - If
OLDPWDmissing βminishell: cd: OLDPWD not set(exit 1)
- Fetch
-
exportcompliance- Added name-only declarations with
bash->export_noval export(no args) prints sorted merged view of env + noval- Correct quoting:
declare -x KEY="VAL"ordeclare -x KEY - Strict identifier validation (reject
A-,HELLO-=123,=,123, β¦) - On
NAME=VAL, update env and remove from noval - On
unset NAME, also remove from noval
- Added name-only declarations with
-
Pipelines
- Builtins inside pipeline stages run in the child of that stage
- Redirections applied per child
- Parent computes final
$?from the rightmost stage
-
Norm refactors
- Split long functions (e.g., pipeline execution into
child_process,pipe_child,wait_for_pipeline) - Respected β€ 25 lines/function, β€ 5 functions/file, β€ 4 params/function, line widths, spacing, includes
- Split long functions (e.g., pipeline execution into
-
Printing & errors
- Bash-like formatting for
exportlines - Consistent error messages for
cd, invalid identifiers, and heredoc cancel
- Bash-like formatting for
.
βββ include/
β βββ minishell.h
β βββ builtin.h
β βββ struct.h
βββ libft/ # 42 libft
βββ minishell.c # entry (delegates to core)
βββ src/
β βββ core/
β β βββ init_minishell.c
β β βββ execute_minishell.c
β βββ lexer/
β β βββ tokenize.c
β β βββ tokenizer_utils.c
β β βββ tokenizer_redirects.c
β βββ parser/
β β βββ parse_tokens.c
β β βββ parse_utils.c
β β βββ parser_cmd.c
β β βββ parser_cmd_utils.c
β β βββ parser_checks.c
β β βββ handle_parse_redirection.c
β β βββ last_token_is_pipe.c
β βββ expand/
β β βββ expand.c
β β βββ expand_utils.c
β β βββ expand_wildcard.c
β β βββ wildcard_utils.c
β β βββ wildcard_utils_2.c
β βββ execution/
β β βββ execve.c
β β βββ get_cmd_path.c
β β βββ redirection.c
β β βββ heredoc.c
β β βββ heredoc_utils.c
β β βββ run_external_cmd.c
β β βββ run_external_cmd_utils.c
β β βββ pipes/
β β βββ create_pipe.c
β β βββ execute_pipeline.c
β βββ builtin/
β β βββ ft_echo.c
β β βββ ft_cd.c
β β βββ ft_pwd.c
β β βββ ft_env.c
β β βββ ft_export.c
β β βββ ft_export_utils.c
β β βββ ft_unset.c
β β βββ ft_exit.c
β βββ env/ # ft_getenv, env_set, etc.
β βββ signal/ # prompt + child signal handling
β βββ utils/ # frees, strings, small helpers
βββ Makefile
βββ README.md
Headers use
#include "minishell.h"with-Iincludein the Makefile.
makeβ buildminishellmake cleanβ remove objectsmake fcleanβ remove objects and binarymake reβ full rebuild
Compile flags (typical 42):
-Wall -Wextra -Werror
The Makefile links -lreadline and includes the correct headers (adjust include paths for your platform if needed).
-
Prompt shows and accepts input until EOF (
Ctrl-D) -
Signals
- Prompt:
Ctrl-Cclears line + sets$?=130 Ctrl-\ignored at prompt- Children:
SIGINTβ130,SIGQUITβ131
- Prompt:
-
Builtins
echo,cd,pwd,export,unset,env,exit- Builtins run in parent unless piped
cd -prints new cwd and updatesPWD/OLDPWDexportwithout args prints sorteddeclare -x ...- Invalid
exportidentifiers β error + status 1 unsetremoves keys (and fromexport_noval)
-
Expansion
$VAR,$?with quote rules- Adjacent tokens concatenate properly
-
Redirections
<,>,>>,<<working- Heredoc quoted delimiter β no expansion
- Heredoc
Ctrl-Ccancels (status 130)
-
Pipelines
- N-stage pipelines, rightmost exit status applies
- Redirs applied in the children
-
Path resolution
127not found;126not executable / is directory
-
Norm
- β€ 25 lines/function, β€ 5 functions/file, β€ 4 parameters/function
- Indentation, headers, includes, forbidden functions: respected
- Christian (chrrodri) β 42 intra:
chrrodriβ[email protected]β GitHub: @kitearuba - bsamy β 42 intra:
bsamy
Educational project for 42 School (Barcelona). Feedback and suggestions are welcome!