Project 11: Mini Shell

Project 11: Mini Shell

Implement a simple command shell from scratch.

Learning Objectives

  • Process creation (fork)
  • Program execution (exec family)
  • Pipes and redirection
  • Signal handling basics

Prerequisites

  • String processing
  • File I/O
  • Pointers and dynamic memory

Stage 1: Basic Shell Structure

The basic shell operation: Read → Parse → Execute → Repeat

Simplest Shell

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

#define MAX_INPUT 1024
#define MAX_ARGS 64

// Split input by whitespace
int parse_input(char* input, char** args) {
    int argc = 0;
    char* token = strtok(input, " \t\n");

    while (token != NULL && argc < MAX_ARGS - 1) {
        args[argc++] = token;
        token = strtok(NULL, " \t\n");
    }
    args[argc] = NULL;

    return argc;
}

// Execute command
void execute(char** args) {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork failed");
        return;
    }

    if (pid == 0) {
        // Child process: execute command
        execvp(args[0], args);
        // If execvp fails
        perror(args[0]);
        exit(EXIT_FAILURE);
    } else {
        // Parent process: wait for child to finish
        int status;
        waitpid(pid, &status, 0);
    }
}

int main(void) {
    char input[MAX_INPUT];
    char* args[MAX_ARGS];

    while (1) {
        // Print prompt
        printf("minish> ");
        fflush(stdout);

        // Read input
        if (fgets(input, sizeof(input), stdin) == NULL) {
            printf("\n");
            break;  // EOF (Ctrl+D)
        }

        // Ignore empty input
        if (input[0] == '\n') continue;

        // Parse
        int argc = parse_input(input, args);
        if (argc == 0) continue;

        // exit command
        if (strcmp(args[0], "exit") == 0) {
            printf("Exiting shell.\n");
            break;
        }

        // Execute
        execute(args);
    }

    return 0;
}

Compile and Test

gcc -o minish minishell_v1.c
./minish

minish> ls -l
minish> pwd
minish> echo hello world
minish> exit

Stage 2: Built-in Commands

Some commands must be handled by the shell itself, not external programs.

Implementing Built-in Commands

// builtins.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

// Built-in command names
const char* builtin_names[] = {
    "cd",
    "pwd",
    "echo",
    "exit",
    "help",
    "export",
    "env",
    NULL
};

// cd: Change directory
int builtin_cd(char** args) {
    const char* path;

    if (args[1] == NULL) {
        // No argument: go to home directory
        path = getenv("HOME");
        if (path == NULL) {
            fprintf(stderr, "cd: HOME environment variable not set\n");
            return 1;
        }
    } else if (strcmp(args[1], "-") == 0) {
        // cd -: go to previous directory
        path = getenv("OLDPWD");
        if (path == NULL) {
            fprintf(stderr, "cd: OLDPWD environment variable not set\n");
            return 1;
        }
        printf("%s\n", path);
    } else if (strcmp(args[1], "~") == 0) {
        path = getenv("HOME");
    } else {
        path = args[1];
    }

    // Save current directory
    char oldpwd[1024];
    getcwd(oldpwd, sizeof(oldpwd));

    if (chdir(path) != 0) {
        perror("cd");
        return 1;
    }

    // Update OLDPWD, PWD environment variables
    setenv("OLDPWD", oldpwd, 1);

    char newpwd[1024];
    getcwd(newpwd, sizeof(newpwd));
    setenv("PWD", newpwd, 1);

    return 0;
}

// pwd: Print current directory
int builtin_pwd(char** args) {
    (void)args;  // Unused

    char cwd[1024];
    if (getcwd(cwd, sizeof(cwd)) != NULL) {
        printf("%s\n", cwd);
        return 0;
    }
    perror("pwd");
    return 1;
}

// echo: Print arguments
int builtin_echo(char** args) {
    int newline = 1;
    int start = 1;

    // -n option: print without newline
    if (args[1] && strcmp(args[1], "-n") == 0) {
        newline = 0;
        start = 2;
    }

    for (int i = start; args[i]; i++) {
        printf("%s", args[i]);
        if (args[i + 1]) printf(" ");
    }

    if (newline) printf("\n");
    return 0;
}

// help: Display help
int builtin_help(char** args) {
    (void)args;

    printf("\n=== Mini Shell Help ===\n\n");
    printf("Built-in commands:\n");
    printf("  cd [directory]  - Change directory\n");
    printf("  pwd             - Print current directory\n");
    printf("  echo [text]     - Print text\n");
    printf("  export VAR=val  - Set environment variable\n");
    printf("  env             - List environment variables\n");
    printf("  help            - Display this help\n");
    printf("  exit            - Exit shell\n");
    printf("\nExternal commands are searched in PATH.\n\n");

    return 0;
}

