Custom Shell Command Processor

Comprehensive Bash command-line interface in Unix environment

Project Overview

Developed a comprehensive Bash command-line interface application in a Unix environment using shell scripting and system programming in Fall 2018. The implementation features custom command parsing, input validation, and process management capabilities, enhancing system interaction efficiency by streamlining command processing, automating task execution, and improving user experience for routine administrative operations.

Shell Architecture Design

Core Components

  • Command Parser: Tokenization and syntax analysis
  • Process Manager: Fork/exec process creation and management
  • Built-in Commands: Internal command implementations
  • I/O Redirection: File input/output handling
  • Pipeline Support: Command chaining with pipes
  • Job Control: Background/foreground process management

System Integration

User Input → Command Parser → Process Manager → System Calls
     ↑             ↓              ↓              ↓
  Prompt    Built-in Check   Fork/Exec      Kernel
Generator      ↓              ↓              ↓
     ↑      External Cmd   Wait/Signal   Process
  Output ← I/O Redirection ← Job Control ← Execution

Command Parser Implementation

Lexical Analysis

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

#define MAX_CMD_LEN 1024
#define MAX_ARGS 64
#define MAX_JOBS 64

typedef enum {
    TOKEN_WORD,
    TOKEN_PIPE,
    TOKEN_REDIRECT_IN,
    TOKEN_REDIRECT_OUT,
    TOKEN_REDIRECT_APPEND,
    TOKEN_BACKGROUND,
    TOKEN_EOF
} token_type_t;

typedef struct {
    token_type_t type;
    char* value;
} token_t;

typedef struct {
    char** args;
    int argc;
    char* input_file;
    char* output_file;
    int append_output;
    int background;
} command_t;

// Tokenize input string
token_t* tokenize(char* input, int* token_count) {
    token_t* tokens = malloc(MAX_ARGS * sizeof(token_t));
    char* token_str;
    int count = 0;
    
    // Handle special characters and operators
    char* special_chars = "|<>&";
    char* current = input;
    
    while (*current && count < MAX_ARGS - 1) {
        // Skip whitespace
        while (*current == ' ' || *current == '\t') current++;
        if (*current == '\0') break;
        
        tokens[count].value = malloc(256);
        
        if (*current == '|') {
            tokens[count].type = TOKEN_PIPE;
            strcpy(tokens[count].value, "|");
            current++;
        } else if (*current == '<') {
            tokens[count].type = TOKEN_REDIRECT_IN;
            strcpy(tokens[count].value, "<");
            current++;
        } else if (*current == '>') {
            if (*(current + 1) == '>') {
                tokens[count].type = TOKEN_REDIRECT_APPEND;
                strcpy(tokens[count].value, ">>");
                current += 2;
            } else {
                tokens[count].type = TOKEN_REDIRECT_OUT;
                strcpy(tokens[count].value, ">");
                current++;
            }
        } else if (*current == '&') {
            tokens[count].type = TOKEN_BACKGROUND;
            strcpy(tokens[count].value, "&");
            current++;
        } else {
            // Regular word token
            tokens[count].type = TOKEN_WORD;
            int pos = 0;
            
            // Handle quoted strings
            if (*current == '"' || *current == '\'') {
                char quote = *current++;
                while (*current && *current != quote && pos < 255) {
                    tokens[count].value[pos++] = *current++;
                }
                if (*current == quote) current++; // Skip closing quote
            } else {
                // Regular word
                while (*current && *current != ' ' && *current != '\t' && 
                       !strchr(special_chars, *current) && pos < 255) {
                    tokens[count].value[pos++] = *current++;
                }
            }
            tokens[count].value[pos] = '\0';
        }
        count++;
    }
    
    // Add EOF token
    tokens[count].type = TOKEN_EOF;
    tokens[count].value = NULL;
    
    *token_count = count;
    return tokens;
}

Command Structure Parsing

