custom-unix-shell / wish.c
wish.c
Raw
//Add your code here
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<ctype.h>
#include<errno.h>
#include<fcntl.h>

// function for finding ampersand (parallel commands)
int findParallelCmds(char* str, char** parallelStr)
{
    int i; // to be used as parallel cmds counter
    size_t length = strlen(str);
    for (i = 0; i < length; i++) {
        parallelStr[i] = strsep(&str, "&");
        if (parallelStr[i] == NULL) {
            break;
        }
    }
    // returns number of parallel cmds on a command line
    // or zero if no parallel cmds found.
    if (i > 1)
        return i;
    else {
        return 0; 
    }
}

// function to check if redirection is requested
int checkRedirection(char *str, char *filename) 
{
    // Find the position of the ">" operator in the command
    // Return codes:
    // 0, valid, no ">" found
    // 1, valid, redirection found
    // -1, invalid, empty cmd with ">"
    // -2, invalid, no file specified
    // -3, invalid, more than one file specified
    char *p = strchr(str, '>');
    if (p == NULL) {
        return 0;
    }
    else if (p == str) {
        return -1;
    }
    else {
        // The > operator was found, extract the filename
        char *f = strtok(p + 1, " ");
        if (f == NULL) {
            return -2;
        }
        // get the next token
        p = strtok(NULL, " \t\r\n");
        if (p != NULL) {
            return -3;
        }
        strcpy(filename, f);
        // remove any leading or trailing whitespace 
        // and only save the file name to filename 
        // (including relative path if specified)
        filename = strtok(filename, " \t\r\n");
        // extract the remaining command args
        str = strtok(str, ">");
    }
    return 1;
}

// function to check and handle built-in commands if requested
int useBuiltInCmds(int argc, char **args) 
{
    // Return 1 if one of the built-in commands was executed with 
    // given arguments. Return 0 if no built-in cmd was used.
    int noOfBuiltInCmds = 3;
    int chosenCmd = 0;
    char* listOfBuiltInCmds[noOfBuiltInCmds];
    listOfBuiltInCmds[0] = "exit";
    listOfBuiltInCmds[1] = "cd";
    listOfBuiltInCmds[2] = "path";

    for (int i = 0; i < noOfBuiltInCmds; i++) {
        if (strcmp(args[0], listOfBuiltInCmds[i]) == 0) {
            chosenCmd = i + 1;
            break;
        }
    }
    switch (chosenCmd) {
    case 1: // exit cmd
        if (argc > 1) {
            char error_message[30] = "An error has occurred\n";
            write(STDERR_FILENO, error_message, strlen(error_message));
            return 1;
        }
        exit(0);
    case 2: // cd cmd
        if (argc == 1 || argc > 2) {
            char error_message[30] = "An error has occurred\n";
            write(STDERR_FILENO, error_message, strlen(error_message));
            return 1;
        }
        if (chdir(args[1]) != 0) {
            char error_message[30] = "An error has occurred\n";
            write(STDERR_FILENO, error_message, strlen(error_message));
        }
        return 1;
    case 3: // path cmd
        if (argc == 1) {
            // set PATH to an empty string if no path passed in
            setenv("PATH", "", 1);
        } else {
            // allocate initial buffer memory for added path
            // initialize the buffer with a null terminator
            char *newPath = (char*) malloc(1);
            newPath[0] = '\0';
            // concatinate the old path to the new path separated by a colon
            char *oldPath = getenv("PATH"); // get the current search path
            newPath = realloc(newPath, strlen(newPath) + strlen(oldPath) + 2);
            newPath = strcat(newPath, oldPath);
            newPath = strcat(newPath, ":");
            // concatinate new input paths
            for (int i = 1; i < argc; i++) {
                if (strlen(newPath) + strlen(args[i]) > strlen(newPath)) {
                    newPath = realloc(newPath, (strlen(newPath) 
                            + strlen(args[i])) * 2 * sizeof(char) + 1);
                }
                newPath = strcat(newPath, args[i]);
                newPath = strcat(newPath, ":");
            }
            // replace the last colon with a null terminator
            newPath[strlen(newPath) - 1] = '\0';
            // set the new PATH environment variable
            setenv("PATH", newPath, 1); 
            free(newPath);
            newPath = NULL;
        }
        return 1;
    default:
        break;
    }
    return 0;
}

// function to execute system execv() and wait for child to terminate
void execvArgs(char **args)
{
    // Forking a child
    pid_t pid = fork();

    if (pid == -1) {
        char error_message[30] = "An error has occurred\n";
        write(STDERR_FILENO, error_message, strlen(error_message));
        return;

    } else if (pid == 0) {
        char *path = getenv("PATH"); // get the current search path
        char *p = strtok(path, ":"); // tokenize the search path
        while (p != NULL) {
            // allocate memory for the executable path
            char *exe = malloc((strlen(p) + strlen(args[0])) * sizeof(char) + 2);
            // construct the full path to the executable
            sprintf(exe, "%s/%s", p, args[0]);
            // check if the executable exists and is executable
            if (access(exe, X_OK) == 0) {
                if (execv(exe, args) < 0) { 
                    fprintf(stderr, "%s\n", strerror(errno));
                }
                exit(0);
            }
            // free the allocated memory
            free(exe);
            exe = NULL;
            // get the next token
            p = strtok(NULL, ":");
        }
        // if path is NULL, kill child process
        char error_message[30] = "An error has occurred\n";
        write(STDERR_FILENO, error_message, strlen(error_message));
        exit(0);
    } else {
        // waiting for child to terminate
        wait(NULL);
        return;
    }
}

