Skip to content

theresasogunle/Shell

Repository files navigation

Building Your Own Shell from Scratch

A shell implementation in Java that interprets commands, executes external programs, and handles built-in commands. This project demonstrates fundamental systems programming concepts including REPL design, process management, and command parsing.

Table of Contents

Overview

Shells are the unsung heroes of computing—they're the interface between users and the operating system kernel. This project is a journey into building one from scratch in Java, implementing core functionality that mirrors what you use every day in bash, zsh, or fish.

Rather than using existing shell libraries, this implementation handles everything manually: parsing input, managing processes, searching the PATH, and executing both built-in and external commands.

Features

Built-in Commands

  • echo - Prints arguments to stdout
  • exit - Terminates the shell with status code 0
  • type - Identifies whether a command is a built-in or external executable

Core Capabilities

  • REPL Loop - Read-Eval-Print Loop for interactive command execution
  • External Command Execution - Runs any executable found in PATH
  • PATH Resolution - Searches through directories in the PATH environment variable
  • Process Management - Spawns and manages child processes using Java's ProcessBuilder
  • Stream Handling - Captures and displays stdout from external commands

Architecture

The shell uses the Command Pattern to achieve a clean, extensible design:

Main (Entry Point)
  └─→ CommandHandler (Dispatcher)
       ├─→ Command Interface
       │    ├─→ EchoCommand
       │    ├─→ ExitCommand
       │    ├─→ TypeCommand
       │    └─→ ExternalCommand

Design Decisions

  1. Command Pattern: Each command is encapsulated as a separate class implementing the Command interface, making it trivial to add new built-ins
  2. HashMap Dispatch: Built-in commands are stored in a HashMap for O(1) lookup time
  3. Graceful Fallback: Unknown commands automatically fall through to external command execution
  4. Process Isolation: External commands run in separate processes using ProcessBuilder

How It Works

1. The REPL Loop

The shell runs an infinite loop that:

while (true) {
    System.out.print("$ ");           // Display prompt
    String input = scanner.nextLine(); // Read user input
    commandHandler.handleCommand(input); // Parse and execute
}

2. Command Parsing

Input is split into command name and arguments:

String[] parts = input.split(" ", 2);
String commandName = parts[0];
String arguments = parts.length > 1 ? parts[1] : "";

3. Command Resolution

The handler checks if the command is built-in, otherwise treats it as external:

Command command = commands.get(commandName);
if (command != null) {
    command.execute(arguments, Optional.empty());
} else {
    // Attempt to run as external command
    extCommand.execute(arguments, Optional.of(commandName));
}

4. PATH Lookup

For external commands and the type command, the shell:

  1. Retrieves the PATH environment variable
  2. Splits it by the platform-specific path separator
  3. Searches each directory for an executable matching the command name
  4. Executes the first match found

5. Process Execution

External commands are executed using ProcessBuilder:

ProcessBuilder builder = new ProcessBuilder(commandParts);
builder.redirectErrorStream(true);
Process process = builder.start();

// Stream output back to user
BufferedReader reader = new BufferedReader(
    new InputStreamReader(process.getInputStream())
);
String line;
while ((line = reader.readLine()) != null) {
    System.out.println(line);
}

Getting Started

Prerequisites

  • Java 25 (uses preview features)
  • Maven for building

Building the Shell

mvn clean package

This produces an executable JAR: target/codecrafters-shell.jar

Running the Shell

./your_program.sh

Or directly with Java:

java --enable-preview -jar target/codecrafters-shell.jar

Usage Examples

$ echo Hello, World!
Hello, World!

$ type echo
echo is a shell builtin

$ type cat
cat is /bin/cat

$ type nonexistent
nonexistent: not found

$ cat file.txt
[Contents of file.txt displayed]

$ ls -la
[Directory listing displayed]

$ exit
[Shell terminates]

Technical Deep Dive

Why Java for a Shell?

While shells are traditionally written in C, Java provides:

  • Cross-platform compatibility (Windows, macOS, Linux)
  • Built-in process management via ProcessBuilder
  • Memory safety without manual memory management
  • Rich standard library for I/O operations

Handling the PATH

The PATH environment variable contains a colon-separated (or semicolon on Windows) list of directories. The shell:

String path = System.getenv("PATH");
String[] dirs = path.split(File.pathSeparator);

for (String dir : dirs) {
    File file = new File(dir, commandName);
    if (file.exists() && file.canExecute()) {
        // Found executable
    }
}

The File.pathSeparator automatically uses : on Unix or ; on Windows.

Process I/O Management

External commands may produce output that needs to be streamed back to the user. The shell:

  1. Redirects stderr to stdout: builder.redirectErrorStream(true)
  2. Reads from the process's InputStream line by line
  3. Prints each line to the user's terminal

This ensures real-time output rather than buffering everything.

Command Pattern Benefits

The Command pattern provides:

  • Open/Closed Principle: Add new commands without modifying existing code
  • Single Responsibility: Each command class has one job
  • Testability: Commands can be unit tested in isolation
  • Extensibility: New commands just implement the interface and register in the HashMap

What I Learned

Systems Programming Concepts

  • REPL Design: How interactive shells process input in a continuous loop
  • Process Management: Creating and managing child processes in Java
  • Environment Variables: How shells use PATH to locate executables
  • Stream Redirection: Capturing and displaying subprocess output

Software Engineering

  • Command Pattern: Practical application of a classic design pattern
  • Error Handling: Gracefully handling missing commands and I/O errors
  • Separation of Concerns: Clean architecture with distinct responsibilities

Java-Specific

  • ProcessBuilder API: Fine-grained control over process creation
  • Optional Type: Using Optional for nullable command names
  • File System API: Platform-independent file and path operations

Future Enhancements

This shell is functional but could be extended with:

Essential Features

  • cd command - Change directory (requires maintaining shell state)
  • pwd command - Print working directory
  • Signal handling - Ctrl+C to interrupt commands
  • Exit codes - Propagate command exit status

Advanced Features

  • Piping - Chain commands with |
  • Redirection - Input/output redirection with <, >, >>
  • Background jobs - Run commands with &
  • Environment variables - Set and expand variables
  • Command history - Navigate previous commands with arrow keys
  • Tab completion - Auto-complete commands and file paths
  • Globbing - Expand wildcards like *.txt
  • Quoting - Handle single/double quotes for arguments with spaces
  • Command substitution - Execute commands within backticks or $()
  • Aliases - User-defined command shortcuts
  • Scripting - Execute shell scripts from files

Quality Improvements

  • Better error messages
  • Unit tests for each command
  • Configuration file (.shellrc)
  • Colorized output
  • Performance profiling

Project Structure

codecrafters-shell-java/
├── src/main/java/
│   └── Main.java              # Shell implementation
├── pom.xml                    # Maven configuration
├── your_program.sh            # Launch script
└── README.md                  # This file

Resources

License

This is a project built as part of the CodeCrafters challenge.


Built with Java 25 • Powered by curiosity and late-night debugging sessions

About

Shell Implementation from scratch

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •