Inspiration
We were inspired by the foundational role that shells play in every operating system. The original smallsh assignment from OS coursework demonstrates how core system utilities, process management, signals, I/O redirection, work under the hood. We wanted to take that concept further: rewrite it in Rust for memory safety and modern ergonomics, and then expand it with built-in coreutils that normally live as separate binaries. The hackathon theme of "Rebuilding the OS: Core System Utilities" was a perfect fit.
What it does
smallsh is a fully functional Unix shell written in Rust. It supports:
- An interactive
:prompt with command parsing, I/O redirection (<,>), and background process execution (&) - Three core shell built-ins:
exit,cd,status - Signal handling: SIGINT (Ctrl-C) is ignored by the shell but kills foreground children; SIGTSTP (Ctrl-Z) toggles foreground-only mode
- Six integrated coreutils that run in-process without
fork/exec: - [echo]
- [cat]
- [grep]
- [cp]
diskusage, andbenchmark diskusagerecursively walks directories and displays file sizes with a Unicode bar chartbenchmarkmeasures disk write speed by streaming data from/dev/zeroand/dev/urandom
How we built it
nixcrate for POSIX syscalls (fork,execvp,waitpid,dup2,sigaction)libccrate for async-signal-safewrite(2)in signal handlersAtomicBoolfor the foreground-only mode flag shared between the signal handler and the main loop- [ProcessTable] struct to replace C's global mutable state for background process tracking
into a dedicated
cmd_*.rsmodule, then registered in the built-in dispatch table - Cross-compilation checks against
x86_64-unknown-linux-gnuwere run throughout development on Windows to catch platform issues early - 19 unit tests cover parsing, grep search, and error handling for every built-in
Challenges we ran into
- Cross-platform development: We developed on Windows but the shell requires Unix APIs (
fork, signals). We solved this with#[cfg(unix)]conditional compilation and cross-compile checks against a Linux target. - Signal handler safety: Rust's
println!(like C'sprintf) is not async-signal-safe. We had to use rawlibc::write(2)with hardcoded file descriptor constants inside the SIGTSTP handler. - Type mismatches: The
nixcrate's API differs subtly from rawlibcwithRawFdimports,OFlagvs raw integers,CStrvsCStringforexecvp. Each module needed careful type wiring.
Accomplishments that we're proud of
- A complete, working Unix shell in Rust with full signal handling, background process management, and I/O redirection
- Six integrated coreutils
- Zero
unsafeoutside of signal handlers, the onlyunsafecode is thelibc::writecalls andsigactionsetup, which are inherently unsafe operations - 19 passing unit tests covering parsing, search algorithms, and error handling
- Clean modular architecture: 12 focused Rust source files, each under 140 lines
What we learned
- How Unix shells actually work at the syscall level with
fork/exec/waitpidlifecycle,dup2for redirection,sigactionfor signal handling - How Rust's ownership model forces you to think carefully about shared state that C lets you scatter across globals
- The
nixcrate provides safe Rust wrappers for POSIX APIs, but signal handlers still require rawunsafecode because async-signal-safety is a runtime property the type system can't enforce - Cross-compilation with
cargo check --targetis a powerful workflow for developing Unix software on non-Unix machines
What's next for Shell
- Pipe operator (
|) chain commands together (ls | grep .rs | wc -l) - Command history with arrow-key navigation (using the
rustylinecrate) - Tab completion for filenames and built-in commands
- Environment variable expansion (
$HOME,$PATH,$$for PID) - Job control (
fg,bg,jobs) for full background process management - More coreutils:
mv,rm,mkdir,head,tail,sort,uniq - Packaging as a standalone Linux binary for embedded/minimal systems
Log in or sign up for Devpost to join the conversation.