Project 1: Minimal Command Executor
Build a minimal interactive shell that forks and execs external commands with correct exit status handling.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 1: Beginner (The Tinkerer) |
| Time Estimate | Weekend |
| Main Programming Language | C |
| Alternative Programming Languages | Rust, Go, Zig |
| Coolness Level | Level 2: Practical but Forgettable |
| Business Potential | 1. The “Resume Gold” (Educational/Personal Brand) |
| Prerequisites | C basics, fork/exec/wait, basic command line usage |
| Key Topics | fork/exec, argv parsing, PATH lookup, exit status |
1. Learning Objectives
By completing this project, you will:
- Explain and implement fork/exec in the context of a shell.
- Build a working minimal command executor that matches the project specification.
- Design tests that validate correctness and edge cases.
- Document design decisions, trade-offs, and limitations.
2. All Theory Needed (Per-Concept Breakdown)
Process Creation and Exec Lifecycle
Fundamentals
A Unix shell is a long-running parent process that repeatedly creates child processes to run external commands. The split between fork() and execve() is the foundation: fork() clones the current process so the child inherits memory, file descriptors, environment, and current working directory, while execve() replaces the child image with a new program. This separation is why a shell can set up redirections, pipelines, and signal dispositions before launching a program. It is also why built-ins must run in the parent: only the parent can change the shell’s own state (like its directory or variables). If you understand when the parent waits, when it does not, and what the child inherits, you can predict how any shell command behaves. This concept is the root of process orchestration and almost every other shell feature.
Deep Dive into the concept
The process lifecycle in a shell is a choreography between parent and child processes that must be deterministic, observable, and correct under failure. When the shell reads a command, it first decides whether the command is a built-in or an external program. Built-ins execute in the parent and therefore can mutate shell state. For external commands, the shell calls fork(). Internally, fork() creates a new task by duplicating the parent’s address space, file descriptor table, signal dispositions, and working directory. Modern kernels implement this with copy-on-write, so the child’s memory is not physically copied until it changes. From the shell’s perspective, fork() returns twice: once in the parent with the child PID, and once in the child with return value 0. This dual return is what allows the same code path to branch into parent logic versus child logic.
Once in the child, the shell must prepare the execution environment. This is where file descriptor wiring happens: dup2() to connect pipes or redirected files onto STDIN_FILENO, STDOUT_FILENO, and STDERR_FILENO; close() to remove unused descriptors; and chdir() if the command is a subshell with a different working directory. The child must also reset signal handlers to default for signals like SIGINT and SIGTSTP if the parent shell ignores them. Failure to do this leaves child programs “immune” to Ctrl+C because they inherit the shell’s ignored handlers. The child may also join or create a process group when pipelines or job control are involved, which matters for terminal control and signal delivery. Only after the environment is correct does the child call execve() (or execvp() for PATH lookup). At that point, the program image is replaced; the child’s memory, stack, and code become the new program, but the file descriptor table and environment remain as you configured them.
The parent does not disappear. It either waits for the child (foreground execution) or returns immediately (background execution). Waiting is done with waitpid(), which reports how the child finished: a normal exit (WIFEXITED) with an exit code, or a signal termination (WIFSIGNALED) with the terminating signal. Shells interpret these status codes to update $? and to print diagnostic messages like “Terminated by signal 9”. A robust shell handles interrupted waits (EINTR) and reaps all children to avoid zombies. In interactive shells, a SIGCHLD handler often records child state changes and wakes the main loop so that completed background jobs are announced promptly.
Failure handling is a central part of the lifecycle. If fork() fails (out of memory or process limit), the shell must report an error and continue running. If execve() fails, the child must print an error and exit with a defined status (commonly 127 for “command not found” and 126 for “found but not executable”). This behavior is relied upon by scripts, so the shell must be consistent. The parent should not attempt to recover from a failed exec by continuing in the child; the child must exit to avoid running shell code in an unexpected state.
Finally, remember that the execution environment is more than variables: it includes umask, current directory, signal mask, resource limits, and open file descriptors. A shell that incorrectly preserves or resets any of these will behave differently from the system shells you are comparing against. For example, if you forget to set close-on-exec on internal file descriptors, a child process might inherit unexpected descriptors, causing hangs (pipes never closing) or security leaks (files exposed). These subtle lifecycle details distinguish toy shells from robust ones.
How this fits on projects This concept is central to command execution, pipelines, redirection, and job control, so it appears in almost every project that launches external programs.
Definitions & key terms
- fork(): Clone the current process into a child process.
- execve(): Replace the current process image with a new program.
- waitpid(): Wait for a child process to change state.
- Zombie: A terminated child that has not been reaped.
- Copy-on-write: Memory optimization where pages are copied only when written.
Mental model diagram
Parent Shell
|
| fork()
v
Child Shell -- set fds/signals -- execve("/bin/ls")
|
| exit(status)
v
Parent waits -> collects status -> updates $?
How it works (step-by-step)
- Parse the command into a simple command node.
- Classify: built-in/function vs external.
- Fork a child if external. Invariant: parent must not block unless foreground.
- Child setup: apply redirections, reset signals, set process group if needed.
- Exec the program image. Failure mode:
execvereturns with errno. - Parent waits for foreground child or records job for background.
- Update
$?and job table; reap zombies. Failure mode: missedwaitpid().
Minimal concrete example
pid_t pid = fork();
if (pid == 0) {
// Child: replace image
execlp("ls", "ls", "-l", NULL);
perror("exec failed");
_exit(127);
}
int status;
waitpid(pid, &status, 0);
printf("status=%d\n", WEXITSTATUS(status));
Common misconceptions
- “fork runs the program” ->
fork()only clones;exec()runs the program. - “exit status is boolean” -> only
0is success; non-zero encodes errors. - “child changes affect parent” -> changes are isolated after
fork().
Check-your-understanding questions
- Why must a shell use
fork()beforeexecve()for external commands? - What happens if a parent never calls
waitpid()for a child? - Why do shells reset signal handlers in the child before
exec()?
Check-your-understanding answers
- The shell must keep running;
execve()replaces the current process. - The child becomes a zombie until it is reaped.
- Otherwise the child inherits ignored signals and cannot be controlled.
Real-world applications
- Interactive shells (
bash,dash,zsh). - Process supervisors and daemons that spawn workers.
- Build systems that run many external commands.
Where you’ll apply it
- In this project: see §3.2 Functional Requirements and §5.10 Phase 2.
- Also used in: P05 Pipeline System, P06 I/O Redirection Engine, P08 Signal Handler, P17 Capstone - Your Own Shell
References
- “Advanced Programming in the UNIX Environment” (Process Control).
- “The Linux Programming Interface” (Process and exec chapters).
- POSIX Shell Command Language (execution environment).
Key insights A shell is primarily a process orchestrator, not a program runner.
Summary Understanding the fork/exec lifecycle gives you the ability to predict how shell commands behave and why the shell can keep control while running external programs.
Homework/Exercises to practice the concept
- Write a launcher that runs a command and prints the exit status.
- Add a flag to run the command in the background without waiting.
- Use
strace -fordtrussto observe fork/exec/wait.
Solutions to the homework/exercises
- Use
fork(),execvp(),waitpid(), andWEXITSTATUS. - Skip
waitpid()for background and add a SIGCHLD reaper. - Trace system calls and confirm the sequence matches your mental model.
Exit Status, Wait Semantics, and Truthiness
Fundamentals
Shells treat exit status as the primary truth value of command execution. An exit status is an integer reported when a process terminates; by convention, 0 means success and non-zero means failure. The shell reads this status via waitpid() and exposes it as $?. This is not just metadata: control flow operators (&&, ||, if, while) all use exit status to decide what to do next. A correct shell must distinguish normal exit from signal termination, map errors to standard codes like 126 and 127, and propagate the right status through pipelines and compound commands. This concept links process control with scripting semantics and is essential for correctness.
Deep Dive into the concept
When a child exits, the kernel encodes the reason in a status word returned by waitpid(). The shell must interpret this correctly: WIFEXITED(status) indicates normal completion and WEXITSTATUS(status) yields the actual exit code (0–255). WIFSIGNALED(status) indicates termination by a signal, and WTERMSIG(status) gives the signal number. Many shells represent signal termination as 128 + signal, so a process killed by SIGINT (2) yields status 130. This is not just a convention; scripts rely on these distinctions. For example, build scripts may treat 127 as “command not found” and fail fast, while they may retry on other codes.
Exit status also interacts with pipelines. POSIX allows the status of a pipeline to be either the status of the last command or a composite status, and some shells provide options (like pipefail) to change this behavior. If you are building a minimal shell, you should document your choice and implement it consistently. For job control, foreground pipelines typically update $? when the last process in the pipeline completes, while background pipelines update job tables but do not immediately change $?.
The concept of “truthiness” in shell scripting is inverted relative to many languages: success is 0 (true), and failure is non-zero (false). This can surprise programmers coming from C-like languages where 0 is false. In shell, if cmd; then ... fi runs the then branch if cmd exits with 0. Control operators && and || are just syntactic sugar over this truth model: a && b runs b only if a succeeded, and a || b runs b only if a failed. A shell that gets this wrong will make scripts behave unpredictably.
Exit status also carries subtle edge cases. For instance, built-ins must set $? explicitly; functions return their last command’s exit status unless overridden by return. If the shell itself exits due to a fatal error, it should return a meaningful exit status to its parent (often 2 for syntax errors in POSIX). If a child is stopped (SIGTSTP), the status indicates stoppage rather than exit; a job control shell must track that and resume it later.
How this fits on projects You will use exit status to decide control flow, report failures, and mirror real shell behavior in every project that executes commands.
Definitions & key terms
- Exit status: Integer code set by a process at termination.
- WIFEXITED/WEXITSTATUS: Macros to inspect normal exit codes.
- WIFSIGNALED/WTERMSIG: Macros to inspect signal termination.
- Truthiness: In shell,
0is true and non-zero is false.
Mental model diagram
child exits -> kernel encodes status -> waitpid() -> shell updates $?
| |
+-- signal? -> 128 + signal +-- normal? -> exit code
How it works (step-by-step)
- Parent calls
waitpid()for a child PID. - Inspect
WIFEXITEDvsWIFSIGNALED. - Map signal termination to
128 + signal. - Store the code in
$?and in job metadata. - For pipelines, choose consistent policy (last command or pipefail).
Minimal concrete example
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
shell_status = WEXITSTATUS(status);
} else if (WIFSIGNALED(status)) {
shell_status = 128 + WTERMSIG(status);
}
Common misconceptions
- “Non-zero is true” -> in shell, non-zero is false.
- “Signal termination has no exit code” -> shells map signals to 128+N.
- “Pipelines always return last status” -> depends on policy.
Check-your-understanding questions
- Why does
false && echo oknot print anything? - What should
$?be when a process is killed by SIGKILL (9)? - How does a shell decide the status of a pipeline?
Check-your-understanding answers
- Because
falseexits non-zero, so the&&short-circuits. - 128 + 9 = 137 in most shells.
- By policy: often the last command, optionally pipefail.
Real-world applications
- Shell scripting control flow.
- CI pipelines that interpret exit codes.
- Service supervisors that restart failed programs.
Where you’ll apply it
- In this project: see §3.2 Functional Requirements and §6 Testing Strategy.
- Also used in: P04 Built-in Commands Engine, P14 Script Interpreter
References
- POSIX Shell Command Language (exit status rules).
- “Advanced Programming in the UNIX Environment” (waitpid).
Key insights Exit status is the shell’s core truth value, not a minor detail.
Summary Correct exit status handling ties process control to scripting semantics and determines whether higher-level logic behaves correctly.
Homework/Exercises to practice the concept
- Write a program that runs
falseand prints its exit status. - Send SIGINT to a child and print the resulting
$?equivalent. - Build a tiny pipeline and decide how you will compute its status.
Solutions to the homework/exercises
- Use
fork/execandwaitpidto capture the status. - Use
kill(child, SIGINT)and map to 128+signal. - Record the last child’s status, or implement pipefail.
Argument Vector Construction and PATH Lookup
Fundamentals
Before a shell can execute a command, it must convert a line of text into an argument vector (argv) and locate the program on disk. This seems simple, but it is the glue between parsing and execution. The first word becomes the command name, and the remaining words become arguments passed to the program. For external commands without a slash, the shell must search each directory in the PATH environment variable, build candidate paths, and test whether they are executable. A correct implementation handles empty path elements, . in PATH, and permission errors. Even minimal shells depend on correct argv construction and path resolution.
Deep Dive into the concept
The execve() system call expects two critical inputs: argv, an array of strings where argv[0] is the program name, and envp, an array of environment variables. Building argv is easy only if you ignore quoting, escaping, and expansions; for a minimal executor, you may split on whitespace, but the code should still produce a valid, NULL-terminated array. In a full shell, argv construction happens after expansions, quote removal, and field splitting. The order matters: for example, quoted strings should not be split by whitespace, and globbing should expand to multiple argv entries. Even in a minimal shell, you should treat consecutive spaces as a single separator and preserve the token order exactly.
PATH lookup is equally subtle. If the command contains a /, the shell must treat it as a path and attempt to execute directly. If it does not, the shell searches the colon-separated list in PATH. Each element can be empty; an empty element means “current directory.” When you iterate over PATH, you must build dir + "/" + cmd carefully, handle trailing slashes, and check execute permissions with access(path, X_OK) or by attempting execve and checking errno. The shell should distinguish between “not found” and “not executable”: POSIX uses exit status 127 for missing commands and 126 for found but non-executable commands. For errors like ENOEXEC (text file without shebang), the shell may choose to run it with /bin/sh or return an error, depending on your design scope.
Another detail is the difference between execvp() and manual PATH search. execvp() searches PATH for you, but you still need to handle error mapping and messaging. If you implement the search manually, you can produce more informative diagnostics, record the resolved path, and implement hashing to cache results. Shells like bash maintain a hash table of command lookups to avoid repeated directory scans, invalidating the cache when PATH changes. While your minimal executor may skip hashing, you should structure the code so it can be added later.
Finally, argument vector construction is not just string splitting; it is a data-structure problem. You must allocate storage for each token, store them in a contiguous char* array, and ensure the array is NULL-terminated. You also need to decide ownership and lifetimes: who frees the strings after exec or built-in handling? A consistent memory strategy will prevent leaks and double frees. Even a small shell benefits from a “command struct” that owns argv, the original line, and any metadata such as redirections.
How this fits on projects This concept connects tokenization to execution and appears in every project that launches commands or resolves filenames.
Definitions & key terms
- argv: Argument vector passed to
execve(). - PATH: Colon-separated list of directories used for command lookup.
- Shebang:
#!line that selects an interpreter for a script. - X_OK: Permission check for executability.
Mental model diagram
"ls -l /tmp" -> tokens -> argv[] -> PATH search -> /bin/ls -> execve
How it works (step-by-step)
- Tokenize line into words.
- Build
argvarray and append NULL terminator. - If command contains
/, attempt directexecve. - Else iterate PATH entries, build candidate paths, test execute.
- On success,
execvethe resolved path; on failure, map errno.
Minimal concrete example
char *argv[] = {"ls", "-l", NULL};
execvp(argv[0], argv); // uses PATH automatically
Common misconceptions
- “argv[0] doesn’t matter” -> many programs read argv[0] for mode.
- “PATH search is simple” -> empty elements mean current directory.
- “execvp handles errors” -> you must map errno to shell status.
Check-your-understanding questions
- When should the shell skip PATH search?
- What is the meaning of an empty PATH element?
- Why is
argvrequired to be NULL-terminated?
Check-your-understanding answers
- When the command contains a
/path separator. - It refers to the current directory.
execveuses NULL to know where the argument list ends.
Real-world applications
- Any CLI launcher or shell.
- Build systems and process managers.
- Scripting environments that run external tools.
Where you’ll apply it
- In this project: see §3.2 Functional Requirements and §4.4 Data Structures.
- Also used in: P02 Shell Lexer/Tokenizer, P07 Environment Variable Manager, P10 Globbing Engine, P13 Tab Completion Engine
References
- POSIX Shell Command Language (command search).
- “Advanced Programming in the UNIX Environment” (exec family).
Key insights Correct argv construction and PATH lookup prevent confusing “command not found” failures.
Summary Tokenization and PATH search are the bridge from text input to executable code.
Homework/Exercises to practice the concept
- Implement a manual PATH search and print the resolved path.
- Test how empty PATH entries behave with
.directories. - Create a command with a slash and confirm PATH is ignored.
Solutions to the homework/exercises
- Split PATH on
:and join with/and the command name. - Insert empty elements and confirm lookup uses current directory.
- Use
./programto verify direct execution is attempted.
3. Project Specification
3.1 What You Will Build
A tiny interactive loop that reads a line, parses whitespace-separated argv, launches the program, and prints a new prompt.
Included:
- Core feature set described above
- Deterministic CLI behavior and exit codes
Excluded:
- No advanced parsing, no pipelines, no redirections, no job control.
3.2 Functional Requirements
- Requirement 1: Read a line from stdin and ignore empty input.
- Requirement 2: Split the line into argv tokens and build a NULL-terminated argv array.
- Requirement 3: Execute external commands using PATH lookup (
execvp). - Requirement 4: Report errors for command-not-found and permission denied.
- Requirement 5: Return to the prompt and set
$?based on child exit status.
3.3 Non-Functional Requirements
- Performance: Interactive latency under 50ms for typical inputs; pipeline setup should scale linearly.
- Reliability: No crashes on malformed input; errors reported clearly with non-zero status.
- Usability: Clear prompts, deterministic behavior, and predictable error messages.
3.4 Example Usage / Output
You will have a tiny shell that can run external programs, show error messages for missing commands, and return to the prompt.
Command Line Outcome Example:
$ ./mysh
mysh> /bin/echo hello world
hello world
mysh> /bin/ls -la
-rw-r--r-- 1 user staff 123 Jan 1 10:00 main.c
mysh> /bin/false
mysh> echo $?
1
mysh> not_a_command
mysh: not_a_command: command not found
mysh> exit
3.5 Data Formats / Schemas / Protocols
- Command line is whitespace-separated tokens; no quoting support in this project.
3.6 Edge Cases
- Empty input line
- Command not found
- Executable without permission
- Large argv list
3.7 Real World Outcome
This is the exact behavior you should be able to demonstrate.
3.7.1 How to Run (Copy/Paste)
- make
- ./mysh
3.7.2 Golden Path Demo (Deterministic)
$ ./mysh
mysh> /bin/echo hello world
hello world
mysh> /bin/ls -la
-rw-r--r-- 1 user staff 123 Jan 1 10:00 main.c
mysh> /bin/false
mysh> echo $?
1
mysh> not_a_command
mysh: not_a_command: command not found
mysh> exit
3.7.3 Failure Demo (Deterministic)
$ ./mysh
mysh> not_a_command
mysh> echo $?
127
4. Solution Architecture
4.1 High-Level Design
[Input] -> [Parser/Lexer] -> [Core Engine] -> [Executor/Output]
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| REPL | Read input and display prompt | Keep it minimal and synchronous. |
| Tokenizer | Split on whitespace into argv | Simple split to focus on process model. |
| Executor | fork/exec/wait and status update | Use execvp for PATH lookup. |
4.4 Data Structures (No Full Code)
struct Command { char **argv; int argc; };
4.4 Algorithm Overview
Key Algorithm: Whitespace Tokenization
- Scan chars, split on spaces
- Build argv array
- Null-terminate
Complexity Analysis:
- Time: O(n) time
- Space: O(n) space
5. Implementation Guide
5.1 Development Environment Setup
# install dependencies (if any)
# build
make
5.2 Project Structure
project-root/
├── src/
│ ├── main.c
│ ├── lexer.c
│ └── executor.c
├── tests/
│ └── test_basic.sh
├── Makefile
└── README.md
5.3 The Core Question You’re Answering
How does a shell create and manage a process without becoming the process itself?
5.4 Concepts You Must Understand First
Stop and research these before coding:
- Process creation (
fork) - Program execution (
execve) - Exit status
5.5 Questions to Guide Your Design
5.6 Thinking Exercise
The “Two Copies” Problem
Trace this code in your head and write the output order:
printf("A\n");
pid_t p = fork();
if (p == 0) printf("B\n");
else printf("C\n");
printf("D\n");
Questions while thinking:
- Which lines execute in child vs parent?
- Why is the output order nondeterministic?
- What happens if the parent exits early?
5.7 The Interview Questions They’ll Ask
5.8 Hints in Layers
Hint 1: Start with a loop
while (true) { printf("mysh> "); if (!fgets(buf, n, stdin)) break; }
Hint 2: Parse tokens with strtok
argv[i++] = strtok(buf, " \t\n");
Hint 3: The fork/exec dance
if (fork() == 0) { execvp(argv[0], argv); perror("exec"); _exit(127); }
Hint 4: Wait and capture status
# use waitpid and WEXITSTATUS
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Process creation | “Advanced Programming in the UNIX Environment” | Ch. 8 |
| Exec and environment | “The Linux Programming Interface” | Ch. 27 |
| Process lifecycle | “Operating Systems: Three Easy Pieces” | Ch. 5 |
5.10 Implementation Phases
Phase 1: Foundation (2-3 days)
Goals:
- Define data structures and interfaces
- Build a minimal end-to-end demo
Tasks:
- Implement the core data structures
- Build a tiny CLI or harness for manual tests
Checkpoint: A demo command runs end-to-end with clear logging.
Phase 2: Core Functionality (1 week)
Goals:
- Implement full feature set
- Validate with unit tests
Tasks:
- Implement core requirements
- Add error handling and edge cases
Checkpoint: All functional requirements pass basic tests.
Phase 3: Polish & Edge Cases (2-4 days)
Goals:
- Harden for weird inputs
- Improve UX and documentation
Tasks:
- Add edge-case tests
- Document design decisions
Checkpoint: Deterministic golden demo and clean error output.
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Parsing depth | Minimal vs full | Incremental | Start small, expand safely |
| Error policy | Silent vs verbose | Verbose | Debuggability for learners |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | Test individual components | Tokenizer, matcher, env builder |
| Integration Tests | Test component interactions | Full command lines |
| Edge Case Tests | Handle boundary conditions | Empty input, bad args |
6.2 Critical Test Cases
- Golden Path: Run the canonical demo and verify output.
- Failure Path: Provide invalid input and confirm error status.
- Stress Path: Run repeated commands to detect leaks or state corruption.
6.3 Test Data
input: echo hello
output: hello
7. Common Pitfalls & Debugging
7.1 Frequent Mistakes
| Pitfall | Symptom | Solution |
|---|---|---|
| Misordered redirection | Output goes to wrong place | Apply redirections left-to-right |
| Leaked file descriptors | Commands hang waiting for EOF | Close unused fds in parent/child |
| Incorrect exit status | &&/|| behave wrong |
Use waitpid macros correctly |
7.2 Debugging Strategies
- Trace syscalls: Use
strace/dtrussto verify fork/exec/dup2 order. - Log state transitions: Print parser states and job table changes in debug mode.
- Compare with
dash: Run the same input in a reference shell.
7.3 Performance Traps
- Avoid O(n^2) behavior in hot paths like line editing.
- Minimize allocations inside the REPL loop.
8. Extensions & Challenges
8.1 Beginner Extensions
- Add a
helpbuilt-in with usage docs. - Add colored prompt themes.
8.2 Intermediate Extensions
- Add a simple profiling mode for command timing.
- Implement a
whichbuilt-in using PATH lookup.
8.3 Advanced Extensions
- Add programmable completion or plugin system.
- Add a scriptable test harness with golden outputs.
9. Real-World Connections
9.1 Industry Applications
- Build systems: shells orchestrate compilation and test pipelines.
- DevOps automation: scripts manage deployments and infrastructure.
9.2 Related Open Source Projects
- bash: The most common interactive shell.
- dash: Minimal POSIX shell often used as /bin/sh.
- zsh: Feature-rich interactive shell.
9.3 Interview Relevance
- Process creation and lifecycle questions.
- Parsing and system programming design trade-offs.
10. Resources
10.1 Essential Reading
- “Advanced Programming in the UNIX Environment” by W. Richard Stevens - focus on the chapters relevant to this project.
- “Advanced Programming in the UNIX Environment” - process control and pipes.
10.2 Video Resources
- Unix process model lectures (any OS course).
- Compiler front-end videos for lexing/parsing projects.
10.3 Tools & Documentation
- strace/dtruss: inspect syscalls.
- man pages:
fork,execve,waitpid,pipe,dup2.
10.4 Related Projects in This Series
11. Self-Assessment Checklist
11.1 Understanding
- I can explain the core concept without notes.
- I can trace a command through my subsystem.
- I understand at least one key design trade-off.
11.2 Implementation
- All functional requirements are met.
- All critical tests pass.
- Edge cases are handled cleanly.
11.3 Growth
- I documented lessons learned.
- I can explain this project in an interview.
12. Submission / Completion Criteria
Minimum Viable Completion:
- Core feature works for the golden demo.
- Errors are handled with non-zero status.
- Code is readable and buildable.
Full Completion:
- All functional requirements met.
- Tests cover edge cases and failures.
Excellence (Going Above & Beyond):
- Performance benchmarks and clear documentation.
- Behavior compared against a reference shell.