// export: Set environment variable
int builtin_export(char** args) {
    if (args[1] == NULL) {
        // No arguments: print environment variables
        extern char** environ;
        for (char** env = environ; *env; env++) {
            printf("export %s\n", *env);
        }
        return 0;
    }

    // Parse VAR=value format
    for (int i = 1; args[i]; i++) {
        char* eq = strchr(args[i], '=');
        if (eq) {
            *eq = '\0';
            setenv(args[i], eq + 1, 1);
            *eq = '=';
        } else {
            // No =: set empty value
            setenv(args[i], "", 1);
        }
    }

    return 0;
}

// env: Print environment variables
int builtin_env(char** args) {
    (void)args;

    extern char** environ;
    for (char** env = environ; *env; env++) {
        printf("%s\n", *env);
    }
    return 0;
}

// Check if built-in command and execute
// Return: -1 (not built-in), 0+ (execution result)
int execute_builtin(char** args) {
    if (args[0] == NULL) return -1;

    if (strcmp(args[0], "cd") == 0) return builtin_cd(args);
    if (strcmp(args[0], "pwd") == 0) return builtin_pwd(args);
    if (strcmp(args[0], "echo") == 0) return builtin_echo(args);
    if (strcmp(args[0], "help") == 0) return builtin_help(args);
    if (strcmp(args[0], "export") == 0) return builtin_export(args);
    if (strcmp(args[0], "env") == 0) return builtin_env(args);

    return -1;  // Not built-in
}

Stage 3: Implementing Redirection

Handle >, >>, < operators.

Redirection Parser

// redirect.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

typedef struct {
    char* input_file;   // < file
    char* output_file;  // > or >> file
    int append;         // 1 if >>
} Redirect;

// Parse redirection
// Remove redirection from args and store in Redirect struct
void parse_redirect(char** args, Redirect* redir) {
    redir->input_file = NULL;
    redir->output_file = NULL;
    redir->append = 0;

    int i = 0;
    int j = 0;

    while (args[i] != NULL) {
        if (strcmp(args[i], "<") == 0) {
            // Input redirection
            if (args[i + 1]) {
                redir->input_file = args[i + 1];
                i += 2;
                continue;
            }
        } else if (strcmp(args[i], ">") == 0) {
            // Output redirection (overwrite)
            if (args[i + 1]) {
                redir->output_file = args[i + 1];
                redir->append = 0;
                i += 2;
                continue;
            }
        } else if (strcmp(args[i], ">>") == 0) {
            // Output redirection (append)
            if (args[i + 1]) {
                redir->output_file = args[i + 1];
                redir->append = 1;
                i += 2;
                continue;
            }
        }

        // Not a redirection argument
        args[j++] = args[i++];
    }
    args[j] = NULL;
}

// Apply redirection (called in child process)
int apply_redirect(Redirect* redir) {
    // Input redirection
    if (redir->input_file) {
        int fd = open(redir->input_file, O_RDONLY);
        if (fd < 0) {
            perror(redir->input_file);
            return -1;
        }
        dup2(fd, STDIN_FILENO);
        close(fd);
    }

    // Output redirection
    if (redir->output_file) {
        int flags = O_WRONLY | O_CREAT;
        flags |= redir->append ? O_APPEND : O_TRUNC;

        int fd = open(redir->output_file, flags, 0644);
        if (fd < 0) {
            perror(redir->output_file);
            return -1;
        }
        dup2(fd, STDOUT_FILENO);
        close(fd);
    }

    return 0;
}

Using Redirection

// execute with redirection
void execute_with_redirect(char** args) {
    Redirect redir;
    parse_redirect(args, &redir);

    if (args[0] == NULL) return;

    pid_t pid = fork();

    if (pid == 0) {
        // Child: apply redirection then execute
        if (apply_redirect(&redir) < 0) {
            exit(EXIT_FAILURE);
        }
        execvp(args[0], args);
        perror(args[0]);
        exit(EXIT_FAILURE);
    } else if (pid > 0) {
        int status;
        waitpid(pid, &status, 0);
    } else {
        perror("fork");
    }
}

Test:

minish> ls -l > files.txt
minish> cat < files.txt
minish> echo "additional content" >> files.txt
minish> wc -l < files.txt

