Code of the Day
AdvancedSystems programming patterns

Challenge: Build a minimal shell

Implement a minimal interactive shell in C that parses commands, forks child processes, handles pipes, and manages signals — integrating the full advanced C curriculum.

Challenge · optionalCAdvanced120 min
Recommended first

A minimal shell is the ultimate C systems programming exercise. It ties together the entire advanced tier: signals (handling Ctrl-C), fork/exec/waitpid (running commands), pipes (connecting commands), string parsing (tokenising input), and process management (foreground/background processes).

What the mini-shell must support

  1. Interactive prompt: msh> .
  2. Simple commands: ls -la /tmp — fork, exec, wait.
  3. Single pipe: ls | grep foo — two children connected by a pipe.
  4. Exit: the built-in exit command.
  5. Signal handling: Ctrl-C kills the foreground child without killing the shell.

Architecture

Read a line
  → Tokenise (split on spaces)
  → Check for pipe (|)
  → If no pipe: fork + exec + wait
  → If pipe:    fork two children, connect with pipe(), exec both

Starter code

/* msh.c -- minimal shell */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <errno.h>

#define MAX_ARGS 64
#define MAX_LINE 1024

static pid_t foreground_pid = -1;

static void sigint_handler(int sig) {
    (void)sig;
    if (foreground_pid > 0) {
        kill(foreground_pid, SIGINT);
    }
}

/* Tokenise line into argv[]. Returns number of arguments. */
int parse_args(char *line, char *argv[], int max_args) {
    int argc = 0;
    char *tok = strtok(line, " \t\n");
    while (tok && argc < max_args - 1) {
        argv[argc++] = tok;
        tok = strtok(NULL, " \t\n");
    }
    argv[argc] = NULL;
    return argc;
}

/* Find pipe position in argv. Returns index of "|" or -1. */
int find_pipe(char *argv[], int argc) {
    for (int i = 0; i < argc; i++) {
        if (strcmp(argv[i], "|") == 0) { return i; }
    }
    return -1;
}

void run_simple(char *argv[]) {
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); return; }
    if (pid == 0) {
        /* Child */
        execvp(argv[0], argv);
        fprintf(stderr, "msh: command not found: %s\n", argv[0]);
        _exit(127);
    }
    /* Parent */
    foreground_pid = pid;
    int status;
    waitpid(pid, &status, 0);
    foreground_pid = -1;
}

void run_pipeline(char *argv1[], char *argv2[]) {
    int pipefd[2];
    if (pipe(pipefd) < 0) { perror("pipe"); return; }

    pid_t left = fork();
    if (left == 0) {
        close(pipefd[0]);
        dup2(pipefd[1], STDOUT_FILENO);
        close(pipefd[1]);
        execvp(argv1[0], argv1);
        _exit(127);
    }

    pid_t right = fork();
    if (right == 0) {
        close(pipefd[1]);
        dup2(pipefd[0], STDIN_FILENO);
        close(pipefd[0]);
        execvp(argv2[0], argv2);
        _exit(127);
    }

    close(pipefd[0]);
    close(pipefd[1]);
    waitpid(left, NULL, 0);
    waitpid(right, NULL, 0);
}

int main(void) {
    /* Install SIGINT handler */
    struct sigaction sa = {0};
    sa.sa_handler = sigint_handler;
    sigaction(SIGINT, &sa, NULL);

    char line[MAX_LINE];
    char *argv[MAX_ARGS];

    while (1) {
        printf("msh> ");
        fflush(stdout);

        if (!fgets(line, sizeof(line), stdin)) {
            printf("\n");
            break; /* EOF (Ctrl-D) */
        }

        int argc = parse_args(line, argv, MAX_ARGS);
        if (argc == 0) { continue; }

        if (strcmp(argv[0], "exit") == 0) { break; }
        if (strcmp(argv[0], "cd") == 0) {
            if (argc > 1 && chdir(argv[1]) < 0) { perror("cd"); }
            continue;
        }

        int pipe_pos = find_pipe(argv, argc);
        if (pipe_pos >= 0) {
            argv[pipe_pos] = NULL;
            char **argv2 = argv + pipe_pos + 1;
            run_pipeline(argv, argv2);
        } else {
            run_simple(argv);
        }
    }

    return 0;
}

Build

gcc -Wall -Wextra -g -std=c11 -D_POSIX_C_SOURCE=200809L msh.c -o msh
./msh

Test checklist

msh> ls
msh> ls -la /tmp
msh> ls | grep msh
msh> echo hello | cat
msh> cd /tmp
msh> pwd

Press Ctrl-C while sleep 10 is running — the shell should survive and print a new prompt.

Type Ctrl-D to exit.

Extension challenges

  1. Multiple pipes: ls | grep .c | wc -l. This requires building a pipeline of N processes with N-1 pipes. Generalise run_pipeline to accept an array of command arrays.

  2. Background processes: sleep 10 & — fork but do not wait immediately. Track background PIDs and reap them with SIGCHLD.

  3. I/O redirection: ls > out.txt and cat < input.txt. Use open() + dup2 in the child before exec.

  4. Command history: use a circular buffer to remember the last N commands, accessible with Up/Down keys (requires readline or raw terminal mode).

What you practised

  • fork/exec/waitpid as the process execution model
  • pipe() + dup2() to connect process I/O
  • sigaction for reliable signal handling without killing the shell
  • strtok for in-place tokenisation
  • Built-in commands (cd, exit) vs. external commands

Completing this challenge means you can read, understand, and extend real Unix shell source code. That is the goal of the Advanced C tier.

Finished reading? Mark it complete to track your progress.

On this page