Minishell is a project from 42 School that challenges us to recreate a minimal yet functional Unix shell almost from scratch. Built entirely in C, this project was developed by the two of us, pushing our skills in system programming, process management, and string parsing to the limit—while also testing our patience more times than we'd like to admit!
- Custom Prompt → A stylish arrow that changes color based on the last return value.
- Command Execution → Run executables from the system’s
$PATH, including absolute and relative paths. - Pipes (
|) → Chain multiple commands together, just like in a real shell. - Redirections (
>,>>,<,<<) → Handle file input/output redirections seamlessly. - Environment Variables (
$VAR) → Expand variables, including$?for the last command’s exit status. - Built-in Commands → Supports
echo,cd,pwd,export,unset,env, andexit. - Signal Handling → Properly responds to
Ctrl-C,Ctrl-D, andCtrl-\. - Logical Operators (
&&and||) → Execute commands conditionally based on exit statuses. - Wildcard Expansion (
*) → Expands wildcards (only within the current directory).
Minishell is not just a simple command parser. It follows a structured pipeline to interpret and execute commands:
- Lexing & Tokenization → The input string is split into tokens and stored in a linked list.
- Syntax Validation → Ensures correct use of quotes, parentheses, pipes, and redirections.
- Here-Document Processing → If
<<is detected, user input is pre-fetched and stored in a temporary file. - Abstract Syntax Tree (AST) Construction → Commands are structured hierarchically based on execution order.
- Execution → The AST is recursively executed, handling pipes, redirections, and operators dynamically.
Given the following command:
➜ [lilefebv] 42-Minishell ❯ (echo $PWD && (ls -l | grep *.c) || echo "No .c files found") > output.txt && cat output.txt | wc -lFirst, it is tokenized, resulting in:
Token Text Previous Self Next
PAREN_OPEN (null) (nil) 0x1a78b40 0x1a78b70
COMMAND echo $PWD 0x1a78b40 0x1a78b70 0x1a78ba0
AND (null) 0x1a78b70 0x1a78ba0 0x1a78bf0
PAREN_OPEN (null) 0x1a78ba0 0x1a78bf0 0x1a78c20
COMMAND ls -l 0x1a78bf0 0x1a78c20 0x1a78c70
PIPE (null) 0x1a78c20 0x1a78c70 0x1a78ca0
COMMAND grep "*.c" 0x1a78c70 0x1a78ca0 0x1a78a30
PAREN_CLOSE (null) 0x1a78ca0 0x1a78a30 0x1a78cf0
OR (null) 0x1a78a30 0x1a78cf0 0x1a5b190
COMMAND echo "No .c files found" 0x1a78cf0 0x1a5b190 0x1a78950
PAREN_CLOSE (null) 0x1a5b190 0x1a78950 0x1a78980
REDIRECT_OUT (null) 0x1a78950 0x1a78980 0x1a789b0
FILE_R output.txt 0x1a78980 0x1a789b0 0x1a78a00
AND (null) 0x1a789b0 0x1a78a00 0x1a78b10
COMMAND cat output.txt 0x1a78a00 0x1a78b10 0x1a78a80
PIPE (null) 0x1a78b10 0x1a78a80 0x1a78ab0
COMMAND wc -l 0x1a78a80 0x1a78ab0 (nil)
Since the syntax is valid (no consecutive operators, misplaced parentheses, etc.), we proceed to the next step.
The parsed command is converted into an AST:
Execution happens recursively, following these principles:
- Command nodes are executed directly.
- Redirection nodes modify input/output for all child nodes.
- Pipe nodes create pipes between left and right child nodes, allowing multiple commands to interact.
- AND/OR nodes execute the left child first, then conditionally execute the right child based on the return value.
Here’s a visual representation of the execution flow (warning: brain overload ahead 😵):
This wasn't our favorite project, but it covered a wide range of fundamental topics and gave us a deeper understanding of how a shell really works.
Our Minishell is quite robust—it survived extensive testing and abuse from fellow students, pushing it to its limits. If you find a bug or crash, feel free to open an issue (though we can't promise we'll have the motivation to fix it 😆).
Hope this README helped you understand how our Minishell works! 🚀
Debugging a shell can be a nightmare, so here are some useful tools to help you track down memory issues and unexpected behaviors.
To check for memory leaks, file descriptor issues, and other tricky bugs, you can use Valgrind with the following command:
valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --show-mismatched-frees=yes --track-fds=yes --trace-children=yes --suppressions=supp.supp ./minishellThis will:
- Detect all types of memory leaks (definitely lost, indirectly lost, etc.).
- Track invalid memory accesses and mismatched frees.
- Show unclosed file descriptors, which can be crucial for debugging a shell.
- Trace child processes, ensuring that forks don’t leave memory leaks undetected.
Since readline has known memory leaks that we can’t fix, we use a suppression file (supp.supp) to ignore them:
{
ignore_libreadline_leaks
Memcheck:Leak
...
obj:*/libreadline.so.*
}
This prevents Valgrind from reporting leaks inside libreadline, making debugging more focused on actual leaks in Minishell.
In the repository, you'll also find a file named testsdesenfer. This contains a collection of extreme edge cases and tricky commands that helped us push Minishell to its limits.
If you want to see how well your shell holds up, run those tests—you might be surprised by what breaks! 🚀
This test battery is really usefull to see how the minishell react on cases where it should work, but if you want to see any potential leak on bad user input, you can try this command
cat /dev/urandom | valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --show-mismatched-frees=yes --track-fds=yes --trace-children=yes --suppressions=supp.supp ./minishell 2> outerrorThis command will fill your minishell with a lot of random characters and it will often leak. It can also create weird files it's normal. If you have a not a tty check disable it when you use this command.
git clone https://github.com/Liammmmmmmm/42-Minishell.git
git submodule --init --recursive

