C++ Mini Shell Project
Your own tiny command shell: run programs, chain them with pipes.
What you will build
Write a miniature version of bash: a prompt, commands you type actually run, cd works, and eventually pipes connect programs together. Nothing teaches how an operating system launches programs like reimplementing the launcher — fork, exec, wait.
What you will learn
- fork(), execvp(), and waitpid()
- Tokenising a command line
- Built-ins (cd, exit) vs external programs
- Redirecting input/output with dup2
Feature checklist
- Prompt that runs any system command
- Built-in cd, pwd, exit
- Output redirection with > and input with <
- One pipe: cmd1 | cmd2
- Shows exit status of the last command
- Handles Ctrl+C without dying (advanced)
Starter code
// POSIX shell core (Linux/macOS)
#include <iostream>
#include <sstream>
#include <vector>
#include <string>
#include <unistd.h>
#include <sys/wait.h>
std::vector<char*> splitArgs(const std::string& line,
std::vector<std::string>& store) {
std::istringstream ss(line);
std::string tok;
while (ss >> tok) store.push_back(tok);
std::vector<char*> argv;
for (auto& s : store) argv.push_back(s.data());
argv.push_back(nullptr);
return argv;
}
int main() {
std::string line;
while (true) {
std::cout << "mini$ " << std::flush;
if (!std::getline(std::cin, line)) break;
if (line == "exit") break;
std::vector<std::string> store;
auto argv = splitArgs(line, store);
if (argv[0] == nullptr) continue;
pid_t pid = fork();
if (pid == 0) { // child: become the command
execvp(argv[0], argv.data());
std::perror("execvp"); std::exit(1);
}
int status; waitpid(pid, &status, 0); // parent: wait for it
}
}Full walkthrough
Step 1: The REPL and splitArgs()
A shell is a loop: print its prompt, 'mysh$ ', then std::getline a line, split it into tokens with splitArgs() — an std::istringstream and operator>> produce a std::vector<std::string> on whitespace. An empty line just loops again. Everything else in this project hangs off this loop.
Step 2: Run external commands with fork + execvp + waitpid
For a non-built-in command: fork(). The CHILD converts the tokens to a char* argv array (NULL-terminated — exec demands it) and calls execvp(argv[0], argv), which searches PATH. The PARENT waitpid()s and records the exit status. If execvp returns at all, it failed: print 'command not found' from the child and _exit(1).
pid_t pid = fork();
if (pid == 0) {
// child: build argv, maybe redirect, then
execvp(argv[0], argv.data());
std::perror("exec"); _exit(1);
}
int status; waitpid(pid, &status, 0); // parent waitsStep 3: Built-ins must NOT fork
cd, pwd, and exit run inside the shell process itself. cd MUST: a child changing its own directory changes nothing for the parent shell, so 'cd' via fork appears to do nothing — the bug that teaches you why built-ins exist. exit just breaks the REPL; pwd prints std::filesystem::current_path() or getcwd().
Step 4: Add output redirection with >
Scan the tokens for '>' (and '<'), remove them from argv, and in the CHILD — after fork, before exec — open the target file and dup2() it onto STDOUT_FILENO. Redirection is per-command, so it belongs to the child's setup; dup2 in the parent would redirect the shell itself.
Step 5: Add one pipe: cmd1 | cmd2
Split the tokens at '|'. Create pipe(fds), fork twice: child 1 dup2s the write end onto its stdout, child 2 dup2s the read end onto its stdin, each execs its half, and the parent closes BOTH ends and waits for both. A parent that forgets to close its copies leaves the pipe open and the reader never sees EOF — the classic hang.
Step 6: Report the last exit status
Decode waitpid's status with WIFEXITED / WEXITSTATUS and print it (or show it in the prompt). $? is how scripts decide success; a shell that hides exit codes cannot be scripted or trusted.
Step 7: (Advanced) Survive Ctrl+C
Install a SIGINT handler in the shell that does NOT exit — real shells shrug off Ctrl+C themselves while the foreground child takes the hit. This requires the child to be in its own process group (setpgid) — attempt it only after everything above works, exactly as the checklist labels it: advanced.
Common mistakes
- Running cd through fork/exec and concluding 'cd does nothing' — built-ins exist precisely because some commands must change the shell itself.
- Forgetting the NULL terminator on the argv array passed to execvp, producing random crashes.
- Leaving the parent's pipe ends open, so the second command never sees end-of-input and the shell hangs forever.
Compile and run
g++ -std=c++17 -o minishell minishell.cppLinux/macOS only — this project is built on POSIX fork/exec, which Windows does not have (use WSL on Windows). No extra libraries needed.
Sample run
$ ./minishell
mysh$ — a tiny shell (prompt: mysh$)
> pwd
/home/you/projects
> echo hello from my own shell
hello from my own shell
> echo saved to a file > greeting.txt
(exit status 0)
> cat greeting.txt
saved to a file
> ls | wc -l
14
> frobnicate
frobnicate: command not found (exit status 1)
> cd /tmp
> pwd
/tmp
> exit
byeTry it in your browser
No browser demo is included for this project. Build the C++ version below — that is the real program.