Skip to main content

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

>, >>, < operators
4

Pipes

Command pipelines with |
5

Job Control

Background processes, Ctrl+C handling

Phase 1: Basic Shell Loop

// 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
// 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

// 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

// 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

// 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

// 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

// 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

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

# 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?.txt

Environment Variables

Support $VAR expansion and export

Command Substitution

Implement $(command) or backtick substitution

Tab Completion

Add readline-style tab completion

Scripting

Execute shell scripts with conditionals and loops

Job Control

Full jobs, fg, bg implementation

Next Up

Build a Database

Build a key-value store with persistence