A clean, readable, and educational UNIX shell implemented in C. It supports core shell functionality you expect from a modern terminal, plus deep lessons in operating system concepts. You will learn how a shell coordinates processes, pipes data, redirects input and output, and manages jobs in the background. The project emphasizes clarity and practical understanding over clever tricks.
What you will find here is a practical project that blends hands-on coding with OS theory. It’s suitable for students, instructors, and curious developers who want to see how a real shell can be built from the ground up. The code is designed to be approachable, well-commented, and designed for experimentation. The repository serves as a learning resource as well as a starting point for extensions.
- Language: C
- Platform: Linux and other POSIX-compliant systems
- Core ideas: Process creation, execve, pipes, I/O redirection, background tasks, signals, and built-ins
- Target audience: Students of operating systems, systems programming, and shell concepts
Table of contents
- Why build your own shell?
- Core features
- How it works under the hood
- Getting started
- Building from source
- How to run and use
- Built-ins and command execution
- Piping, redirection, and I/O
- Background jobs and process management
- Signals and safety
- Debugging, tracing, and testing
- Code structure and design decisions
- Extending the shell
- Contributing and collaboration
- Release assets and where to find them
- License and attribution
Why build your own shell? A shell is the bridge between user intent and the operating system. It interprets user input, parses commands, and coordinates processes. It decides how to connect commands through pipes, where to send input and where to capture output, and when to run tasks in the background. Building a shell is a practical way to learn:
- How processes are created with fork and replaced with programs via execve
- How file descriptors are rearranged to implement input and output redirection
- How pipes connect the standard output of one process to the standard input of another
- How the kernel schedules tasks and how signals interrupt execution
- How to design clean interfaces between a command line parser, a command executor, and a job control subsystem
This project treats these topics as a practical, hands-on exploration. You will gain a better understanding of what happens when you run a simple command and why certain features exist in modern shells. It is an excellent companion to OS lectures, textbooks, and systems programming labs.
Core features
- Command parsing and tokenization
- Simple, predictable syntax for commands
- Handling of arguments, options, and subdivision into pipelines
- Execution model
- Fork-exec pattern to launch external programs
- Clean separation between parsing and execution
- Pipelining
- Support for one or more pipes in a single command line
- Data flow from left to right through pipes
- I/O redirection
- Redirect input and output using <, >, and >> operators
- Support for here-doc style syntax in extended configurations
- Built-in commands
- cd, exit, pwd, and a small help set
- Internal state management for current directory and shell options
- Background processing
- Run processes in the background with & and manage jobs
- Basic job control utilities to monitor and manipulate background tasks
- Signals
- Basic handling of common signals (SIGINT, SIGCHLD, SIGHUP)
- Safe cleanup on termination
- Shell state and history
- Minimal but practical command history support
- Environment variable propagation to child processes
- Extensibility
- Clear module boundaries for parser, executor, and built-ins
- Easy hooks to add more built-ins or features later
How it works under the hood Think of the shell as three collaborating parts:
- The parser reads a line from the user, breaks it into tokens, and builds a structured representation of the command sequence.
- The executor takes this representation and creates processes, wires up pipes, and applies redirections.
- The job control and signal subsystem monitors running processes, handles background tasks, and prepares for clean termination.
A typical interactive session follows this flow:
- The user types a command line.
- The parser validates syntax, resolves tokens, and builds a command graph.
- The executor forks processes for external commands, sets up needed pipes and redirections, and calls execve for each child.
- The shell waits for foreground jobs to finish or returns control to the user for next input while background jobs keep running.
This workflow is designed to be readable and easy to extend. Each piece has a small, clear responsibility, which makes it easier to reason about the behavior of a shell in different scenarios, such as when a pipe spans multiple processes or when a built-in needs to modify the current directory.
Getting started Follow these steps to explore the project locally. The goal is to provide a reliable, educational environment where you can experiment with the core ideas behind Unix shells.
Prerequisites
- A POSIX-compliant environment (Linux is ideal, macOS is supported with minor differences)
- A C compiler (gcc or clang)
- Basic build tools (make is convenient, but not strictly required for a manual build)
- A shell session with access to a terminal (for interactive use)
Project goals and scope
- The shell aims to be minimal yet complete enough to demonstrate the essential OS concepts.
- It focuses on clarity over feature breadth. You can read and modify the code without chasing obscure edge cases.
- It provides a solid base for education. You can add new built-ins, experiment with additional redirection modes, or implement more advanced job control features.
What you will learn by exploring this project
- How a command line is parsed into structure that can be executed
- How a child process is created and what happens during an execve call
- How to redirect input and output to files or pipes
- How to connect multiple processes with pipes for streaming data
- How to manage foreground and background tasks, including basic job control
- How to handle signals to avoid zombie processes and clean up resources
Getting started with the repository
- Clone the repository to your local machine
- Inspect the source tree to understand the organization
- src/ contains the core logic for parsing, executing, and job control
- include/ contains header definitions used across modules
- examples/ contains illustrative usage scenarios and test scripts
- tests/ contains small unit and integration tests to validate behavior
- Read the README and documentation to understand design decisions, interfaces, and usage patterns
Building from source This project is designed to be approachable for learners and easy to build on common development setups. The build steps are straightforward, and you can adapt them if you want to experiment with alternative compilers or toolchains.
Option 1: Using a simple Makefile
- If the repository includes a Makefile, run:
- make
- This will compile the source files and produce an executable in a standard location (for instance, build/shell or bin/shell)
- To run the shell:
- ./bin/shell
- If the Makefile places the binary elsewhere, adjust the path accordingly.
Option 2: Manual build with a compiler
- If you prefer to compile manually, you can compile the core modules with a command like:
- gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -c src/parser.c -o build/parser.o
- gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -c src/executor.c -o build/executor.o
- gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -c src/builtins.c -o build/builtins.o
- gcc -std=c11 -O2 -Wall -Wextra -Wpedantic -c src/main.c -o build/main.o
- gcc -o bin/shell build/parser.o build/executor.o build/builtins.o build/main.o
- The exact commands depend on your file layout and the selected entry point. If you need to adapt, adjust include paths and dependencies accordingly.
Option 3: Using a containerized environment
- For reproducible builds, you can use a container with a POSIX environment and the necessary toolchain.
- A minimal Dockerfile might install build-essential and compile the project inside a clean image.
- This approach helps when you want to demonstrate the shell in a controlled environment or present it in a classroom setting.
Running and using the shell Once you have built the shell, you can begin exploring its behavior in interactive mode.
Launching the shell
- Start from the terminal:
- ./bin/shell
- You will see a prompt and be ready to type commands.
Interactive behavior
- The shell reads a line of input from the user and interprets it as a command or a pipeline.
- It supports built-in commands to manage its environment and state.
- It can spawn external processes for non-built-in commands via fork and exec.
- It handles common shell features like pipelines and redirections to demonstrate real-world usage.
Examples of typical commands
- Simple command
- ls -l
- Piping
- ps aux | grep bash | sort -k2
- Redirection
- ls -l > listing.txt
- wc -l < listing.txt
- Appending output
- echo "Hello, shell" >> greetings.log
- Background tasks
- sleep 5 &
- echo "Background job started" && jobs
- Built-in commands
- pwd
- cd /tmp
- exit
Important usage notes
- The shell exposes a simple yet representative set of features to illustrate the core concepts.
- It is designed to be safe in an educational setting, but you should avoid running untrusted code in any shell you build or modify.
- The command parser aims for predictable behavior. If you provide complex or ambiguous syntax, the shell will respond with a clear error and show a help message.
Built-ins and command execution Built-in commands are implemented inside the shell so you can see how a shell manages its internal state without launching a separate process. At a minimum, you should expect:
- cd: Change the current working directory of the shell process.
- exit: Terminate the shell gracefully, ensuring all background processes are managed correctly.
- pwd: Print the current working directory.
- help: Show a short guide to available commands and options.
How the parser works
- The parser tokenizes the input line into commands, arguments, and operators.
- It recognizes pipes, redirection operators, and background execution markers.
- The parser builds a data structure that represents a sequence of commands and the connections between them (for example, a pipeline consisting of several processes).
How execution works
- For a simple external command, the shell forks a child process and calls execve with the given program and arguments.
- For pipelines, the shell creates a chain of pipes and arranges each process so that the output of one command becomes the input of the next.
- For redirection, the shell adjusts file descriptors in the child processes before invoking execve.
- For built-ins, the shell executes them directly in the parent process when it is safe to do so (for example, cd and exit).
Piping and I/O redirection in detail
- Piping
- Each pipe creates a pair of file descriptors: read end and write end.
- The first command’s stdout connects to the write end of the first pipe.
- Each intermediate command connects its stdin to the previous pipe’s read end and its stdout to the next pipe’s write end.
- The last command reads from the final pipe’s read end and writes to the terminal or a redirection target.
- Redirection
- Output redirection (>, >>) attaches the command’s stdout to a file descriptor pointing to a file.
- Input redirection (<) attaches the command’s stdin to a file descriptor reading from a file.
- The shell uses dup2 to swap the standard file descriptors with the file descriptor created for the redirection, then closes the original descriptors.
- Here-doc and advanced redirection
- If supported, here-doc allows embedding a short input block directly in the command line, which is fed to the command’s stdin during execution.
Background processes and job control
- The shell supports running processes in the background using the & symbol.
- In the background mode, the shell does not wait for the child to finish; it continues accepting new commands.
- Job status and basic information are tracked to allow listing and basic management.
- Basic signals are used to prevent zombie processes. The shell waits for children only when needed and reaps them safely.
Signals and safety
- SIGINT (Ctrl-C) typically interrupts foreground processes.
- SIGCHLD is captured to detect when a child process exits or stops.
- The shell should avoid leaving orphaned resources and ensure file descriptors are properly closed.
- When a child process terminates, the shell updates its internal job state and prints a concise notification if appropriate.
Debugging, tracing, and testing
- The code base includes hooks for debugging and tracing execution flow.
- You can enable verbose logging for parsing, forking, and execution events while learning how a shell behaves internally.
- A small test suite validates the behavior of parsing, pipeline creation, and redirection handling.
- You can extend tests to cover edge cases you discover in your experiments.
Code structure and design decisions
- Modularity
- The project is organized into modules with clear responsibilities. Parser, executor, and built-in command logic live in separate components with clean interfaces.
- The separation makes it easier to introduce new features, such as additional built-ins or more advanced job control.
- Readability
- The code emphasizes readability and straightforward flow. I/O, process management, and signal handling are implemented with direct, well-documented code.
- Comments explain why a given approach was chosen, not just what the code does.
- Extensibility
- The shell is designed to be extended. Adding a new built-in is a matter of implementing a handler and hooking it into the command dispatcher.
- You can adapt the parsing logic to support new operators or more advanced syntax as you learn more about shell behavior.
Code structure overview (high level)
- include/
- shell.h: Public interfaces for the shell core, built-ins, and utilities
- config.h: Compile-time options and constants
- src/
- main.c: Entry point and the main loop
- parser.c / parser.h: Tokenizing input and building command graphs
- executor.c / executor.h: Setting up pipes, redirections, and exec calls
- builtin.c / builtin.h: Built-in command implementations
- job_control.c / job_control.h: Job tracking and status
- signals.c / signals.h: Signal handling and cleanup
- utils.c / utils.h: Small helpers used across modules
- tests/
- test_parser.c, test_executor.c: Lightweight tests for core functionality
- test_integration.sh: Basic end-to-end checks
- docs/
- design-notes.md: Rationale and deeper insights
- usage.md: Practical usage guide and examples
- examples/
- usage-examples.sh: Script with representative command sequences
- pipelines-and-redirection.sh: Demonstrates common pipelines and redirections
Extending the shell
- How to add a new built-in
- Create a function that implements the built-in’s behavior with a consistent interface
- Register the built-in in the dispatcher so the parser can recognize the command name and route it to the handler
- Ensure the built-in modifies the shell state safely when it changes the current directory, shell options, or environment
- How to add a new operator or feature
- Update the parser to recognize the new operator and attach it to the command graph
- Extend the executor to implement the required data flow, redirection, or process control
- Add tests to verify the new behavior and prevent regressions
- How to observe and debug
- Enable verbose logging in the parser and executor
- Use the provided test scripts to reproduce scenarios
- Create new test cases for edge conditions you encounter while experimenting
Releases and downloads This project uses a releases-based distribution to share binaries, tarballs, or source snapshots. The main releases page holds the latest assets. To obtain a ready-to-run version or a development snapshot, visit the Releases page and download the asset that matches your needs.
- If you want to grab a binary or a source snapshot, visit the releases page:
- https://github.com/Destides/Build-your-own-shell/releases
- From the releases page, download the release asset (for example a tarball named something like Build-your-own-shell-.tar.gz) and follow the extraction and setup instructions.
- After extraction, build or run the binary as described in the Getting started and Building sections.
- If you prefer to inspect the code and build from source yourself, clone the repository and follow the build instructions in this README. The releases page also serves as a convenient way to compare the official build with your local work.
- The link to the Releases page is the right place to find binaries, source snippets, and documentation associated with each version.
Downloads note
- The link provided above has a path component, so the release asset is the file you typically download and execute. For example, you would download the tarball, extract it, and run the included binary or build with your compiler to produce the executable you use in your terminal. If you download a prebuilt binary, you will likely run something like ./shell or a similar executable. If you download a source snapshot, you will compile it locally following the instructions in the Getting started and Building from source sections.
Screenshots, diagrams, and visuals
- Terminal style imagery helps convey the shell environment. For a quick visual representation, you can include this simple shell diagram in your docs or classroom materials:
- A schematic showing the flow from user input to process creation can be created using ASCII art or simple vector diagrams. The important thing is to illustrate the flow:
- User input -> Parser -> Command graph -> Fork to create child processes -> Set up pipes and redirections -> Exec in children -> Foreground vs background -> Shell waits or returns to prompt
- You can also include a small dashboard-like image or a terminal screenshot demonstrating typical behavior (for example, a pipeline with three commands and a background job). When using images, ensure you respect licensing terms and provide attribution if required.
Usage tips and best practices
- Start with simple commands to verify the basic loop works: a single command with no redirection.
- Gradually add pipes to observe how data moves from one process to another.
- Introduce redirection one operator at a time to understand how file descriptors are re-routed.
- Try background tasks to learn how the shell handles concurrency and cleanup.
- Build small, repeatable test cases to verify each feature you modify or extend.
- Use built-in commands to manage the environment and neighborhood of commands, such as PWD and CD.
How to contribute
- You can contribute by:
- Fixing bugs reported in issues or discovered by your experiments
- Implementing new built-ins or shell features
- Enhancing the parser or executor to support more nuanced syntax
- Adding tests to extend coverage
- Improving documentation and usage examples
- Before contributing, please discuss major changes in an issue. This helps avoid conflicting work and aligns the implementation with the project’s goals.
- Follow conventional commits when you write changes. A clear, descriptive message helps others understand your intent.
Test coverage and quality
- A small test suite validates core features like parsing correctness, basic command execution, and simple pipelines.
- Integration tests demonstrate the end-to-end flow from input to output, including redirection and simple background jobs.
- You can extend tests to cover additional edge cases, such as complex pipelines or unusual path handling.
Environment and dependencies
- The project aims to minimize external dependencies to keep the learning curve gentle.
- If you enable optional features (for example, readline support or enhanced history), you may depend on external libraries. Document any optional dependencies and how to enable them.
Conventional usage patterns
- Interactive mode
- Start the shell in an interactive session and type commands as you would in a regular terminal.
- Scripted usage
- You can feed a file to the shell or execute a sequence of commands as part of an automated test or lesson plan. This helps demonstrate batch processing, pipelines, and redirections in a controlled setting.
- Educational labs
- The repository is well suited to teaching labs where students implement a subset of features and build up to full functionality.
Documentation structure
- The docs folder contains design notes and usage guides.
- The design-notes.md explains the rationale behind architecture choices.
- The usage.md demonstrates common workflows and illustrates the behavior with examples.
- Each file is written to be readable by students and instructors, with clear sections, code examples, and diagrams where helpful.
Tips for educators and learners
- Start with a minimal interactive shell and add one feature at a time.
- Use the code to demonstrate a concept, then implement a corresponding lesson in class or in a lab.
- Encourage students to modify and extend the shell. This hands-on approach makes OS concepts tangible.
- Compare the implemented shell with a standard system shell to highlight similarities and differences.
Frequently asked questions
- Can I run this shell on macOS?
- Yes, with POSIX-compliant features and standard C libraries, it runs on macOS. Some minor differences may apply depending on system calls and available headers.
- Do I need to be an expert in OS theory to use it?
- No. The project is designed for learners. You can start with basic command execution and gradually explore pipes and redirection as you gain confidence.
- Can I add new built-ins?
- Yes. The architecture is built around clear interfaces for built-ins, so you can implement new commands and hook them into the dispatcher.
Release notes and versioning
- Each release contains a snapshot of the code and, when available, prebuilt assets or build instructions for different environments.
- If you want a quick read on what changed between versions, refer to the CHANGELOG or the release notes associated with each asset on the Releases page.
License and attribution
- This project uses an OS-friendly, permissive license appropriate for educational use and classroom sharing.
- You can reuse, adapt, and extend the code for teaching and learning, provided you respect the license terms and attribution guidelines.
Acknowledgments
- The shell project draws on classic operating systems concepts and standard POSIX mechanisms.
- It benefits from a long line of educational material on forking, exec, pipes, and signal handling.
Final notes
- The repository aims to be a practical, readable, and educational resource for learning OS concepts and system programming through the lens of a working shell.
- You are invited to explore, experiment, and extend the code to deepen your understanding of how shells work and how OS primitives cooperate.
- For the latest assets and official releases, visit the Releases page and grab the appropriate release asset, such as a tarball or prebuilt binary, then follow the included setup instructions. The Releases page is your gateway to the latest official materials:
End of document.