C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
220: Implementing a Simple Shell
When you first decide to build a shell, the temptation is to get a prompt on the screen and make it "work" as quickly as possible. You want to type ls -la and see your files. If you look through the standard library, you'll find a function called system() that seems like a miracle. It takes a string, hands it to the OS, and runs it. It's seductive because it allows you to write a "shell" in about ten lines of code.
The shortcut that isn't actually a solution
In a naive implementation, you might write a loop that reads a line of input using fgets() and then passes that string directly into system(). It looks like this:
while (1) {
printf("my_shell> ");
fgets(input, 1024, stdin);
system(input);
}
Here is the problem: system() is a wrapper. Under the hood, it literally calls /bin/sh -c "your_command". If you use system(), you haven't actually built a shell; you've built a very thin, redundant wrapper around the existing system shell. You're not managing processes; you're just asking another program to do it for you.
Beyond the "cheating" aspect, this approach is a security disaster. Because system() invokes a shell, it's prone to shell injection. If your program ever took input from an untrusted source, a user could pass ls; rm -rf / and your program would happily execute both. I've seen juniors do this in internal tools, and it usually ends with a very stressful afternoon for the DevOps team.
Taking the steering wheel with fork and exec
To build a real shell, you need to understand how the Unix process model actually works. You don't just "run" a program; you create a copy of your current process and then transform that copy into the program you want. This is the fork() and exec() pattern.
Instead of system(), you first call fork(). This creates a child process that is an exact duplicate of your shell. At this point, you have two identical programs running the same code. You then use the return value of fork() to tell them apart: the parent continues to wait for the user, while the child calls execvp().
pid_t pid = fork();
if (pid == 0) {
// I am the child. I will replace myself with the new program.
if (execvp(args[0], args) == -1) {
perror("Execution failed");
}
exit(EXIT_FAILURE);
} else if (pid < 0) {
perror("Fork failed");
} else {
// I am the parent. I need to wait for the child to finish.
int status;
waitpid(pid, &status, 0);
}
The execvp() function is the magic here. It doesn't "start" a new program in the way we think about it; it replaces the entire memory image of the child process with the binary of the command you're calling (like /bin/ls). The child process ceases to be your shell and becomes the ls program.
Why the extra boilerplate is worth the headache
You might look at the fork/exec/wait dance and think it's overkill. Why write twenty lines of code when system() does it in one? The answer is control. When you manage the process yourself, you gain abilities that system() hides from you.
- Signal Handling: You can now decide what happens when the user hits Ctrl+C. Should the shell exit, or should only the running child process be killed? With
fork(), you can send signals specifically to the child's PID. - Input/Output Redirection: If you want to implement
ls > files.txt, you can calldup2()in the child process after the fork but before the exec. This swaps the child's standard output for a file descriptor. You can't do that withsystem()without manually concatenating strings and hoping for the best. - Environment Control: You can use
execvpe()to pass a custom environment to the child process without altering the environment of your main shell.
The trade-off is that you are now responsible for "zombie" processes. If you fork() but forget to waitpid(), the child process remains in the system table after it finishes, wasting resources. It's a small price to pay for actually understanding how your OS handles execution.
📋 Practical Task
Exercise: Implementing a Directory-Aware Shell with Built-in Commands
One critical realization when building a shell is that some commands cannot be run via fork() and exec(). Specifically, the cd command. Since a child process cannot change the working directory of its parent, calling execvp("cd", ...) will change the directory for the child, which then immediately exits, leaving your shell exactly where it started.
Your task is to expand the shell logic discussed in the lesson. Create a program that:
- Implements a standard
fork/exec/waitloop for general commands (likels,ps, ordate). - Intercepts the
cdcommand specifically. If the user typescd [path], the shell should call the C library functionchdir()directly in the parent process instead of forking. - Handles the
exitcommand to terminate the shell loop gracefully. - Properly parses the input string into an array of arguments (
char *args[]) so thatexecvpcan receive them correctly.
There are no comments for now.