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.
- Overview
- Features
- Architecture
- How It Works
- Getting Started
- Usage Examples
- Technical Deep Dive
- What I Learned
- Future Enhancements
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.
echo- Prints arguments to stdoutexit- Terminates the shell with status code 0type- Identifies whether a command is a built-in or external executable
- 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
The shell uses the Command Pattern to achieve a clean, extensible design:
Main (Entry Point)
└─→ CommandHandler (Dispatcher)
├─→ Command Interface
│ ├─→ EchoCommand
│ ├─→ ExitCommand
│ ├─→ TypeCommand
│ └─→ ExternalCommand
- Command Pattern: Each command is encapsulated as a separate class implementing the
Commandinterface, making it trivial to add new built-ins - HashMap Dispatch: Built-in commands are stored in a HashMap for O(1) lookup time
- Graceful Fallback: Unknown commands automatically fall through to external command execution
- Process Isolation: External commands run in separate processes using
ProcessBuilder
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
}Input is split into command name and arguments:
String[] parts = input.split(" ", 2);
String commandName = parts[0];
String arguments = parts.length > 1 ? parts[1] : "";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));
}For external commands and the type command, the shell:
- Retrieves the PATH environment variable
- Splits it by the platform-specific path separator
- Searches each directory for an executable matching the command name
- Executes the first match found
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);
}- Java 25 (uses preview features)
- Maven for building
mvn clean packageThis produces an executable JAR: target/codecrafters-shell.jar
./your_program.shOr directly with Java:
java --enable-preview -jar target/codecrafters-shell.jar$ 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]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
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.
External commands may produce output that needs to be streamed back to the user. The shell:
- Redirects stderr to stdout:
builder.redirectErrorStream(true) - Reads from the process's InputStream line by line
- Prints each line to the user's terminal
This ensures real-time output rather than buffering everything.
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
- 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
- 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
- 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
This shell is functional but could be extended with:
-
cdcommand - Change directory (requires maintaining shell state) -
pwdcommand - Print working directory - Signal handling - Ctrl+C to interrupt commands
- Exit codes - Propagate command exit status
- 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
- Better error messages
- Unit tests for each command
- Configuration file (
.shellrc) - Colorized output
- Performance profiling
codecrafters-shell-java/
├── src/main/java/
│ └── Main.java # Shell implementation
├── pom.xml # Maven configuration
├── your_program.sh # Launch script
└── README.md # This file
- CodeCrafters Shell Challenge
- POSIX Shell Specification
- Java ProcessBuilder Documentation
- Command Pattern Explained
This is a project built as part of the CodeCrafters challenge.
Built with Java 25 • Powered by curiosity and late-night debugging sessions