// Parse tokens into command structure
command_t* parse_command(token_t* tokens, int* cmd_count) {
    command_t* commands = malloc(MAX_ARGS * sizeof(command_t));
    int current_cmd = 0;
    int token_idx = 0;
    
    // Initialize first command
    commands[current_cmd].args = malloc(MAX_ARGS * sizeof(char*));
    commands[current_cmd].argc = 0;
    commands[current_cmd].input_file = NULL;
    commands[current_cmd].output_file = NULL;
    commands[current_cmd].append_output = 0;
    commands[current_cmd].background = 0;
    
    while (tokens[token_idx].type != TOKEN_EOF) {
        switch (tokens[token_idx].type) {
            case TOKEN_WORD:
                commands[current_cmd].args[commands[current_cmd].argc] = 
                    strdup(tokens[token_idx].value);
                commands[current_cmd].argc++;
                break;
                
            case TOKEN_PIPE:
                // Null-terminate current command args
                commands[current_cmd].args[commands[current_cmd].argc] = NULL;
                
                // Start new command
                current_cmd++;
                commands[current_cmd].args = malloc(MAX_ARGS * sizeof(char*));
                commands[current_cmd].argc = 0;
                commands[current_cmd].input_file = NULL;
                commands[current_cmd].output_file = NULL;
                commands[current_cmd].append_output = 0;
                commands[current_cmd].background = 0;
                break;
                
            case TOKEN_REDIRECT_IN:
                token_idx++; // Move to filename
                if (tokens[token_idx].type == TOKEN_WORD) {
                    commands[current_cmd].input_file = strdup(tokens[token_idx].value);
                }
                break;
                
            case TOKEN_REDIRECT_OUT:
                token_idx++; // Move to filename
                if (tokens[token_idx].type == TOKEN_WORD) {
                    commands[current_cmd].output_file = strdup(tokens[token_idx].value);
                    commands[current_cmd].append_output = 0;
                }
                break;
                
            case TOKEN_REDIRECT_APPEND:
                token_idx++; // Move to filename
                if (tokens[token_idx].type == TOKEN_WORD) {
                    commands[current_cmd].output_file = strdup(tokens[token_idx].value);
                    commands[current_cmd].append_output = 1;
                }
                break;
                
            case TOKEN_BACKGROUND:
                commands[current_cmd].background = 1;
                break;
        }
        token_idx++;
    }
    
    // Null-terminate final command args
    commands[current_cmd].args[commands[current_cmd].argc] = NULL;
    
    *cmd_count = current_cmd + 1;
    return commands;
}

Built-in Command Implementation

Core Built-in Commands

// Built-in command structure
typedef struct {
    char* name;
    int (*function)(char** args);
    char* description;
} builtin_t;

// Built-in command implementations
int builtin_cd(char** args) {
    if (args[1] == NULL) {
        // No argument, change to home directory
        char* home = getenv("HOME");
        if (home == NULL) {
            fprintf(stderr, "cd: HOME not set\n");
            return 1;
        }
        if (chdir(home) != 0) {
            perror("cd");
            return 1;
        }
    } else {
        if (chdir(args[1]) != 0) {
            perror("cd");
            return 1;
        }
    }
    return 0;
}

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

int builtin_echo(char** args) {
    for (int i = 1; args[i] != NULL; i++) {
        printf("%s", args[i]);
        if (args[i + 1] != NULL) printf(" ");
    }
    printf("\n");
    return 0;
}

int builtin_export(char** args) {
    if (args[1] == NULL) {
        // Print all environment variables
        extern char** environ;
        for (int i = 0; environ[i] != NULL; i++) {
            printf("export %s\n", environ[i]);
        }
    } else {
        // Set environment variable
        char* eq_pos = strchr(args[1], '=');
        if (eq_pos != NULL) {
            *eq_pos = '\0';
            char* name = args[1];
            char* value = eq_pos + 1;
            
            if (setenv(name, value, 1) != 0) {
                perror("export");
                return 1;
            }
        } else {
            fprintf(stderr, "export: invalid format\n");
            return 1;
        }
    }
    return 0;
}

