Nash
Abstract
Abstract
Google Meet Link: https://meet.google.com/kit-msea-zyj
GitHub Repository: https://github.com/hashShank/Nash
Introduction
The Unix shell is one of the most foundational tools in systems programming. It serves as the primary interface between a user and the operating system — accepting commands, locating executables, spawning child processes, and managing their lifecycle. Despite its seemingly simple surface, a shell encapsulates several of the most important abstractions in OS design: processes, file descriptors, and the fork/exec model.
nash (Nitk-Bash) was developed entirely from scratch in C, deliberately invoking raw system calls rather than relying on high-level library wrappers. The aim was to build a genuinely functional shell while gaining a concrete understanding of how the operating system creates and manages processes, how executables are located and loaded, and how standard I/O streams can be redirected at the file-descriptor level.
Modern shells such as bash and zsh provide dozens of additional features — job control, history, tab completion, scripting — all built on top of exactly the same primitives that nash implements. Building nash therefore provides a clear, hands-on window into the internals of any production-grade shell.
Objectives
- Build a working interactive shell with a REPL (Read -> Evaluate -> Print -> Loop) loop and a custom prompt (nash>).
- Execute external commands by searching user-defined PATH directories using fork() and execv().
- Implement three built-in commands — exit, cd, and path — handled internally without forking.
- Support output redirection using the > operator, routing both stdout and stderr to a file via dup2().
- Enable parallel execution of multiple commands separated by the & operator.
- Handle all error conditions with a single, uniform error message written to stderr.
Implementation
REPL Loop
nash runs an infinite while loop in main(). On each iteration it prints the prompt nash>, flushes stdout, and reads a line with getline(). If EOF is encountered (Ctrl+D), the loop breaks and the shell exits cleanly. Trailing newlines are stripped and empty lines are skipped.
Parallel Execution Splitting
Before any parsing, the raw input line is duplicated and split on the & delimiter using strtok(). Each non-empty segment is stored in cmd_strings[]. All segments are then forked in a first pass, and a separate second pass collects all child exit statuses with waitpid() — ensuring all commands run concurrently rather than sequentially.
Command Parsing
parse_command() tokenises a single command string using strtok() on whitespace. It scans for the > token to detect output redirection, extracting the filename that follows. Multiple > symbols or a missing filename are treated as errors. The remaining tokens populate argv[], which is NULL-terminated for execv().
Path Resolution
nash maintains a global array path_dirs[] initialised to /bin and /usr/bin. find_in_path() iterates through these directories, constructing candidate paths with snprintf() and testing each with access(candidate, X_OK). The first executable match is returned as a malloc'd string. Commands beginning with / are checked directly without searching.
Built-in Commands
Built-ins are checked by name before any forking occurs inside process_command():
exit — calls free_paths() then exit(0). Any argument is an error.
cd <dir> — calls chdir() with exactly one argument. Zero or more than one argument is an error.
path <dir> ... — calls free_paths() to clear the current list, then repopulates path_dirs[] with strdup() copies of the supplied arguments. Passing no arguments clears the path entirely, preventing any external command from running.
Output Redirection
When parse_command() detects a > token, the child process (after fork()) opens the target file with O_WRONLY | O_CREAT | O_TRUNC and redirects STDOUT_FILENO to it via dup2(). The original file descriptor is then closed before execv() is called, so the executed program writes directly to the file.
Error Handling
A single global error string is used throughout:
cconst char error_message[] = "An error has occurred\n";
write(STDERR_FILENO, error_message, strlen(error_message));
This message is emitted for unknown commands, bad argument counts, failed forks, failed chdir calls, missing redirect targets, and multiple redirect operators. The shell continues running after non-fatal errors.
Results
nash was tested across a broad set of scenarios:
- Single and multi-argument commands (e.g. ls -la /tmp) execute correctly, with the binary located via path_dirs[].
- The path built-in correctly updates the search list at runtime; clearing it with path (no args) prevents all external commands from running.
- cd changes the working directory correctly and reports an error for wrong argument counts.
- Output redirection writes both stdout and stderr to the target file; nothing is printed to the terminal.
- Commands joined with & all launch concurrently — the shell forks all of them before calling waitpid() on any — and the prompt only returns after all have finished.
- Malformed input (extra > symbols, missing filenames, unknown commands) all produce the single standardised error message without crashing the shell.
- Variable whitespace around commands, arguments, and operators is handled correctly by the strtok() tokeniser.
Conclusion
nash demonstrates that a capable, interactive Unix shell can be built with a compact and well-structured set of system calls. The project makes concrete several abstractions that are easy to take for granted in daily use: the separation between a shell and the programs it runs, the mechanics of the fork/exec model, and the way file descriptors can be safely manipulated in a child process before a new program image is loaded.
The modular design — separate functions for path initialisation, command parsing, path resolution, and execution — provides a clean foundation for future extension. Planned improvements include pipe support (|), stderr-only redirection, background job tracking, command history, and signal handling for Ctrl-C and Ctrl-Z.
References
[1] Kerrisk, M. — The Linux Programming Interface. No Starch Press, 2010.
[2] Linux Programmer's Manual — fork(2), execv(2), waitpid(2), dup2(2), chdir(2), access(2), getline(3), strtok(3), open(2).
[3] Arpaci-Dusseau, R. H. & Arpaci-Dusseau, A. C. — Operating Systems: Three Easy Pieces. Arpaci-Dusseau Books, 2023. http://ostep.org
[4] IEEE NITK Virtual Expo 2025 — https://ieee.l2.nitk.ac.in/virtual_expo/
Report Information
Team Members
Team Members
Report Details
Created: May 17, 2026, 10:58 p.m.
Approved by: None
Approval date: None
Report Details
Created: May 17, 2026, 10:58 p.m.
Approved by: None
Approval date: None