Stage 4: Implementing Pipes

Connect commands with | operator.

Pipe Processing

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

#define MAX_PIPES 10

// Count pipe-separated commands
int count_pipes(char** args) {
    int count = 0;
    for (int i = 0; args[i]; i++) {
        if (strcmp(args[i], "|") == 0) count++;
    }
    return count;
}

// Split args at pipe positions
// commands[0] = first command's args
// commands[1] = second command's args
// ...
int split_by_pipe(char** args, char*** commands) {
    int cmd_count = 0;
    commands[cmd_count++] = args;

    for (int i = 0; args[i]; i++) {
        if (strcmp(args[i], "|") == 0) {
            args[i] = NULL;  // Replace pipe with NULL
            if (args[i + 1]) {
                commands[cmd_count++] = &args[i + 1];
            }
        }
    }

    return cmd_count;
}

// Execute pipe
void execute_pipe(char** args) {
    char** commands[MAX_PIPES + 1];
    int cmd_count = split_by_pipe(args, commands);

    if (cmd_count == 1) {
        // No pipe: normal execution
        execute_with_redirect(commands[0]);
        return;
    }

    int pipes[MAX_PIPES][2];  // Pipe file descriptors

    // Create pipes
    for (int i = 0; i < cmd_count - 1; i++) {
        if (pipe(pipes[i]) < 0) {
            perror("pipe");
            return;
        }
    }

    // Execute each command
    for (int i = 0; i < cmd_count; i++) {
        pid_t pid = fork();

        if (pid == 0) {
            // Child process

            // Connect previous pipe's read end to stdin
            if (i > 0) {
                dup2(pipes[i - 1][0], STDIN_FILENO);
            }

            // Connect next pipe's write end to stdout
            if (i < cmd_count - 1) {
                dup2(pipes[i][1], STDOUT_FILENO);
            }

            // Close all pipes
            for (int j = 0; j < cmd_count - 1; j++) {
                close(pipes[j][0]);
                close(pipes[j][1]);
            }

            // Execute command
            execvp(commands[i][0], commands[i]);
            perror(commands[i][0]);
            exit(EXIT_FAILURE);

        } else if (pid < 0) {
            perror("fork");
            return;
        }
    }

    // Parent: close all pipes
    for (int i = 0; i < cmd_count - 1; i++) {
        close(pipes[i][0]);
        close(pipes[i][1]);
    }

    // Wait for all child processes
    for (int i = 0; i < cmd_count; i++) {
        wait(NULL);
    }
}

Test:

minish> ls -l | grep ".c"
minish> cat file.txt | wc -l
minish> ps aux | grep bash | head -5

Stage 5: Complete Mini Shell

Full Code

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

#define MAX_INPUT 1024
#define MAX_ARGS 64
#define MAX_PIPES 10

// ============ Global Variables ============
static int last_exit_status = 0;

// ============ Signal Handler ============
void sigint_handler(int sig) {
    (void)sig;
    printf("\n");
    // Don't print prompt again (handled in main loop)
}

// ============ Utilities ============

// Trim whitespace from both ends
char* trim(char* str) {
    while (*str == ' ' || *str == '\t') str++;

    if (*str == '\0') return str;

    char* end = str + strlen(str) - 1;
    while (end > str && (*end == ' ' || *end == '\t' || *end == '\n')) {
        *end-- = '\0';
    }

    return str;
}

// ============ Parsing ============

int parse_args(char* input, char** args) {
    int argc = 0;
    char* token = strtok(input, " \t\n");

    while (token && argc < MAX_ARGS - 1) {
        args[argc++] = token;
        token = strtok(NULL, " \t\n");
    }
    args[argc] = NULL;

    return argc;
}

// ============ Redirection ============

typedef struct {
    char* infile;
    char* outfile;
    int append;
} Redirect;

void parse_redirect(char** args, Redirect* r) {
    r->infile = NULL;
    r->outfile = NULL;
    r->append = 0;

    int i = 0, j = 0;
    while (args[i]) {
        if (strcmp(args[i], "<") == 0 && args[i+1]) {
            r->infile = args[i+1];
            i += 2;
        } else if (strcmp(args[i], ">") == 0 && args[i+1]) {
            r->outfile = args[i+1];
            r->append = 0;
            i += 2;
        } else if (strcmp(args[i], ">>") == 0 && args[i+1]) {
            r->outfile = args[i+1];
            r->append = 1;
            i += 2;
        } else {
            args[j++] = args[i++];
        }
    }
    args[j] = NULL;
}

