All projects

completedC / POSIX / Linux

Minishell

A POSIX-compliant Unix shell written in C from scratch, with a full lexer-parser-executor pipeline, pipes, redirections, and built-in commands.

  • C
  • POSIX
  • Linux
  • Unix
  • GNU Readline
  • Git
  • gdb
  • Valgrind
~/minishell
minishell$ echo hello
hello
minishell$ export TEST=42
minishell$ env | grep ^TEST=
TEST=42
minishell$ echo $TEST
42
minishell$ cat << EOF
> hello world
> $USER
> EOF
hello world
farmoham
minishell$ echo "hello world $USER" > file.txt
minishell$ cat file.txt
hello world farmoham
minishell$ echo "
minishell: syntax error: unclosed quote
minishell$ ^C
minishell$ ^C
minishell$ ^C
minishell$ ^C
minishell$ ff
minishell: ff: command not found
minishell$ 
exit
A session from the project's own screenshot, typed out as text.

Overview

Minishell is a Unix shell implemented from scratch in C as part of the 42 Prague curriculum, built with a partner under strict coding standards. It reproduces core Bash behavior end to end: an interactive prompt with command history via GNU Readline, a lexer and parser that turn raw input into an executable command structure, environment variable expansion, and a process-based executor that runs built-ins and external binaries.

Architecture

The codebase is split into a lexer/parser/expander pipeline (lexer.c, lexer_token.c, lexer_extract.c, parser.c, parser_utils.c, expander.c, expander_utils.c) that turns raw input into a structured command list, and an execution layer (executor.c, executor_utils.c, path_finder.c, redirections.c, heredoc.c, wait.c) that forks processes, wires up pipes and file descriptors, and waits on children. Built-in commands run in-process rather than being forked, and a dedicated environment layer (env.c, env_utils.c) manages the shell's variable list independently of the OS environment array. A custom libft utility library backs the string and memory helpers used throughout.

Key Features

  • Interactive prompt with command history via GNU Readline
  • Lexer and parser that tokenize and structure raw input, including quote handling
  • Pipelines (|) and multi-command chaining through process creation
  • Redirections (<, >, >>) and heredocs (<<) with delimiter support
  • Built-in commands: echo, cd, pwd, env, export, unset, exit
  • Environment variable ($VAR) expansion with an independently managed environment list
  • Signal handling that adapts to interactive mode, heredoc input, and child execution

Technical Challenges

  • Reproducing Bash-like parsing and expansion behavior without access to Bash's own source or internal state
  • Managing file descriptors and process lifetimes correctly across pipelines, redirections, and heredocs so nothing leaked or closed early
  • Handling signals differently depending on shell state: interactive prompt, heredoc input, or a running child process

Engineering Decisions

  • Separated the shell's environment state (env.c) from the OS environment array so built-ins like export and unset can mutate it without exec() side effects
  • Kept built-ins running in the parent process instead of forking, since commands like cd and export must affect the shell's own state, not a child's
  • Split lexing, parsing, and expansion into distinct passes instead of one large parser function, making quote handling and $VAR expansion easier to reason about and test in isolation

Technologies Used

Written in C targeting Linux/Unix, using POSIX system calls (fork, execve, waitpid, dup2, pipe) for process and file-descriptor control, GNU Readline for interactive input and history, and a custom libft utility library for string and memory helpers.

Lessons Learned

  • Strengthened low-level debugging skills using gdb and Valgrind to track down descriptor leaks and memory issues
  • Learned to validate shell behavior empirically against real Bash rather than assumptions
  • Practiced Git-based collaboration on a shared, peer-reviewed C codebase under strict coding-standard constraints