int builtin_history(char** args) {
    extern char** command_history;
    extern int history_count;
    
    for (int i = 0; i < history_count; i++) {
        printf("%4d  %s\n", i + 1, command_history[i]);
    }
    return 0;
}

int builtin_jobs(char** args) {
    extern job_t jobs[];
    extern int job_count;
    
    for (int i = 0; i < job_count; i++) {
        if (jobs[i].active) {
            printf("[%d]  %s  %s\n", i + 1, 
                   jobs[i].background ? "Running" : "Stopped",
                   jobs[i].command);
        }
    }
    return 0;
}

int builtin_exit(char** args) {
    // Clean up and exit
    cleanup_shell();
    exit(0);
}

// Built-in command table
builtin_t builtins[] = {
    {"cd", builtin_cd, "Change directory"},
    {"pwd", builtin_pwd, "Print working directory"},
    {"echo", builtin_echo, "Display text"},
    {"export", builtin_export, "Set environment variable"},
    {"history", builtin_history, "Show command history"},
    {"jobs", builtin_jobs, "List active jobs"},
    {"exit", builtin_exit, "Exit shell"},
    {NULL, NULL, NULL}
};

// Check if command is built-in
int execute_builtin(char** args) {
    if (args[0] == NULL) return 0;
    
    for (int i = 0; builtins[i].name != NULL; i++) {
        if (strcmp(args[0], builtins[i].name) == 0) {
            return builtins[i].function(args);
        }
    }
    return -1; // Not a built-in command
}

Process Management

Fork/Exec Implementation

// Job control structure
typedef struct {
    pid_t pid;
    char* command;
    int background;
    int active;
    int job_id;
} job_t;

job_t jobs[MAX_JOBS];
int job_count = 0;

// Execute external command
int execute_external(command_t* cmd) {
    pid_t pid = fork();
    
    if (pid == 0) {
        // Child process
        
        // Handle input redirection
        if (cmd->input_file) {
            int fd = open(cmd->input_file, O_RDONLY);
            if (fd < 0) {
                perror("input redirection");
                exit(1);
            }
            dup2(fd, STDIN_FILENO);
            close(fd);
        }
        
        // Handle output redirection
        if (cmd->output_file) {
            int flags = O_WRONLY | O_CREAT;
            flags |= cmd->append_output ? O_APPEND : O_TRUNC;
            
            int fd = open(cmd->output_file, flags, 0644);
            if (fd < 0) {
                perror("output redirection");
                exit(1);
            }
            dup2(fd, STDOUT_FILENO);
            close(fd);
        }
        
        // Execute command
        if (execvp(cmd->args[0], cmd->args) < 0) {
            perror(cmd->args[0]);
            exit(1);
        }
    } else if (pid < 0) {
        perror("fork");
        return 1;
    } else {
        // Parent process
        if (!cmd->background) {
            // Foreground job - wait for completion
            int status;
            waitpid(pid, &status, 0);
            return WEXITSTATUS(status);
        } else {
            // Background job - add to job list
            add_job(pid, create_command_string(cmd), 1);
            printf("[%d] %d\n", job_count, pid);
            return 0;
        }
    }
}

Pipeline Implementation