int setup_redirect(Redirect* r) {
    if (r->infile) {
        int fd = open(r->infile, O_RDONLY);
        if (fd < 0) { perror(r->infile); return -1; }
        dup2(fd, STDIN_FILENO);
        close(fd);
    }
    if (r->outfile) {
        int flags = O_WRONLY | O_CREAT | (r->append ? O_APPEND : O_TRUNC);
        int fd = open(r->outfile, flags, 0644);
        if (fd < 0) { perror(r->outfile); return -1; }
        dup2(fd, STDOUT_FILENO);
        close(fd);
    }
    return 0;
}

// ============ Built-in Commands ============

int builtin_cd(char** args) {
    const char* path = args[1] ? args[1] : getenv("HOME");

    if (strcmp(path, "-") == 0) {
        path = getenv("OLDPWD");
        if (!path) {
            fprintf(stderr, "cd: OLDPWD not set\n");
            return 1;
        }
        printf("%s\n", path);
    } else if (strcmp(path, "~") == 0) {
        path = getenv("HOME");
    }

    char oldpwd[1024];
    getcwd(oldpwd, sizeof(oldpwd));

    if (chdir(path) != 0) {
        perror("cd");
        return 1;
    }

    setenv("OLDPWD", oldpwd, 1);
    char newpwd[1024];
    getcwd(newpwd, sizeof(newpwd));
    setenv("PWD", newpwd, 1);

    return 0;
}

int builtin_pwd(void) {
    char cwd[1024];
    if (getcwd(cwd, sizeof(cwd))) {
        printf("%s\n", cwd);
        return 0;
    }
    perror("pwd");
    return 1;
}

int builtin_echo(char** args) {
    int newline = 1, start = 1;
    if (args[1] && strcmp(args[1], "-n") == 0) {
        newline = 0;
        start = 2;
    }

    for (int i = start; args[i]; i++) {
        // Environment variable expansion ($VAR)
        if (args[i][0] == '$') {
            char* val = getenv(args[i] + 1);
            printf("%s", val ? val : "");
        } else {
            printf("%s", args[i]);
        }
        if (args[i + 1]) printf(" ");
    }
    if (newline) printf("\n");
    return 0;
}

int builtin_export(char** args) {
    if (!args[1]) {
        extern char** environ;
        for (char** e = environ; *e; e++) {
            printf("export %s\n", *e);
        }
        return 0;
    }

    for (int i = 1; args[i]; i++) {
        char* eq = strchr(args[i], '=');
        if (eq) {
            *eq = '\0';
            setenv(args[i], eq + 1, 1);
        }
    }
    return 0;
}

int builtin_unset(char** args) {
    for (int i = 1; args[i]; i++) {
        unsetenv(args[i]);
    }
    return 0;
}

int builtin_help(void) {
    printf("\n");
    printf("╔═══════════════════════════════════════╗\n");
    printf("║        Mini Shell Help                ║\n");
    printf("╠═══════════════════════════════════════╣\n");
    printf("║ Built-in commands:                    ║\n");
    printf("║   cd [dir]    Change directory        ║\n");
    printf("║   pwd         Print current directory ║\n");
    printf("║   echo [...]  Print text              ║\n");
    printf("║   export V=X  Set environment var     ║\n");
    printf("║   unset VAR   Unset environment var   ║\n");
    printf("║   help        Display this help       ║\n");
    printf("║   exit [N]    Exit shell              ║\n");
    printf("╠═══════════════════════════════════════╣\n");
    printf("║ Redirection:                          ║\n");
    printf("║   cmd > file  Redirect output to file║\n");
    printf("║   cmd >> file Append output to file  ║\n");
    printf("║   cmd < file  Read input from file   ║\n");
    printf("╠═══════════════════════════════════════╣\n");
    printf("║ Pipes:                                ║\n");
    printf("║   cmd1 | cmd2 Pipe output to next cmd║\n");
    printf("╚═══════════════════════════════════════╝\n");
    printf("\n");
    return 0;
}

// Execute built-in command (-1: not built-in)
int run_builtin(char** args) {
    if (!args[0]) return -1;

    if (strcmp(args[0], "cd") == 0) return builtin_cd(args);
    if (strcmp(args[0], "pwd") == 0) return builtin_pwd();
    if (strcmp(args[0], "echo") == 0) return builtin_echo(args);
    if (strcmp(args[0], "export") == 0) return builtin_export(args);
    if (strcmp(args[0], "unset") == 0) return builtin_unset(args);
    if (strcmp(args[0], "help") == 0) return builtin_help();

    return -1;
}

