A POSIX-compliant Unix shell written from scratch in C — pipes, I/O redirection, job control, and signal handling, all built on raw fork/exec/wait primitives.
| Feature | Details |
|---|---|
| Pipelines | cmd1 | cmd2 | cmd3 — stdout of each stage wired to stdin of the next |
| I/O Redirection | > overwrite, >> append, < input from file |
| Background Jobs | cmd & — tracked by job ID, listed with jobs |
| Job Control | fg %N, bg %N — move jobs between foreground and background |
| Signal Handling | SIGINT (Ctrl-C), SIGTSTP (Ctrl-Z), SIGCHLD (child reaping) |
| Built-ins | cd, exit, jobs, fg, bg |
| Process Groups | Each pipeline gets its own pgid for correct signal delivery |
git clone https://github.com/Harsh7115/unix-shell
cd unix-shell
make
./shell# Multi-stage pipeline
ls -la | grep ".c" | wc -l
# I/O redirection
sort < input.txt > sorted.txt
cat log.txt >> archive.txt
# Background job + control
sleep 60 & # [1] 4821
jobs # [1]+ Running sleep 60
fg %1 # bring to foreground
# Ctrl-Z # suspend it
bg %1 # resume in background
# Nested pipes
cat file.txt | tr 'a-z' 'A-Z' | revInput string
│
▼
Tokenizer quoted strings, whitespace, metachar splitting
│
▼
Parser builds pipeline: [ cmd1 | cmd2 | cmd3 ]
│
▼
Executor
├── for each stage: fork() → child calls execvp()
├── pipe(2) connects adjacent stages
├── dup2(2) wires stdin/stdout to pipe ends
├── parent calls setpgid() to assign pipeline to a process group
└── tcsetpgrp() hands terminal control to foreground pgid
│
▼
Reaper waitpid(WNOHANG) in SIGCHLD handler, updates job table
| Signal | Source | Shell behavior |
|---|---|---|
SIGINT |
Ctrl-C | Forwarded to foreground process group; shell ignores it |
SIGTSTP |
Ctrl-Z | Stops foreground process group; shell marks job STOPPED |
SIGCHLD |
Child exit/stop | waitpid loop reaps zombies, updates job state |
SIGTTOU |
Background write to tty | Ignored by shell to prevent self-stops |
Every pipeline runs in its own process group. tcsetpgrp() grants the terminal to the foreground pgid, so SIGINT/SIGTSTP only reach the running job — not the shell itself. When the job finishes or is suspended, tcsetpgrp() returns control to the shell's pgid.
unix-shell/
├── shell.c # REPL loop, prompt, readline
├── tokenizer.c/h # Lexer: splits input into tokens, handles quoting
├── parser.c/h # Builds pipeline structs from token stream
├── executor.c/h # fork/exec, pipe wiring, pgid management
├── jobs.c/h # Job table: add, update, print, reap
├── builtins.c/h # cd, exit, jobs, fg, bg
├── signals.c/h # Signal handler setup and dispatch
└── Makefile
waitpid(-1, WNOHANG, WUNTRACED)in theSIGCHLDhandler reaps all finished children without blockingdup2is called in the child afterforkbut beforeexecvpto redirect file descriptors before the process image is replaced- The tokenizer handles single/double quotes and backslash escapes before the parser sees any input
- Built-ins are checked before
execvpbecause they need to run inside the shell process (e.g.cdmust change the shell's own cwd)
C · POSIX APIs (fork, execvp, pipe, dup2, waitpid, tcsetpgrp, sigaction) · GNU Make