Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OShell

OShell is a small Unix-like shell built from scratch in C as a semester-long operating-systems learning project.

The purpose of this project is to understand what a shell actually does by implementing features incrementally while learning processes, system calls, file descriptors, signals, and terminal control.

Project principles

  • Keep OS-facing behavior visible in the code.
  • Add one concept at a time as it is covered in the course.
  • Separate input, parsing, built-ins, process execution, and job control.
  • Test every feature from the terminal and document what was learned.
  • Prefer understanding the underlying system calls before adding abstractions or libraries.

Implementation roadmap

Phase 0: Project foundation

Set up the repository, a C build system, compiler warnings, a README, and basic manual testing notes.

The first milestone is a program that starts, displays a prompt, reads input, and exits cleanly.

Phase 1: Command execution

Learn and implement:

  • fork()
  • execvp()
  • waitpid()
  • Process IDs
  • Exit statuses
  • Error handling with perror()

For an external command, the shell should:

  1. Read a command line.
  2. Parse it into an argument array.
  3. Call fork().
  4. Have the child call execvp().
  5. Have the parent call waitpid().

This is the core shell and process model.

Phase 2: Built-in commands

Implement built-ins inside the shell process:

  • exit
  • cd
  • pwd
  • help

cd must run inside the shell process. If it ran in a child, only the child’s working directory would change.

Also add:

  • Shell return status
  • Empty-command handling
  • Ctrl-D/EOF handling
  • Clear error messages

Phase 3: Better command parsing

Start with whitespace-separated arguments, then improve the parser to support:

  • Single quotes: 'hello world'
  • Double quotes: "hello world"
  • Backslash escaping: hello\ world
  • Unmatched-quote errors
  • Maximum-argument limits
  • Empty quoted strings

Keep parsing separate from execution:

input line → tokenizer → parsed command structure → executor

Phase 4: Environment and command lookup

Learn about:

  • Environment variables
  • getenv()
  • setenv()
  • unsetenv()
  • environ
  • PATH

Add:

  • $VAR expansion
  • export
  • unset
  • Optional env
  • A prompt showing the current directory

Initially rely on execvp() for PATH lookup. Later, manually implement command lookup as a learning exercise.

Phase 5: Input and output redirection

Implement:

  • >
  • >>
  • <
  • Optional 2> and 2>>

Learn:

  • open()
  • File permission flags
  • dup2()
  • close()
  • Standard file descriptors: stdin, stdout, and stderr

The child should configure redirection before calling execvp().

Example tests:

echo hello > output.txt
cat < output.txt
echo again >> output.txt

Phase 6: Pipelines

Implement commands such as:

ls | grep ".c" | wc -l

Learn:

  • pipe()
  • Multiple children
  • Connecting descriptors with dup2()
  • Closing unused pipe ends
  • Waiting for every process in a pipeline

Pipeline execution should conceptually follow this sequence:

parse pipeline
    ↓
create pipes
    ↓
fork one child per command
    ↓
connect stdin/stdout
    ↓
close unused descriptors
    ↓
wait for all children

Phase 7: Background processes

Add support for commands such as:

sleep 10 &

Learn:

  • Foreground versus background processes
  • Non-blocking waitpid()
  • WNOHANG
  • Zombie processes
  • Reaping completed children

Possible built-ins include jobs and wait.

The shell must remain usable while a background command runs.

Phase 8: Signals

Handle:

  • SIGINT from Ctrl-C
  • SIGTSTP from Ctrl-Z
  • SIGCHLD when children change state
  • SIGQUIT

Desired behavior:

  • Ctrl-C interrupts the running command.
  • Ctrl-C does not terminate the shell itself.
  • The shell waits correctly for interrupted children.
  • Background children are reaped.
  • Signal handlers follow async-signal-safety rules.

Phase 9: Job control

This is the advanced shell stage. Learn:

  • Process groups
  • Sessions
  • Controlling terminals
  • setpgid()
  • tcsetpgrp()
  • tcgetpgrp()

Then support:

  • Ctrl-Z
  • fg
  • bg
  • jobs
  • Stopped and resumed processes
  • Pipelines treated as one job

Example:

sleep 100 | cat &
jobs
fg

Phase 10: Startup and polish

Optional final features:

  • Startup configuration file
  • Command history
  • History expansion
  • Aliases
  • Custom prompts
  • source
  • umask
  • which or type
  • Command timing
  • Colored output

History editing can first be implemented manually for learning. A library such as GNU Readline can be studied later.

Suggested four-to-five-month schedule

Time Main focus
Weeks 1–2 Input loop, basic parsing, fork, exec, waitpid
Weeks 3–4 Built-ins, exit codes, errors, EOF
Weeks 5–7 Quotes, escaping, parser redesign
Weeks 8–9 Environment variables and PATH
Weeks 10–12 Redirection
Weeks 13–15 Pipelines
Weeks 16–17 Background processes and zombie handling
Weeks 18–20 Signals and basic jobs
Weeks 21–24 Job control, testing, documentation, and polish

The schedule is flexible. It is more important to understand each stage than to reach every optional feature.

Suggested repository structure

Begin with one or two source files. Split the project only when the code becomes difficult to navigate.

OShell/
├── README.md
├── Makefile
├── src/
│   ├── main.c
│   ├── parser.c
│   ├── parser.h
│   ├── builtins.c
│   ├── builtins.h
│   ├── executor.c
│   ├── executor.h
│   ├── jobs.c
│   └── jobs.h
├── tests/
└── docs/
    ├── processes.md
    ├── file-descriptors.md
    ├── signals.md
    └── job-control.md

Feature checklist

  • Prompt and input loop
  • Basic argument parsing
  • fork() and execvp()
  • waitpid() and exit status reporting
  • cd, pwd, help, and exit
  • EOF and error handling
  • Quotes and escaping
  • Environment expansion
  • export and unset
  • Input/output redirection
  • Pipelines
  • Background processes
  • Zombie reaping
  • Signal handling
  • Job control
  • Startup configuration
  • History and aliases
  • Automated tests

Study notes for every feature

For each feature, record:

  1. Which system calls or library functions are involved.
  2. Which process performs each operation.
  3. What happens on success and failure.
  4. How file descriptors and process state change.
  5. How the behavior was tested.

These notes should become part of the project documentation by the end of the semester.

About

A Unix-like shell written from scratch in C

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages