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.
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
- Interactive prompt:
msh>. - Simple commands:
ls -la /tmp— fork, exec, wait. - Single pipe:
ls | grep foo— two children connected by a pipe. - Exit: the built-in
exitcommand. - 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 bothStarter 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
./mshTest checklist
msh> ls
msh> ls -la /tmp
msh> ls | grep msh
msh> echo hello | cat
msh> cd /tmp
msh> pwdPress Ctrl-C while sleep 10 is running — the shell should survive and print a new prompt.
Type Ctrl-D to exit.
Extension challenges
-
Multiple pipes:
ls | grep .c | wc -l. This requires building a pipeline of N processes with N-1 pipes. Generaliserun_pipelineto accept an array of command arrays. -
Background processes:
sleep 10 &— fork but do not wait immediately. Track background PIDs and reap them withSIGCHLD. -
I/O redirection:
ls > out.txtandcat < input.txt. Useopen()+dup2in the child beforeexec. -
Command history: use a circular buffer to remember the last N commands, accessible with Up/Down keys (requires
readlineor raw terminal mode).
What you practised
fork/exec/waitpidas the process execution modelpipe()+dup2()to connect process I/Osigactionfor reliable signal handling without killing the shellstrtokfor 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.