// ============ Pipe Execution ============

int split_pipe(char** args, char*** cmds) {
    int n = 0;
    cmds[n++] = args;

    for (int i = 0; args[i]; i++) {
        if (strcmp(args[i], "|") == 0) {
            args[i] = NULL;
            if (args[i + 1]) {
                cmds[n++] = &args[i + 1];
            }
        }
    }
    return n;
}

void run_pipeline(char** args) {
    char** cmds[MAX_PIPES + 1];
    int n = split_pipe(args, cmds);

    // No pipe: single command execution
    if (n == 1) {
        Redirect r;
        parse_redirect(cmds[0], &r);

        if (!cmds[0][0]) return;

        // Check built-in
        int builtin_result = run_builtin(cmds[0]);
        if (builtin_result != -1) {
            last_exit_status = builtin_result;
            return;
        }

        // External command
        pid_t pid = fork();
        if (pid == 0) {
            setup_redirect(&r);
            execvp(cmds[0][0], cmds[0]);
            fprintf(stderr, "%s: command not found\n", cmds[0][0]);
            exit(127);
        } else if (pid > 0) {
            int status;
            waitpid(pid, &status, 0);
            last_exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : 1;
        }
        return;
    }

    // With pipes
    int pipes[MAX_PIPES][2];
    for (int i = 0; i < n - 1; i++) {
        pipe(pipes[i]);
    }

    for (int i = 0; i < n; i++) {
        pid_t pid = fork();

        if (pid == 0) {
            // Connect input
            if (i > 0) {
                dup2(pipes[i-1][0], STDIN_FILENO);
            }
            // Connect output
            if (i < n - 1) {
                dup2(pipes[i][1], STDOUT_FILENO);
            }

            // Close all pipes
            for (int j = 0; j < n - 1; j++) {
                close(pipes[j][0]);
                close(pipes[j][1]);
            }

            // Handle redirection (only for first/last command)
            Redirect r;
            parse_redirect(cmds[i], &r);
            if (i == 0 && r.infile) {
                int fd = open(r.infile, O_RDONLY);
                if (fd >= 0) { dup2(fd, STDIN_FILENO); close(fd); }
            }
            if (i == n - 1 && r.outfile) {
                int flags = O_WRONLY | O_CREAT | (r.append ? O_APPEND : O_TRUNC);
                int fd = open(r.outfile, flags, 0644);
                if (fd >= 0) { dup2(fd, STDOUT_FILENO); close(fd); }
            }

            execvp(cmds[i][0], cmds[i]);
            fprintf(stderr, "%s: command not found\n", cmds[i][0]);
            exit(127);
        }
    }

    // Parent: close pipes and wait
    for (int i = 0; i < n - 1; i++) {
        close(pipes[i][0]);
        close(pipes[i][1]);
    }

    int status;
    for (int i = 0; i < n; i++) {
        wait(&status);
    }
    last_exit_status = WIFEXITED(status) ? WEXITSTATUS(status) : 1;
}

// ============ Prompt ============

void print_prompt(void) {
    char cwd[256];
    char* dir = getcwd(cwd, sizeof(cwd));

    // Display home directory as ~
    char* home = getenv("HOME");
    if (home && dir && strncmp(dir, home, strlen(home)) == 0) {
        printf("\033[1;34m~%s\033[0m", dir + strlen(home));
    } else {
        printf("\033[1;34m%s\033[0m", dir ? dir : "?");
    }

    // Change color based on exit code
    if (last_exit_status == 0) {
        printf(" \033[1;32m❯\033[0m ");
    } else {
        printf(" \033[1;31m❯\033[0m ");
    }

    fflush(stdout);
}

// ============ Main ============

int main(void) {
    char input[MAX_INPUT];
    char* args[MAX_ARGS];

    // Set signal handler
    signal(SIGINT, sigint_handler);

    printf("\n\033[1;36m=== Mini Shell ===\033[0m\n");
    printf("Type 'help' for help\n\n");

    while (1) {
        print_prompt();

        if (fgets(input, sizeof(input), stdin) == NULL) {
            printf("\nexit\n");
            break;
        }

        char* trimmed = trim(input);
        if (*trimmed == '\0') continue;

        // Ignore comments
        if (trimmed[0] == '#') continue;

        // Copy input (strtok modifies original)
        char input_copy[MAX_INPUT];
        strncpy(input_copy, trimmed, sizeof(input_copy));

        // Parse
        int argc = parse_args(input_copy, args);
        if (argc == 0) continue;

        // exit command
        if (strcmp(args[0], "exit") == 0) {
            int code = args[1] ? atoi(args[1]) : last_exit_status;
            printf("exit\n");
            exit(code);
        }

        // Execute
        run_pipeline(args);
    }

    return last_exit_status;
}

