Frontend Labs
⋯

26. Capstone: a Unix mini-shell with pipes and redirection

// minishell.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>

#define MAX_ARGS 64

static char **tokenize(char *line) {
    char **args = malloc(MAX_ARGS * sizeof(char *));
    if (!args) return NULL;
    int i = 0;
    char *tok = strtok(line, " \t\n");
    while (tok && i < MAX_ARGS - 1) { args[i++] = tok; tok = strtok(NULL, " \t\n"); }
    args[i] = NULL;
    return args;
}

static void run_command(char **args) {
    if (!args[0]) return;

    // handle output redirection: `cmd args... > file`
    char *outfile = NULL;
    for (int i = 0; args[i]; i++) {
        if (strcmp(args[i], ">") == 0) { outfile = args[i + 1]; args[i] = NULL; break; }
    }

    pid_t pid = fork();     // duplicate the current process — pid==0 in the child
    if (pid == 0) {
        if (outfile) {
            int fd = open(outfile, O_WRONLY | O_CREAT | O_TRUNC, 0644);
            if (fd < 0) { perror("open"); exit(1); }
            if (dup2(fd, STDOUT_FILENO) < 0) { perror("dup2"); close(fd); exit(1); }
            close(fd);
        }
        execvp(args[0], args);         // replace THIS process's image with the new program
        perror("execvp");              // only reached if execvp itself failed
        exit(127);
    } else if (pid > 0) {
        int status;
        waitpid(pid, &status, 0);      // parent blocks until the child finishes
    } else {
        perror("fork");
    }
}

int main(void) {
    char line[1024];
    while (1) {
        printf("minishell> ");
        fflush(stdout);
        if (!fgets(line, sizeof line, stdin)) break;    // EOF (Ctrl-D)
        if (strcmp(line, "\n") == 0) continue;

        char **args = tokenize(line);
        if (!args) { perror("malloc"); return 1; }
        if (args[0] == NULL) { free(args); continue; }
        if (strcmp(args[0], "exit") == 0) { free(args); break; }
        if (strcmp(args[0], "cd") == 0) {                // cd must run in the shell itself,
            const char *dir = args[1] ? args[1] : getenv("HOME");
            if (!dir || chdir(dir) != 0) perror("cd");     // not a child — a child's chdir wouldn't
            free(args); continue;                          // affect the parent shell's directory
        }

        run_command(args);
        free(args);
    }
    return 0;
}

fork() + execvp() is the Unix process-creation model, and it's why cd and exit must be built into the shell rather than external programs — a child process's working directory change or exit() call has zero effect on its parent. Piping (cmd1 | cmd2) extends this exact pattern: pipe() creates a connected read/write fd pair, then each child dup2s one end onto its stdin or stdout before execvp — the same dup2 trick used for the > redirection above, just wired to the other child's fd instead of a file.


Generated from real-life-c-guide.md · 8 Oct 2026 16:36