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.
- 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.
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.
Learn and implement:
fork()execvp()waitpid()- Process IDs
- Exit statuses
- Error handling with
perror()
For an external command, the shell should:
- Read a command line.
- Parse it into an argument array.
- Call
fork(). - Have the child call
execvp(). - Have the parent call
waitpid().
This is the core shell and process model.
Implement built-ins inside the shell process:
exitcdpwdhelp
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
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
Learn about:
- Environment variables
getenv()setenv()unsetenv()environPATH
Add:
$VARexpansionexportunset- Optional
env - A prompt showing the current directory
Initially rely on execvp() for PATH lookup. Later, manually implement command lookup as a learning exercise.
Implement:
>>><- Optional
2>and2>>
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
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
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.
Handle:
SIGINTfromCtrl-CSIGTSTPfromCtrl-ZSIGCHLDwhen children change stateSIGQUIT
Desired behavior:
Ctrl-Cinterrupts the running command.Ctrl-Cdoes not terminate the shell itself.- The shell waits correctly for interrupted children.
- Background children are reaped.
- Signal handlers follow async-signal-safety rules.
This is the advanced shell stage. Learn:
- Process groups
- Sessions
- Controlling terminals
setpgid()tcsetpgrp()tcgetpgrp()
Then support:
Ctrl-Zfgbgjobs- Stopped and resumed processes
- Pipelines treated as one job
Example:
sleep 100 | cat &
jobs
fg
Optional final features:
- Startup configuration file
- Command history
- History expansion
- Aliases
- Custom prompts
sourceumaskwhichortype- Command timing
- Colored output
History editing can first be implemented manually for learning. A library such as GNU Readline can be studied later.
| 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.
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
- Prompt and input loop
- Basic argument parsing
-
fork()andexecvp() -
waitpid()and exit status reporting -
cd,pwd,help, andexit - EOF and error handling
- Quotes and escaping
- Environment expansion
-
exportandunset - Input/output redirection
- Pipelines
- Background processes
- Zombie reaping
- Signal handling
- Job control
- Startup configuration
- History and aliases
- Automated tests
For each feature, record:
- Which system calls or library functions are involved.
- Which process performs each operation.
- What happens on success and failure.
- How file descriptors and process state change.
- How the behavior was tested.
These notes should become part of the project documentation by the end of the semester.