Compile and Run

gcc -o minishell minishell.c -Wall -Wextra
./minishell

Test Examples

=== Mini Shell ===
Type 'help' for help

~  help
~  pwd
/Users/username
~  cd /tmp
/tmp  ls -la
/tmp  echo $HOME
/Users/username
/tmp  export MY_VAR=hello
/tmp  echo $MY_VAR
hello
/tmp  ls -l | grep ".txt" | wc -l
/tmp  cat /etc/passwd | head -5 > first5.txt
/tmp  cat first5.txt
/tmp  cd -
/Users/username
~  exit

Stage 6: Additional Features

History Feature

#define HISTORY_SIZE 100

static char* history[HISTORY_SIZE];
static int history_count = 0;

void add_history(const char* cmd) {
    if (history_count < HISTORY_SIZE) {
        history[history_count++] = strdup(cmd);
    } else {
        // Remove oldest
        free(history[0]);
        memmove(history, history + 1, (HISTORY_SIZE - 1) * sizeof(char*));
        history[HISTORY_SIZE - 1] = strdup(cmd);
    }
}

int builtin_history(char** args) {
    int n = history_count;
    if (args[1]) {
        n = atoi(args[1]);
        if (n > history_count) n = history_count;
    }

    int start = history_count - n;
    for (int i = start; i < history_count; i++) {
        printf("%5d  %s\n", i + 1, history[i]);
    }
    return 0;
}

void free_history(void) {
    for (int i = 0; i < history_count; i++) {
        free(history[i]);
    }
}

Background Execution (&)

// Check for &
int is_background(char** args) {
    int i = 0;
    while (args[i]) i++;

    if (i > 0 && strcmp(args[i - 1], "&") == 0) {
        args[i - 1] = NULL;  // Remove &
        return 1;
    }
    return 0;
}

// Modified execution function
void run_command(char** args) {
    int bg = is_background(args);

    pid_t pid = fork();

    if (pid == 0) {
        // Child
        execvp(args[0], args);
        perror(args[0]);
        exit(127);
    } else if (pid > 0) {
        if (bg) {
            printf("[%d] %d\n", 1, pid);
            // Background: don't wait
        } else {
            int status;
            waitpid(pid, &status, 0);
        }
    }
}

Wildcard Expansion (*)

#include <glob.h>

// Wildcard expansion
int expand_wildcards(char** args, char** expanded, int max_expanded) {
    int count = 0;

    for (int i = 0; args[i] && count < max_expanded - 1; i++) {
        // Arguments containing * or ?
        if (strchr(args[i], '*') || strchr(args[i], '?')) {
            glob_t results;
            int ret = glob(args[i], GLOB_NOCHECK | GLOB_TILDE, NULL, &results);

            if (ret == 0) {
                for (size_t j = 0; j < results.gl_pathc && count < max_expanded - 1; j++) {
                    expanded[count++] = strdup(results.gl_pathv[j]);
                }
            }
            globfree(&results);
        } else {
            expanded[count++] = args[i];
        }
    }

    expanded[count] = NULL;
    return count;
}

Exercises

Exercise 1: Semicolon Support

Implement sequential execution of multiple commands with cmd1 ; cmd2 format.

Exercise 2: && and || Operators

  • cmd1 && cmd2: Execute cmd2 only if cmd1 succeeds
  • cmd1 || cmd2: Execute cmd2 only if cmd1 fails

Exercise 3: Quote Handling

Process echo "hello world" so that "hello world" is treated as a single argument.

Exercise 4: Tab Completion

Use the readline library to implement tab auto-completion.


Key Concepts Summary

Function Description
fork() Clone process
exec*() Execute program
wait() Wait for child process
pipe() Create pipe
dup2() Duplicate file descriptor
open() Open file
signal() Register signal handler
Concept Description
Pipe Unidirectional inter-process communication
Redirection Change input/output direction
Environment variables Settings passed to processes
Signal Notification sent to processes

Next Steps

Once you've completed the mini shell, proceed to the next project: - Project 12: Multithreaded Programming - Using pthread

to navigate between lessons