// Execute pipeline of commands
int execute_pipeline(command_t* commands, int cmd_count) {
    int pipefds[cmd_count - 1][2];
    pid_t pids[cmd_count];
    
    // Create pipes
    for (int i = 0; i < cmd_count - 1; i++) {
        if (pipe(pipefds[i]) < 0) {
            perror("pipe");
            return 1;
        }
    }
    
    // Execute each command in pipeline
    for (int i = 0; i < cmd_count; i++) {
        pids[i] = fork();
        
        if (pids[i] == 0) {
            // Child process
            
            // Set up input (from previous pipe or file)
            if (i > 0) {
                dup2(pipefds[i-1][0], STDIN_FILENO);
            } else if (commands[i].input_file) {
                int fd = open(commands[i].input_file, O_RDONLY);
                if (fd < 0) {
                    perror("input redirection");
                    exit(1);
                }
                dup2(fd, STDIN_FILENO);
                close(fd);
            }
            
            // Set up output (to next pipe or file)
            if (i < cmd_count - 1) {
                dup2(pipefds[i][1], STDOUT_FILENO);
            } else if (commands[i].output_file) {
                int flags = O_WRONLY | O_CREAT;
                flags |= commands[i].append_output ? O_APPEND : O_TRUNC;
                
                int fd = open(commands[i].output_file, flags, 0644);
                if (fd < 0) {
                    perror("output redirection");
                    exit(1);
                }
                dup2(fd, STDOUT_FILENO);
                close(fd);
            }
            
            // Close all pipe file descriptors
            for (int j = 0; j < cmd_count - 1; j++) {
                close(pipefds[j][0]);
                close(pipefds[j][1]);
            }
            
            // Execute command
            if (execvp(commands[i].args[0], commands[i].args) < 0) {
                perror(commands[i].args[0]);
                exit(1);
            }
        } else if (pids[i] < 0) {
            perror("fork");
            return 1;
        }
    }
    
    // Close all pipe file descriptors in parent
    for (int i = 0; i < cmd_count - 1; i++) {
        close(pipefds[i][0]);
        close(pipefds[i][1]);
    }
    
    // Wait for all processes (if foreground)
    if (!commands[cmd_count-1].background) {
        int status;
        for (int i = 0; i < cmd_count; i++) {
            waitpid(pids[i], &status, 0);
        }
        return WEXITSTATUS(status);
    } else {
        // Add pipeline as background job
        char* pipeline_cmd = create_pipeline_string(commands, cmd_count);
        add_job(pids[cmd_count-1], pipeline_cmd, 1);
        printf("[%d] %d\n", job_count, pids[cmd_count-1]);
        return 0;
    }
}

Advanced Features

Command History

#define HISTORY_SIZE 1000

char* command_history[HISTORY_SIZE];
int history_count = 0;
int history_index = 0;

// Add command to history
void add_to_history(char* command) {
    if (history_count < HISTORY_SIZE) {
        command_history[history_count] = strdup(command);
        history_count++;
    } else {
        // Circular buffer - overwrite oldest
        free(command_history[history_index]);
        command_history[history_index] = strdup(command);
        history_index = (history_index + 1) % HISTORY_SIZE;
    }
}

// History expansion (!n, !!, !string)
char* expand_history(char* input) {
    if (input[0] != '!') return strdup(input);
    
    if (input[1] == '!') {
        // !! - repeat last command
        if (history_count > 0) {
            int last_idx = (history_count - 1) % HISTORY_SIZE;
            return strdup(command_history[last_idx]);
        }
    } else if (isdigit(input[1])) {
        // !n - repeat command n
        int n = atoi(&input[1]);
        if (n > 0 && n <= history_count) {
            return strdup(command_history[n - 1]);
        }
    } else {
        // !string - repeat last command starting with string
        char* search = &input[1];
        for (int i = history_count - 1; i >= 0; i--) {
            if (strncmp(command_history[i], search, strlen(search)) == 0) {
                return strdup(command_history[i]);
            }
        }
    }
    
    return strdup(input); // No expansion found
}

Tab Completion

#include <dirent.h>
#include <glob.h>