// function to execute multiple system execv() and DO NOT wait 
// for a child to terminate (parallelism)
void execMultipleArgs(char **args)
{
    // Forking a child
    pid_t pid = fork();

    if (pid == -1) {
        char error_message[30] = "An error has occurred\n";
        write(STDERR_FILENO, error_message, strlen(error_message));
        return;
    } else if (pid == 0) {
        char *path = getenv("PATH"); // get the current search path
        char *p = strtok(path, ":"); // tokenize the search path
        while (p != NULL) {
            // allocate memory for the executable path
            char *exe = malloc(strlen(p) + strlen(args[0]) + 2);
            // construct the full path to the executable
            sprintf(exe, "%s/%s", p, args[0]); 
            // check if the executable exists and is executable
            if (access(exe, X_OK) == 0) {
                if (execv(exe, args) < 0) { 
                    fprintf(stderr, "%s\n", strerror(errno));
                    exit(0);
                }
            }
            // free the allocated memory
            free(exe);
            exe = NULL;
            // get the next token
            p = strtok(NULL, ":");
        }
        // if path is NULL, kill child process
        char error_message[30] = "An error has occurred\n";
        write(STDERR_FILENO, error_message, strlen(error_message));
        exit(0);
    }
}

// function for parsing a single command line
int parseSpace(char *str, int argsCounter, char **args)
{
    // Return argument counter. Otherwise, return 0.
    int i;
    size_t length = strlen(str);
    for (i = 0; i < length; i++) {
        args[i] = strsep(&str, " \t\n");
        if (args[i] == NULL) {
            break;
        }
        if (strlen(args[i]) == 0) {
            i--;
        }
    }
    return i;
}

// this function executes multiple commands
void execMultipleCmd(char *str, char *filename, char** args)
{
    int argsCounter = 0; // number of arguments, including the cmd arg
    
    // base on redirection codes for cases
    int redirection_code = checkRedirection(str, filename);

    argsCounter = parseSpace(str, argsCounter, args);
    if (argsCounter == 0) { // only whitespace found
        return;
    }
    if (redirection_code == 1) { // redirection
        // save the original file descriptor for stdout/stderr
        int orig_stdout_fd = dup(STDOUT_FILENO);
        int orig_stderr_fd = dup(STDERR_FILENO);
        // open a file for writing and redirect to it
        int fd;
        fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
        if (fd == -1) {
            char error_message[30] = "An error has occurred\n";
            write(STDERR_FILENO, error_message, strlen(error_message));
        }
        // redirect stderr to stdout
        if (dup2(fd, STDOUT_FILENO) == -1 || dup2(fd, STDERR_FILENO) == -1) {
            char error_message[30] = "An error has occurred\n";
            write(STDERR_FILENO, error_message, strlen(error_message));
        }
        // execute
        execMultipleArgs(args);

        // reverse redirection after finish
        dup2(orig_stderr_fd, STDERR_FILENO);
        dup2(orig_stdout_fd, STDOUT_FILENO);
        // close file
        close(fd);
    }
    else if (redirection_code == 0) { // no redirection
        execMultipleArgs(args);
    }
    else { // errors with cmd
        char error_message[30] = "An error has occurred\n";
        write(STDERR_FILENO, error_message, strlen(error_message));
    }
}

// function to execute single command
void execSingleCmd(char *str, char *filename, char** args)
{
    int argsCounter = 0; // number of arguments, including the cmd arg
    // single cmd found
    // base on redirection codes for cases
    int redirection_code = checkRedirection(str, filename);
    // count the number of arguments
    argsCounter = parseSpace(str, argsCounter, args);
    if (argsCounter == 0) { // only whitespace found
        return;
    }
    if (redirection_code == 1) { // redirection
        // save the original file descriptor for stdout/stderr
        int orig_stdout_fd = dup(STDOUT_FILENO);
        int orig_stderr_fd = dup(STDERR_FILENO);
        // open a file for writing and redirect to it
        int fd;
        fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
        if (fd == -1) {
            char error_message[30] = "An error has occurred\n";
            write(STDERR_FILENO, error_message, strlen(error_message));
        }
        // redirect stderr to stdout
        if (dup2(fd, STDOUT_FILENO) == -1 || dup2(fd, STDERR_FILENO) == -1) {
            char error_message[30] = "An error has occurred\n";
            write(STDERR_FILENO, error_message, strlen(error_message));
        }
        // check with built-in first
        if (useBuiltInCmds(argsCounter, args) == 0) {
            // did not execute with built-in cmd
            execvArgs(args);
        }
        // reverse redirection after finish
        dup2(orig_stderr_fd, STDERR_FILENO);
        dup2(orig_stdout_fd, STDOUT_FILENO);
        // close file
        close(fd);
    }
    else if (redirection_code == 0) { // no redirection
        // check with built-in first
        if (useBuiltInCmds(argsCounter, args) == 0) {
            // did not execute with built-in cmd
            execvArgs(args);
        }
    }
    else { // errors with cmd
        char error_message[30] = "An error has occurred\n";
        write(STDERR_FILENO, error_message, strlen(error_message));
    }
}

