Project: Build a Unix Shell
Put your C skills to the test by building a functional Unix shell. This project covers process management, signal handling, pipes, and I/O redirection.Project Overview
We’ll build a shell with these features:1
Basic Command Execution
Parse and execute simple commands
2
Built-in Commands
cd, exit, pwd, history, etc.
3
I/O Redirection
>, >>, < operators4
Pipes
Command pipelines with
|5
Job Control
Background processes, Ctrl+C handling
Phase 1: Basic Shell Loop
Copy
// shell.h
#ifndef SHELL_H
#define SHELL_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <errno.h>
#include <signal.h>
#include <fcntl.h>
#define MAX_LINE 1024
#define MAX_ARGS 64
#define MAX_HISTORY 100
// Command structure
typedef struct {
char **args; // Argument array
int argc; // Argument count
char *input_file; // < redirection
char *output_file; // > redirection
int append; // >> instead of >
int background; // & at end
} Command;
// Function prototypes
void shell_loop(void);
char *read_line(void);
Command *parse_line(char *line);
int execute(Command *cmd);
void free_command(Command *cmd);
// Built-in commands
int builtin_cd(char **args);
int builtin_exit(char **args);
int builtin_pwd(char **args);
int builtin_history(char **args);
int builtin_help(char **args);
#endif
Copy
// main.c
#include "shell.h"
int main(int argc, char *argv[]) {
// Setup signal handling
signal(SIGINT, SIG_IGN); // Ignore Ctrl+C in parent
signal(SIGQUIT, SIG_IGN);
signal(SIGTSTP, SIG_IGN); // Ignore Ctrl+Z
printf("Welcome to MyShell!\n");
printf("Type 'help' for available commands.\n\n");
shell_loop();
return 0;
}
void shell_loop(void) {
char *line;
Command *cmd;
int status = 1;
while (status) {
// Print prompt
char cwd[1024];
getcwd(cwd, sizeof(cwd));
printf("\033[1;32mmysh\033[0m:\033[1;34m%s\033[0m$ ", cwd);
fflush(stdout);
// Read input
line = read_line();
if (line == NULL) {
printf("\n");
break; // EOF
}
// Skip empty lines
if (strlen(line) == 0) {
free(line);
continue;
}
// Parse and execute
cmd = parse_line(line);
if (cmd && cmd->argc > 0) {
status = execute(cmd);
}
free_command(cmd);
free(line);
}
}
char *read_line(void) {
char *line = NULL;
size_t bufsize = 0;
if (getline(&line, &bufsize, stdin) == -1) {
free(line);
return NULL;
}
// Remove trailing newline
size_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n') {
line[len - 1] = '\0';
}
return line;
}
Phase 2: Command Parsing
Copy
// parser.c
#include "shell.h"
Command *parse_line(char *line) {
Command *cmd = calloc(1, sizeof(Command));
cmd->args = malloc(MAX_ARGS * sizeof(char*));
char *token;
char *saveptr;
int i = 0;
// Simple tokenization (doesn't handle quotes yet)
token = strtok_r(line, " \t", &saveptr);
while (token != NULL && i < MAX_ARGS - 1) {
// Handle I/O redirection
if (strcmp(token, "<") == 0) {
token = strtok_r(NULL, " \t", &saveptr);
if (token) cmd->input_file = strdup(token);
}
else if (strcmp(token, ">") == 0) {
token = strtok_r(NULL, " \t", &saveptr);
if (token) {
cmd->output_file = strdup(token);
cmd->append = 0;
}
}
else if (strcmp(token, ">>") == 0) {
token = strtok_r(NULL, " \t", &saveptr);
if (token) {
cmd->output_file = strdup(token);
cmd->append = 1;
}
}
else if (strcmp(token, "&") == 0) {
cmd->background = 1;
}
else {
cmd->args[i++] = strdup(token);
}
token = strtok_r(NULL, " \t", &saveptr);
}
cmd->args[i] = NULL;
cmd->argc = i;
return cmd;
}
void free_command(Command *cmd) {
if (cmd == NULL) return;
for (int i = 0; cmd->args[i] != NULL; i++) {
free(cmd->args[i]);
}
free(cmd->args);
free(cmd->input_file);
free(cmd->output_file);
free(cmd);
}
Phase 3: Built-in Commands
Copy
// builtins.c
#include "shell.h"
// Built-in command names and functions
char *builtin_names[] = {
"cd",
"exit",
"pwd",
"history",
"help",
NULL
};
int (*builtin_funcs[])(char**) = {
builtin_cd,
builtin_exit,
builtin_pwd,
builtin_history,
builtin_help,
NULL
};
// History storage
static char *history[MAX_HISTORY];
static int history_count = 0;
void add_to_history(const char *line) {
if (history_count < MAX_HISTORY) {
history[history_count++] = strdup(line);
} else {
free(history[0]);
memmove(history, history + 1, (MAX_HISTORY - 1) * sizeof(char*));
history[MAX_HISTORY - 1] = strdup(line);
}
}
int is_builtin(const char *cmd) {
for (int i = 0; builtin_names[i] != NULL; i++) {
if (strcmp(cmd, builtin_names[i]) == 0) {
return i;
}
}
return -1;
}
int builtin_cd(char **args) {
const char *path = args[1];
if (path == NULL) {
path = getenv("HOME");
if (path == NULL) {
fprintf(stderr, "cd: HOME not set\n");
return 1;
}
}
if (chdir(path) != 0) {
perror("cd");
return 1;
}
return 1;
}
int builtin_exit(char **args) {
int code = 0;
if (args[1] != NULL) {
code = atoi(args[1]);
}
printf("Goodbye!\n");
exit(code);
}
int builtin_pwd(char **args) {
(void)args;
char cwd[1024];
if (getcwd(cwd, sizeof(cwd)) != NULL) {
printf("%s\n", cwd);
} else {
perror("pwd");
}
return 1;
}
int builtin_history(char **args) {
(void)args;
for (int i = 0; i < history_count; i++) {
printf("%4d %s\n", i + 1, history[i]);
}
return 1;
}
int builtin_help(char **args) {
(void)args;
printf("MyShell - A simple Unix shell\n\n");
printf("Built-in commands:\n");
printf(" cd [dir] Change directory\n");
printf(" pwd Print working directory\n");
printf(" history Show command history\n");
printf(" help Show this help\n");
printf(" exit [code] Exit shell\n");
printf("\nFeatures:\n");
printf(" cmd > file Redirect stdout to file\n");
printf(" cmd >> file Append stdout to file\n");
printf(" cmd < file Redirect stdin from file\n");
printf(" cmd1 | cmd2 Pipe output\n");
printf(" cmd & Run in background\n");
return 1;
}
Phase 4: External Command Execution
Copy
// execute.c
#include "shell.h"
int execute(Command *cmd) {
// Check for empty command
if (cmd->argc == 0 || cmd->args[0] == NULL) {
return 1;
}
// Check for built-in commands
int builtin_idx = is_builtin(cmd->args[0]);
if (builtin_idx >= 0) {
return builtin_funcs[builtin_idx](cmd->args);
}
// Fork and execute external command
pid_t pid = fork();
if (pid < 0) {
perror("fork");
return 1;
}
if (pid == 0) {
// Child process
// Reset signal handlers
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
signal(SIGTSTP, SIG_DFL);
// Handle input redirection
if (cmd->input_file) {
int fd = open(cmd->input_file, O_RDONLY);
if (fd < 0) {
perror(cmd->input_file);
exit(1);
}
dup2(fd, STDIN_FILENO);
close(fd);
}
// Handle output redirection
if (cmd->output_file) {
int flags = O_WRONLY | O_CREAT;
flags |= cmd->append ? O_APPEND : O_TRUNC;
int fd = open(cmd->output_file, flags, 0644);
if (fd < 0) {
perror(cmd->output_file);
exit(1);
}
dup2(fd, STDOUT_FILENO);
close(fd);
}
// Execute command
execvp(cmd->args[0], cmd->args);
// If we get here, exec failed
fprintf(stderr, "%s: command not found\n", cmd->args[0]);
exit(127);
}
// Parent process
if (!cmd->background) {
int status;
waitpid(pid, &status, 0);
} else {
printf("[%d] Started in background\n", pid);
}
return 1;
}
Phase 5: Pipes
Copy
// pipes.c
#include "shell.h"
// Parse pipe-separated commands
// Returns array of Command pointers, NULL-terminated
Command **parse_pipeline(char *line) {
Command **pipeline = malloc(MAX_ARGS * sizeof(Command*));
int count = 0;
char *segment;
char *saveptr;
segment = strtok_r(line, "|", &saveptr);
while (segment != NULL && count < MAX_ARGS - 1) {
// Trim whitespace
while (*segment == ' ') segment++;
char *end = segment + strlen(segment) - 1;
while (end > segment && *end == ' ') *end-- = '\0';
pipeline[count++] = parse_line(segment);
segment = strtok_r(NULL, "|", &saveptr);
}
pipeline[count] = NULL;
return pipeline;
}
int execute_pipeline(Command **pipeline) {
int num_cmds = 0;
while (pipeline[num_cmds] != NULL) num_cmds++;
if (num_cmds == 0) return 1;
if (num_cmds == 1) return execute(pipeline[0]);
int pipes[num_cmds - 1][2];
pid_t pids[num_cmds];
// Create pipes
for (int i = 0; i < num_cmds - 1; i++) {
if (pipe(pipes[i]) < 0) {
perror("pipe");
return 1;
}
}
// Fork processes
for (int i = 0; i < num_cmds; i++) {
pids[i] = fork();
if (pids[i] < 0) {
perror("fork");
return 1;
}
if (pids[i] == 0) {
// Child process
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
// Connect to previous pipe (stdin)
if (i > 0) {
dup2(pipes[i - 1][0], STDIN_FILENO);
}
// Connect to next pipe (stdout)
if (i < num_cmds - 1) {
dup2(pipes[i][1], STDOUT_FILENO);
}
// Close all pipe fds
for (int j = 0; j < num_cmds - 1; j++) {
close(pipes[j][0]);
close(pipes[j][1]);
}
// Handle first command's input redirection
if (i == 0 && pipeline[i]->input_file) {
int fd = open(pipeline[i]->input_file, O_RDONLY);
if (fd >= 0) {
dup2(fd, STDIN_FILENO);
close(fd);
}
}
// Handle last command's output redirection
if (i == num_cmds - 1 && pipeline[i]->output_file) {
int flags = O_WRONLY | O_CREAT;
flags |= pipeline[i]->append ? O_APPEND : O_TRUNC;
int fd = open(pipeline[i]->output_file, flags, 0644);
if (fd >= 0) {
dup2(fd, STDOUT_FILENO);
close(fd);
}
}
execvp(pipeline[i]->args[0], pipeline[i]->args);
fprintf(stderr, "%s: command not found\n", pipeline[i]->args[0]);
exit(127);
}
}
// Parent: close all pipe fds
for (int i = 0; i < num_cmds - 1; i++) {
close(pipes[i][0]);
close(pipes[i][1]);
}
// Wait for all children
for (int i = 0; i < num_cmds; i++) {
waitpid(pids[i], NULL, 0);
}
return 1;
}
Phase 6: Signal Handling and Job Control
Copy
// signals.c
#include "shell.h"
// Global for signal handler
static volatile pid_t foreground_pid = 0;
void sigint_handler(int sig) {
(void)sig;
if (foreground_pid > 0) {
kill(foreground_pid, SIGINT);
}
printf("\n");
}
void sigchld_handler(int sig) {
(void)sig;
int status;
pid_t pid;
while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
if (WIFEXITED(status)) {
printf("\n[%d] Done (exit %d)\n", pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("\n[%d] Killed by signal %d\n", pid, WTERMSIG(status));
}
}
}
void setup_signals(void) {
struct sigaction sa;
// Handle SIGINT (Ctrl+C)
sa.sa_handler = sigint_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa, NULL);
// Handle SIGCHLD (child terminated)
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
// Ignore SIGQUIT and SIGTSTP in shell
signal(SIGQUIT, SIG_IGN);
signal(SIGTSTP, SIG_IGN);
}
void set_foreground_pid(pid_t pid) {
foreground_pid = pid;
}
Makefile
Copy
CC = gcc
CFLAGS = -Wall -Wextra -Werror -std=c11 -g
LDFLAGS =
SRCS = main.c parser.c builtins.c execute.c pipes.c signals.c
OBJS = $(SRCS:.c=.o)
TARGET = mysh
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(OBJS) $(LDFLAGS) -o $@
%.o: %.c shell.h
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: all clean
Testing Your Shell
Copy
# Basic commands
ls -la
pwd
cd /tmp
pwd
cd
# I/O redirection
echo "Hello World" > test.txt
cat < test.txt
echo "More text" >> test.txt
wc -l < test.txt
# Pipes
ls -la | grep ".c"
cat /etc/passwd | grep root | wc -l
ps aux | grep bash | head -5
# Background processes
sleep 10 &
jobs
# Built-in commands
history
help
exit 0
Extensions to Implement
Globbing
Expand wildcards like
*.c and file?.txtEnvironment Variables
Support
$VAR expansion and exportCommand Substitution
Implement
$(command) or backtick substitutionTab Completion
Add readline-style tab completion
Scripting
Execute shell scripts with conditionals and loops
Job Control
Full
jobs, fg, bg implementationNext Up
Build a Database
Build a key-value store with persistence