// Simple tab completion for files and commands
char** complete_command(char* partial, int* count) {
    char** completions = malloc(256 * sizeof(char*));
    *count = 0;
    
    // Check if it's a path completion
    if (strchr(partial, '/') != NULL) {
        // File/directory completion
        char* dir_part = strdup(partial);
        char* base_part = strrchr(dir_part, '/');
        
        if (base_part) {
            *base_part = '\0';
            base_part++;
            
            DIR* dir = opendir(dir_part[0] ? dir_part : ".");
            if (dir) {
                struct dirent* entry;
                while ((entry = readdir(dir)) != NULL && *count < 255) {
                    if (strncmp(entry->d_name, base_part, strlen(base_part)) == 0) {
                        completions[*count] = malloc(strlen(dir_part) + strlen(entry->d_name) + 2);
                        sprintf(completions[*count], "%s/%s", dir_part, entry->d_name);
                        (*count)++;
                    }
                }
                closedir(dir);
            }
        }
        free(dir_part);
    } else {
        // Command completion from PATH
        char* path = getenv("PATH");
        char* path_copy = strdup(path);
        char* dir = strtok(path_copy, ":");
        
        while (dir && *count < 255) {
            DIR* d = opendir(dir);
            if (d) {
                struct dirent* entry;
                while ((entry = readdir(d)) != NULL && *count < 255) {
                    if (strncmp(entry->d_name, partial, strlen(partial)) == 0) {
                        completions[*count] = strdup(entry->d_name);
                        (*count)++;
                    }
                }
                closedir(d);
            }
            dir = strtok(NULL, ":");
        }
        free(path_copy);
    }
    
    return completions;
}

Main Shell Loop

Interactive Shell

int main() {
    char input[MAX_CMD_LEN];
    char* line;
    
    // Initialize shell
    init_shell();
    
    while (1) {
        // Display prompt
        display_prompt();
        
        // Read input
        if (fgets(input, sizeof(input), stdin) == NULL) {
            if (feof(stdin)) {
                printf("\n");
                break; // EOF (Ctrl+D)
            }
            continue;
        }
        
        // Remove newline
        input[strcspn(input, "\n")] = '\0';
        
        // Skip empty input
        if (strlen(input) == 0) continue;
        
        // Add to history
        add_to_history(input);
        
        // Expand history if needed
        char* expanded = expand_history(input);
        
        // Tokenize input
        int token_count;
        token_t* tokens = tokenize(expanded, &token_count);
        
        // Parse commands
        int cmd_count;
        command_t* commands = parse_command(tokens, &cmd_count);
        
        // Execute commands
        if (cmd_count == 1) {
            // Single command
            int builtin_result = execute_builtin(commands[0].args);
            if (builtin_result == -1) {
                // External command
                execute_external(&commands[0]);
            }
        } else if (cmd_count > 1) {
            // Pipeline
            execute_pipeline(commands, cmd_count);
        }
        
        // Cleanup
        free_tokens(tokens, token_count);
        free_commands(commands, cmd_count);
        free(expanded);
        
        // Check for completed background jobs
        check_background_jobs();
    }
    
    cleanup_shell();
    return 0;
}

void display_prompt() {
    char cwd[256];
    char* user = getenv("USER");
    char hostname[64];
    
    getcwd(cwd, sizeof(cwd));
    gethostname(hostname, sizeof(hostname));
    
    // Colorized prompt: user@hostname:cwd$
    printf("\033[32m%s\033[0m@\033[34m%s\033[0m:\033[33m%s\033[0m$ ", 
           user ? user : "user", hostname, cwd);
    fflush(stdout);
}

Key Achievements

Shell Functionality

  • Complete Command Processing: Parsing, validation, and execution
  • Built-in Commands: 7 essential shell built-ins implemented
  • I/O Redirection: Full support for <, >, » operators
  • Pipeline Support: Multi-command pipelines with proper process handling
  • Job Control: Background/foreground job management

System Programming Excellence

  • Process Management: Fork/exec model implementation
  • Signal Handling: Proper signal management for job control
  • Memory Management: Dynamic allocation with proper cleanup
  • Error Handling: Comprehensive error detection and reporting

User Experience Enhancement

  • Command History: Full history with expansion support
  • Tab Completion: File and command completion
  • Colorized Prompt: Visual feedback with current directory
  • Administrative Automation: Streamlined routine operations

Technologies Used

  • C Programming for system-level shell implementation
  • Unix System Calls for process and file management
  • POSIX APIs for portable system programming
  • Shell Scripting for automation and configuration
  • GNU Readline for advanced input handling (optional enhancement)

The project demonstrates comprehensive understanding of operating system concepts, system programming, process management, and Unix shell design essential for systems administration, DevOps engineering, and low-level software development.