// this function executes when the user does not put
// any argument after ./wish, meaning they use stdin
void takeStdInput()
{
    while(1) {
        // print shell prompt to terminal
        printf("wish> ");
        // for getline()
        char *line = NULL; // buffer to store each line read
        size_t len = 0; // length of the line read
        ssize_t read; // number of characters read by getline

        // take input line by line
        while ((read = getline(&line, &len, stdin)) != -1) {
            // remove newline character from command string
            if (line[strlen(line) - 1] == '\n') {
                line[strlen(line) - 1] = '\0';
            }
            break;
        }
        // check for empty string
        if (strlen(line) == 0) {
            // Free the buffers & reset the line pointer
            // in case of empty input command
            free(line);
            line = NULL;
            len = 0;
            continue;
        }
        // string found, set up variables
        size_t strLength = strlen(line);
        char filename[strLength];
        char *parallelStr[strLength];

        // check for parallel cmds
        int parallelCmdsNum = findParallelCmds(line, parallelStr);
        if (parallelCmdsNum) {
            // ptr to a list of several lists of arguments
            char **parallelArgs[strLength/2+1];
            for (int j = 0; j < parallelCmdsNum; j++) {
                parallelArgs[j] = (char **)malloc(strLength * sizeof(char *));
                execMultipleCmd(parallelStr[j], filename, parallelArgs[j]);
            }
            for (int j = 0; j < parallelCmdsNum; j++) {
                wait(NULL);
            }
            for (int j = 0; j < parallelCmdsNum; j++) {
                free(parallelArgs[j]);
                parallelArgs[j] = NULL;
            }
        } else {
            char *args[strLength/2+1]; // ptr to a list of arguments
            execSingleCmd(line, filename, args);
        }
        // Free the buffers & reset the line pointer
        free(line);
        line = NULL;
        len = 0;
    }
}

// function to execute given batch file
void takeBatchInput(char *inputFile) 
{
    FILE * fp = fopen(inputFile, "r");
    if (fp == NULL) {
        char error_message[30] = "An error has occurred\n";
        write(STDERR_FILENO, error_message, strlen(error_message));
        exit(1); // bad batch file err
    }
    char *line = NULL; // buffer to store each line read
    size_t len = 0; // length of the line read
    ssize_t read; // number of characters read by getline

    // take input line by line
    while ((read = getline(&line, &len, fp)) != -1) {
        // remove newline character from command string
        if (line[strlen(line) - 1] == '\n') {
            line[strlen(line) - 1] = '\0';
        }
        // check for empty string
        if (strlen(line) == 0) {
            // Free the buffers & reset the line pointer
            // in case of empty input command
            free(line);
            line = NULL;
            len = 0;
            continue;
        }
        // string found, set up variables
        size_t strLength = strlen(line);
        char filename[strLength]; // store filename of output
        char *parallelStr[strLength]; // store parallel cmds
        
        // check for parallel cmds
        int parallelCmdsNum = findParallelCmds(line, parallelStr);
        if (parallelCmdsNum) {
            // ptr to a list of several lists of arguments
            char **parallelArgs[strLength/2+1];
            for (int j = 0; j < parallelCmdsNum; j++) {
                parallelArgs[j] = (char **)malloc(strLength * sizeof(char *));
                execMultipleCmd(parallelStr[j], filename, parallelArgs[j]);
            }
            for (int j = 0; j < parallelCmdsNum; j++) {
                wait(NULL);
            }
            for (int j = 0; j < parallelCmdsNum; j++) {
                free(parallelArgs[j]);
                parallelArgs[j] = NULL;
            }
        }
        else {
            char *args[strLength/2+1]; // ptr to a list of arguments
            execSingleCmd(line, filename, args);
        }
    }
    // free the buffers & reset the line pointer and size
    free(line);
    line = NULL;
    len = 0;
    // close input file
    fclose(fp);
}

int main(int argc, char *argv[]) 
{   
    setenv("PATH", "/bin", 1); // initialize shell path to default

    // If given more than ./wish and a file
    if (argc > 2) {
        char error_message[30] = "An error has occurred\n";
        write(STDERR_FILENO, error_message, strlen(error_message));
        exit(1);
    }
    // If given just ./wish -> prompt input
    else if (argc == 1) {
        takeStdInput();
    }
    // If given ./wish and a batch file -> read and execute commands in file
    else {
        takeBatchInput(argv[1]);
    }
    return 0;
}