Kelick Tech Hub

Build, learn & keep up — tech, Linux & sports news included.

C++ Mini Shell Project

Intermediate•3–4 days•processes, parsing, POSIX

Your own tiny command shell: run programs, chain them with pipes.

Share on X

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

Feature checklist

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 waits

Step 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

Compile and run

g++ -std=c++17 -o minishell minishell.cpp

Linux/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
bye

Try it in your browser

No browser demo is included for this project. Build the C++ version below — that is the real program.

Related Intermediate projects

← All 60 projects · Classic site (Learn & Sandbox)

Download Fake GPS