diff --git a/Code/.hist_list.txt b/Code/.hist_list.txt new file mode 100644 index 0000000..e69de29 diff --git a/Code/Task 1/main.c b/Code/Task 1/main.c deleted file mode 100644 index 96f43e0..0000000 --- a/Code/Task 1/main.c +++ /dev/null @@ -1,110 +0,0 @@ -#include -#include -#include - -#include "main.h" - -void tokenizeInput(char *inp, char **out) { - int i = 0; // Pointer counter - - // Can be 1 big string, but easier to see the delimiters this way - char delims[] = {' ', '\t', '|', '<', '>', '&', ';'}; - - // Get 1st token - char *token = strtok(inp, delims); - - // Whilst there are tokens - while(token != NULL) { - out[i] = token; // Store token in string - i++; - - token = strtok(NULL, delims); // Get next token - } - - out[i] = NULL; // Null terminate -} - -void removeTrailingWhitespaces(char *string) { - int i = 0, j = 0; - - // Skip leading whitespaces - while(string[i] == ' ' || - string[i] == '\t' || - string[i] == '|' || - string[i] == '<' || - string[i] == '>' || - string[i] == '&' || - string[i] == ';') i++; - - - if(string[i+1] == '\0') { - printf("Empty String!\n"); - return; - } - - while((string[j++] = string[i++])); // Shift chars over - - // Remove trailing newline on the input string - string[strlen(string)-1] = '\0'; -} - -void exitProgram() { - printf("\nExiting Shell...\n"); - exit(0); -} - -void prompt() { - char input[1024]; // Capture user input - char *tokens[512]; // Truncate string to right size in case of chars exceeding limit - - printf("> "); // Display prompt - - // Get user input & store it - char *detected_input = fgets(input, sizeof input, stdin); - - // Check for EOF (CTRL + D on Linux, CTRL + C on Windows) - if(detected_input == NULL) { - exitProgram(); - } - - // If no EOF, check user input string - else { - // Check to see if we exceed the character limit, if we do print message & return - if(strlen(input) > 512) { - printf("Too many characters! (> 512)\n"); - return; - } - - // Empty string? Return - else if(input[0] == '\n') { - return; - } - - // No issues with the initial input, continue to processing - else { - removeTrailingWhitespaces(input); - tokenizeInput(input, tokens); - - // Print out tokens to console - for(int i = 0; tokens[i] != NULL; i++) { - printf("%d => \"%s\"\n", i, tokens[i]); - } - - // if the 1st token is the exit command - if(strcmp(tokens[0], "exit") == 0) { - exitProgram(); - } - } - } -} - -// ? MAIN LOOP - -int main(void) { - // Infinite loop until exit - while(1) { - prompt(); - }; - - return 0; -} \ No newline at end of file diff --git a/Code/Task 1/main.h b/Code/Task 1/main.h deleted file mode 100644 index ffeadc3..0000000 --- a/Code/Task 1/main.h +++ /dev/null @@ -1,16 +0,0 @@ -/** @brief Take a string, tokenize it & return it - * @param inp The string to tokenize - * @param out The string to return through -*/ -void tokenizeInput(char *inp, char **out); - -/** @brief Remove all trailing illegal/whitespace characters from a string - * @param string The string to remove the illegal/whitespace characters from -*/ -void removeTrailingWhitespaces(char *string); - -/** @brief Exit the shell */ -void exitProgram(); - -/** @brief Display the shell prompt & process user input */ -void prompt(); \ No newline at end of file diff --git a/Code/Task 2/helper.c b/Code/Task 2/helper.c deleted file mode 100644 index e700796..0000000 --- a/Code/Task 2/helper.c +++ /dev/null @@ -1,100 +0,0 @@ -#include -#include -#include - -#include "external.h" - -void tokenizeInput(char *inp, char **out) { - int i = 0; // Pointer counter - - // Can be 1 big string, but easier to see the delimiters this way - char delims[] = {' ', '\t', '|', '<', '>', '&', ';'}; - - // Get 1st token - char *token = strtok(inp, delims); - - // Whilst there are tokens - while(token != NULL) { - out[i] = token; // Store token in string - i++; - - token = strtok(NULL, delims); // Get next token - } - - out[i] = NULL; // Null terminate -} - -void removeTrailingWhitespaces(char *string) { - int i = 0, j = 0; - - // Skip leading whitespaces - while(string[i] == ' ' || - string[i] == '\t' || - string[i] == '|' || - string[i] == '<' || - string[i] == '>' || - string[i] == '&' || - string[i] == ';') i++; - - - if(string[i+1] == '\0') { - printf("Empty String!\n"); - return; - } - - while((string[j++] = string[i++])); // Shift chars over - - // Remove trailing newline on the input string - string[strlen(string)-1] = '\0'; -} - -void exitProgram() { - printf("\nExiting Shell...\n"); - exit(0); -} - -void prompt() { - char input[1024]; // Capture user input - char *tokens[512]; // Truncate string to right size in case of chars exceeding limit - - printf("> "); // Display prompt - - // Get user input & store it - char *detected_input = fgets(input, sizeof input, stdin); - - // Check for EOF (CTRL + D on Linux, CTRL + C on Windows) - if(detected_input == NULL) { - exitProgram(); - } - - // If no EOF, check user input string - else { - // Check to see if we exceed the character limit, if we do print message & return - if(strlen(input) > 512) { - printf("Too many characters! (> 512)\n"); - return; - } - - // Empty string? Return - else if(input[0] == '\n') { - return; - } - - // No issues with the initial input, continue to processing - else { - removeTrailingWhitespaces(input); - tokenizeInput(input, tokens); - - // Print out tokens to console - for(int i = 0; tokens[i] != NULL; i++) { - printf("%d => \"%s\"\n", i, tokens[i]); - } - - // if the 1st token is the exit command - if(strcmp(tokens[0], "exit") == 0) { - exitProgram(); - } - } - } -} - diff --git a/Code/Task 2/helper.h b/Code/Task 2/helper.h deleted file mode 100644 index ffeadc3..0000000 --- a/Code/Task 2/helper.h +++ /dev/null @@ -1,16 +0,0 @@ -/** @brief Take a string, tokenize it & return it - * @param inp The string to tokenize - * @param out The string to return through -*/ -void tokenizeInput(char *inp, char **out); - -/** @brief Remove all trailing illegal/whitespace characters from a string - * @param string The string to remove the illegal/whitespace characters from -*/ -void removeTrailingWhitespaces(char *string); - -/** @brief Exit the shell */ -void exitProgram(); - -/** @brief Display the shell prompt & process user input */ -void prompt(); \ No newline at end of file diff --git a/Code/Task 2/main.c b/Code/Task 2/main.c deleted file mode 100644 index 3810c15..0000000 --- a/Code/Task 2/main.c +++ /dev/null @@ -1,21 +0,0 @@ -#include - -#include "helper.h" - -// ? IMPORTANT INFORMATION - TASK 2 -// ? STEP 1 - Modify existing code to sort tokens into an array of Strings (max array size of 50) -// ? STEP 2 - fork(), exec() & wait() functions -// ? STEP 2.1 - Figure out how to execute fork() & make the parent process wait() -// ? STEP 2.2 - Figure out how to call exec() on external programs through C -// ? STEP 2.3 - Make sure exec() is only searching through PATH variables(?) - -// MAIN LOOP - -int main(void) { - // Infinite loop until exit - while(1) { - prompt(); - }; - - return 0; -} \ No newline at end of file diff --git a/Code/main.c b/Code/main.c new file mode 100644 index 0000000..79515df --- /dev/null +++ b/Code/main.c @@ -0,0 +1,86 @@ +#include +#include +#include +#include + +#include "utils/userinput.h" +#include "utils/history.h" +#include "utils/commands.h" + +char original_path[1024]; + +/** @brief Setup the initial values for the shell */ +void setupShell(void) { + char *path = getenv("PATH"); + + if(path != NULL) { + strcpy(original_path, path); + } + + char *home = getenv("HOME"); + + if(home != NULL) { + if(chdir(home) != 0) { + printf("Error: Could not change to HOME directory\n"); + } + } + + char cwd[1024]; + + if(getcwd(cwd, sizeof(cwd)) != NULL) { + printf("Current working directory: %s\n", cwd); + } + + // TASK 6 GOES HERE + // Read saved history from file + // History is loaded using getCommandHistory() in main +} + +// MAIN LOOP + +/** @brief Display the shell prompt & process user input */ +int main(void) { + // Set PATH initially + setupShell(); + + // Get command history + history = getCommandHistory(); // Makes use of history global variable + + char input[1024]; + char *tokens[512]; + + while(1) { + // Print prompt + printf("> "); + + // Get & check user input for NULL + if(fgets(input, sizeof(input), stdin) == NULL) { + break; // EOF (Ctrl + D OR Ctrl + D) + } + + // Input validation + if(strlen(input) > 512 || input[0] == '\n') { + printf("Error occurred with the input\n"); + break; + } + + // Duplicate input as tokenizeInput irreversibly changes input when used + char *dupe_input = malloc(strlen(input) + 1); + strcpy(dupe_input, input); + + // Tokenize input String into tokens + tokenizeInput(input, tokens); + + // Check for NULL command name + if(tokens[0] == NULL) { + printf("Command is NULL"); + break; + } + + // Check the tokens against known commands + checkForCommands(tokens, history, dupe_input); + free(dupe_input); // Free duplicated input upon completion + } + + return 0; +} \ No newline at end of file diff --git a/Code/utils/commands.c b/Code/utils/commands.c new file mode 100644 index 0000000..1f1382b --- /dev/null +++ b/Code/utils/commands.c @@ -0,0 +1,93 @@ +#include +#include +#include + +#include "commands.h" +#include "history.h" // Allows us to use the global variable history + +extern char original_path[1024]; + +void getPath(void) { + // Get the current path & print it, if there is an error also print that + char *path = getenv("PATH"); + + printf(path ? "Current PATH: %s\n" : "Error: Could not get PATH\n", path); +} + +void setPath(char **tokens) { + // If there is no path argument + if(tokens[1] == NULL) { + printf("Error: setpath requires a path argument\n"); + return; + } + + // If we fail to set the path + if(setenv("PATH", tokens[1], 1) != 0) { + printf("Error: Could not set PATH\n"); + } + + // Otherwise, we assume it has worked + else { + printf("PATH set to: %s\n", tokens[1]); + } +} + +void changeDir(char **tokens) { + // If there is a path after the command + if(tokens[1] != NULL) { + // If we change directories successfully + if(chdir(tokens[1]) == 0) { + printf("Changed to -> %s!\n", tokens[1]); + } + + // If something goes wrong when changing + else { + printf("Error: Cannot find directory: \"%s\"\n", tokens[1]); + } + } + + // If there is no path after the command, just go back 1 directory + else { + chdir("../"); + } +} + +void exitProgram(void) { + printf("\nRestoring original PATH...\n"); + + //sets cwd to home + char *home = getenv("HOME"); + + if(home != NULL) { + if(chdir(home) != 0) { + printf("Error: Could not change to HOME directory\n"); + } + } + + // Save the history to the .hist_list file, clear history, then free the memory history was using + // Uses the global variable history + if (history != NULL){ + saveHistory(history); + clearCommandHistory(history); + free(history); + history = NULL; + } + + // Set the path to the original one before the program started + + setenv("PATH", original_path, 1); + + // if (setenv("PATH", original_path, 1) == 0) { + // printf("Original PATH restored: %s\n", original_path); + // } + + // If we can't, send an error msg + // else { + // printf("Error: Could not restore original PATH\n"); + // } + + // Exit shell + + printf("Exiting Shell...\n"); + exit(0); +} diff --git a/Code/utils/commands.h b/Code/utils/commands.h new file mode 100644 index 0000000..17b1c7e --- /dev/null +++ b/Code/utils/commands.h @@ -0,0 +1,22 @@ +#ifndef COMMANDS_H +#define COMMANDS_H + +extern char original_path[1024]; + +/** @brief Get the current working directory */ +void getPath(void); + +/** @brief Set the current working directory + * @param tokens The tokenized command String +*/ +void setPath(char **tokens); + +/** @brief Change the working directory + * @param tokens The tokenized command String +*/ +void changeDir(char **tokens); + +/** @brief Exit the shell */ +void exitProgram(void); + +#endif \ No newline at end of file diff --git a/Code/utils/history.c b/Code/utils/history.c new file mode 100644 index 0000000..b38f450 --- /dev/null +++ b/Code/utils/history.c @@ -0,0 +1,140 @@ +#include +#include +#include + +#include "history.h" + +struct CommandHistory *history; //Made history a global variable so it can be used by main.c and commands.c + +struct CommandHistory *getCommandHistory(void) { + + history = malloc(sizeof(struct CommandHistory)); + + if(history == NULL) { + fprintf(stderr, "Failed to allocate memory for history.\n"); + exit(EXIT_FAILURE); + } + + /*Opens .hist_list.txt and if it is there, loads the history. + If the .hist_list.txt file is not there, initialise an empty history*/ + FILE *histFile; + histFile = fopen(".hist_list.txt", "r"); + if(histFile != NULL) { + loadHistory(history); + fclose(histFile); + return history; + } + + else { + history -> count = 0; + history -> index = 0; + + for(int i = 0; i < MAX_HISTORY; i++) { + history -> commands[i] = NULL; + } + } + + return history; +} + +void addHistory(struct CommandHistory *hist, char *input) { + // Free oldest command if its there + if(hist -> commands[hist -> index] != NULL) { + free(hist -> commands[hist -> index]); + } + + // Store new command + hist -> commands[hist -> index] = strdup(input); + + // Move index forward + hist -> index = (hist -> index + 1) % MAX_HISTORY; + + // If haven't filled up history, increment count + if(hist -> count < MAX_HISTORY) { + hist -> count++; + } +} + +void clearCommandHistory(struct CommandHistory *hist) { + for(int i = 0; i < hist -> count; i++) { + free(hist -> commands[i]); + } + + hist -> count = 0; + hist -> index = 0; +} + +char *executeHistory(struct CommandHistory *hist, int index) { + // Check if index is within the bounds of the array + if(index < 1 || index > hist -> count) { + printf("Error - Invalid history index.\n"); + return NULL; + } + + // Make memory for tokens + char *input = malloc(sizeof(hist -> commands[index-1])); + strcpy(input, hist -> commands[index - 1]); + + if(input == NULL) { + printf("Error - Couldn't find command in history.\n"); + return NULL; + } + + return input; +} + +void printHistory(struct CommandHistory *hist) { + for(int i = 0; i < hist -> count; i++) { + printf("%d %s", i, hist -> commands[i]); + } + + printf("Total Count: %d\n", hist -> count); +} + +// Save the history to the .hist_list file. +// This function is called from commands.c in the exitProgram() function +/* At present this is having issues where it saves .hist_list.txt in the current directory the shell is in. + As a result, it does not load the history correctly (because it can't find it) if you have succesfully changed directory using our shell and dont return to home before exiting.*/ +void saveHistory(struct CommandHistory *hist) { + FILE *histFile; + histFile = fopen(".hist_list.txt", "w"); // Opens the file in write mode, overwriting anything previous + + if(histFile == NULL) { // If the file can't be opened, print an error message + printf("Error - History file not found or could not be opened.\n"); + return; + } + + fprintf(histFile, "%d\n", hist->index); // Saves the index value + fprintf(histFile, "%d\n", hist->count); // Saves the count value + + for(int i = 0; i < hist->count; ++i){ // loops through the history and saves each command + fprintf(histFile, "%s", hist->commands[i]); + } + + fclose(histFile); +} + +// Load the history from the .hist_list file +void loadHistory(struct CommandHistory *hist) { + FILE *histFile; + histFile = fopen(".hist_list.txt", "r"); // Opens the file in read mode + + if(histFile == NULL) { // Again if the file can't be opened, print an error message + printf("Error - History file not found or could not be opened.\n"); + return; + } + + fscanf(histFile, "%d\n", &hist->index); // Reads the index value + fscanf(histFile, "%d\n", &hist->count); // Reads the count value + + for(int i = 0; i < hist->count; ++i){ // loops through the history + hist->commands[i] = malloc(1024); // Allocates memory for each command + if(hist->commands[i] == NULL) { + printf("Error - Could not allocate memory for history command.\n"); + return; + } + fgets(hist->commands[i], 1024, histFile); // Reads and loads each command into history + } + + fclose(histFile); +} \ No newline at end of file diff --git a/Code/utils/history.h b/Code/utils/history.h new file mode 100644 index 0000000..371f5aa --- /dev/null +++ b/Code/utils/history.h @@ -0,0 +1,36 @@ +#ifndef HISTORY_H +#define HISTORY_H + +#define MAX_HISTORY 20 + +extern struct CommandHistory *history; // The global variable for history + +struct CommandHistory { + char *commands[MAX_HISTORY]; // List of commands + int index; // Insertion point + int count; // Total commands in history +}; + +/** @brief Checks for prior command history, if none return a new struct + * @return A CommandHistory struct +*/ +struct CommandHistory *getCommandHistory(void); + +/** @brief Add to the saved history */ +void addHistory(struct CommandHistory *hist, char *input); + +/** @brief Clear out the saved history & free all memory */ +void clearCommandHistory(struct CommandHistory *hist); + +/** @brief Get & return a previous command string */ +char *executeHistory(struct CommandHistory *hist, int index); + +void printHistory(struct CommandHistory *hist); + +/** @brief Save the history to the .hist_list file */ +void saveHistory(struct CommandHistory *hist); + +/** @brief Load the history from the .hist_list file */ +void loadHistory(struct CommandHistory *hist); + +#endif \ No newline at end of file diff --git a/Code/utils/userinput.c b/Code/utils/userinput.c new file mode 100644 index 0000000..ae3099d --- /dev/null +++ b/Code/utils/userinput.c @@ -0,0 +1,181 @@ +#include +#include +#include +#include +#include +#include + +#include "commands.h" +#include "userinput.h" +#include "history.h" + +struct Command { + const char *command; + void (*function)(char **tokens); +}; + +struct Command commands[] = { + {"exit", (void (*)(char **))exitProgram}, + {"getpath", (void (*)(char **))getPath}, + {"setpath", (void (*)(char **))setPath}, + {"cd", (void (*)(char **))changeDir}, + {NULL, NULL} // Marks the end of the array +}; + +void executeSystemCommand(char **tokens) { + // Make fork + pid_t pid = fork(); + + // Error when forking + if (pid < 0) { + printf("Error: Forking failed.\n"); + exit(EXIT_FAILURE); + } + + // Child process + if(pid == 0) { + // Ignore the default "command not found" error message entirely + if(execvp(tokens[0], tokens) == -1) { + // Simply exit the child without printing anything + exit(EXIT_FAILURE); + } + } + + // Parent process + else { + wait(NULL); + } +} + +int executeCommand(char **tokens) { + int i = 0; + + while(commands[i].command != NULL) { + if(strcmp(tokens[0], commands[i].command) == 0) { + commands[i].function(tokens); + return 1; + } + + i++; + } + + return 0; +} + +void tokenizeInput(char *inp, char **out) { + // Counter + int i = 0; + + // Get the first token in the String + char *token = strtok(inp, " \t|<>&;\n"); + + // While we have tokens + while(token != NULL) { + // Set the token to the array index + out[i] = token; + + // Increment counter + i++; + + // Get new token, if any, from String + token = strtok(NULL, " \t|<>&;\n"); + } + + out[i] = NULL; +} + +void checkForCommands(char **tokens, struct CommandHistory *hist, char *input) { + // Do command at index x + if(tokens[0][0] == '!' && isdigit(tokens[0][1])) { + // Get user inputted index + int history_num = atoi(&tokens[0][1]); // Get history index + + // Get tokens in history + char *hist_input = executeHistory(hist, history_num); + + if(hist_input == NULL) { + printf("Error when retrieving history command\n"); + return; + } + + char *hist_tokens[512]; + + // Tokenize history command String + tokenizeInput(hist_input, hist_tokens); + + // Execute command with history tokens + int executed = executeCommand(hist_tokens); + + if(!executed) { + executeSystemCommand(hist_tokens); + } + + free(hist_input); + return; + } + + // Do command at index x (with dash) + else if(tokens[0][0] == '!' && tokens[0][1] == '-' && isdigit(tokens[0][2])) { + int history_num = atoi(&tokens[0][2]); // Get history index + + char *hist_input = executeHistory(hist, history_num); + + if(hist_input == NULL) { + printf("Error when retrieving history command\n"); + return; + } + + char *hist_tokens[512]; + + tokenizeInput(hist_input, hist_tokens); + + int executed = executeCommand(hist_tokens); + + if(!executed) { + executeSystemCommand(hist_tokens); + } + + free(hist_input); + return; + } + + // Do last command only + else if(tokens[0][0] == '!' && tokens[0][1] == '!') { + char *hist_input = executeHistory(hist, 0); + + if(hist_input == NULL) { + printf("Error when retrieving history command\n"); + return; + } + + char *hist_tokens[512]; + + tokenizeInput(hist_input, hist_tokens); + + int executed = executeCommand(hist_tokens); + + if(!executed) { + executeSystemCommand(hist_tokens); + } + + free(hist_input); + return; + } + + else if(strcmp(tokens[0], "history") == 0) { + printHistory(hist); + } + + // Its not a history call, so execute normal command + else { + // Execute regular command + int executed = executeCommand(tokens); + + addHistory(hist, input); + + // If there is no reference to the command in our struct, assume its a system command + if(!executed) { + executeSystemCommand(tokens); + } + } +} \ No newline at end of file diff --git a/Code/utils/userinput.h b/Code/utils/userinput.h new file mode 100644 index 0000000..3117e40 --- /dev/null +++ b/Code/utils/userinput.h @@ -0,0 +1,31 @@ +#ifndef USERINPUT_H +#define USERINPUT_H + +#ifndef COMMANDS_H +#define COMMANDS_H + +#include "history.h" + +/** @brief Execute a system command + * @param tokens The tokenized command String +*/ +void executeSystemCommand(char **tokens); + +/** @brief Execute a command + * @param tokens The tokenzied command String +*/ +int executeCommand(char **tokens); + +/** @brief Take a string, tokenize it & return it + * @param inp The string to tokenize + * @param out The string to return through +*/ +void tokenizeInput(char *inp, char **out); + +/** @brief Take the tokens & check for specific commands + * @param tokens The tokens to check +*/ +void checkForCommands(char **tokens, struct CommandHistory *hist, char *input); + +#endif +#endif \ No newline at end of file diff --git a/Documentation/BugFixes.md b/Documentation/BugFixes.md deleted file mode 100644 index 40823a3..0000000 --- a/Documentation/BugFixes.md +++ /dev/null @@ -1,15 +0,0 @@ -# Bug Fixes - -This file keeps logs on what bugs were encountered during development and how we fixed them. -Please keep a running log when developing to ensure comprehensive documentation is retained. - ---- - -| Bug | Issue | Solution(s) | -| -------------------------------------- | ----- | ----------- | -| "exit" command not working as intended | Last token in input string always contained a newline character | Preemptively remove the newline from the string by setting the last character to a null terminator before tokenization | -| `ctrl + D` not working | `ctrl + d` keyboard command does not exit the shell program as expected | added new variable to hold `fgets()` output & check for EOF/NULL, exit if detected. Otherwise, continue on with the program as normal | -| String of delimiters crashes shell | Having a string of only delimiters crashes the shell somehow | Added a check for an empty string before doing anything else to the string | - - - diff --git a/Documentation/Task Specification.pdf b/Documentation/Task Specification.pdf deleted file mode 100644 index 51b80f7..0000000 Binary files a/Documentation/Task Specification.pdf and /dev/null differ diff --git a/README.md b/README.md deleted file mode 100644 index 50089ce..0000000 --- a/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# CS210-Shell-Project - -CS210 Simple Shell Project for Semester 2 - -## Repo Structure - -- Code - - This directory contains all the current files for the shell project on the branch - -- Documentation - - This directory contains all the files for documentation of the project. - - Task Specification >> The original PDF of the task, all stages & testing parameters - - Bug Fixes >> A table for all the bugs encountered during development, how they affected the program & how we fixed it - -## HOW TO RUN - -- Open your terminal window -- Navigate to the directory containing the c files, e.g. `cd tasks\task2` -- Run gcc & compile the c files into an executable, e.g. `gcc main.c` or `gcc helper.c main.c` for more than 1 file -- Run the compiled executable, e.g. `a.out` or `a.exe`