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.