Skip to content
Imtiaz Hossain

work / systems / custom-shell

Custom UNIX Shell

A UNIX shell in C with pipes, redirection, chaining, history, and signal handling, built from fork(), execvp(), and careful file-descriptor bookkeeping.

period

2025

status

coursework

operators
| > >> < ; &&
full parsing + execution
builtins
cd, history
plus exit
signals
SIGINT
ctrl+c handled
language
C
no libraries, raw POSIX

system architecture / interactive

Read Lineprompt loopParsertokenize opsBuiltinscd / historyfork + execvpchild procsdup2 Plumbingpipes + files
fig. 00 / custom-shell / hover nodes to trace the data flow

Why build a shell

A shell is the best possible tour of UNIX process machinery: to make cat file | grep x | wc -l work you have to genuinely understand fork(), execvp(), pipe(), dup2(), file descriptors, and process waiting. This project implements a working interactive shell in C with no parsing or readline libraries, just POSIX.

What it supports

  • Command execution of anything on PATH (ls -l, pwd, ...)
  • Piping with |, including multi-stage pipelines like cat test.txt | grep Hello | wc -l
  • Redirection: output >, append >>, and input <
  • Chaining: sequential ; and conditional && (mkdir temp && cd temp && touch hello.txt)
  • Builtins: cd and history (which must run in the parent process, since a child changing its own working directory or reading its own history helps nobody)
  • Signal handling: Ctrl+C interrupts the foreground child without killing the shell itself, verified with a dedicated sleep-based tester program

The parts that taught the most

Pipelines are where the file-descriptor bookkeeping gets honest. Each stage needs its stdout wired to the write end of one pipe and its stdin to the read end of the previous one, and every unused descriptor must be closed in both parent and children, because a single leaked write-end keeps the reader blocked forever waiting for EOF.

Signal handling has a subtle ownership question: SIGINT should kill the running child, not the shell. The shell installs its own handler that re-prompts, while children restore default behavior, which is exactly how real shells keep Ctrl+C scoped.

Conditional chaining (&&) requires reading child exit codes via waitpid status macros and short-circuiting the rest of the line on failure, a small interpreter decision that makes the shell feel real.

The code is organized as shell.c (main loop, signals, parsing), command.c (tokenization, builtins, dispatch), and execution.c (redirection and pipe plumbing).

stack

CPOSIXfork/execpipessignalsGCC