diff --git a/4-ShellP2/.dshlib.c.swp b/4-ShellP2/.dshlib.c.swp new file mode 100644 index 0000000000000000000000000000000000000000..7b1c66f6e023becd69f71d5ed390e4de36e131a8 Binary files /dev/null and b/4-ShellP2/.dshlib.c.swp differ diff --git a/4-ShellP2/bats/assignment_test.sh b/4-ShellP2/bats/assignment_test.sh new file mode 100644 index 0000000000000000000000000000000000000000..30b12ec1661cd179396f1e2ee3fd8ef39de7eb96 --- /dev/null +++ b/4-ShellP2/bats/assignment_test.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bats + +############################ DO NOT EDIT THIS FILE ##################################### +# File: assignement_tests.sh +# +# DO NOT EDIT THIS FILE +# +# Add/Edit Student tests in student_tests.sh +# +# All tests in this file must pass - it is used as part of grading! +######################################################################################## + +@test "Change directory" { + current=$(pwd) + + cd /tmp + mkdir -p dsh-test + + run "${current}/dsh" <<EOF +cd dsh-test +pwd +EOF + + # Strip all whitespace (spaces, tabs, newlines) from the output + stripped_output=$(echo "$output" | tr -d '[:space:]') + + # Expected output with all whitespace removed for easier matching + expected_output="/tmp/dsh-testdsh2>dsh2>dsh2>cmdloopreturned0" + + # These echo commands will help with debugging and will only print + #if the test fails + echo "Captured stdout:" + echo "Output: $output" + echo "Exit Status: $status" + echo "${stripped_output} -> ${expected_output}" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] + + # Assertions + [ "$status" -eq 0 ] +} + +@test "Change directory - no args" { + current=$(pwd) + + cd /tmp + mkdir -p dsh-test + + run "${current}/dsh" <<EOF +cd +pwd +EOF + + # Strip all whitespace (spaces, tabs, newlines) from the output + stripped_output=$(echo "$output" | tr -d '[:space:]') + + # Expected output with all whitespace removed for easier matching + expected_output="/tmpdsh2>dsh2>dsh2>cmdloopreturned0" + + # These echo commands will help with debugging and will only print + #if the test fails + echo "Captured stdout:" + echo "Output: $output" + echo "Exit Status: $status" + echo "${stripped_output} -> ${expected_output}" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] + + # Assertions + [ "$status" -eq 0 ] +} + + +@test "Which which ... which?" { + run "./dsh" <<EOF +which which +EOF + + # Strip all whitespace (spaces, tabs, newlines) from the output + stripped_output=$(echo "$output" | tr -d '[:space:]') + + # Expected output with all whitespace removed for easier matching + expected_output="/usr/bin/whichdsh2>dsh2>cmdloopreturned0" + + # These echo commands will help with debugging and will only print + #if the test fails + echo "Captured stdout:" + echo "Output: $output" + echo "Exit Status: $status" + echo "${stripped_output} -> ${expected_output}" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] +} + +@test "It handles quoted spaces" { + run "./dsh" <<EOF + echo " hello world " +EOF + + # Strip all whitespace (spaces, tabs, newlines) from the output + stripped_output=$(echo "$output" | tr -d '\t\n\r\f\v') + + # Expected output with all whitespace removed for easier matching + expected_output=" hello world dsh2> dsh2> cmd loop returned 0" + + # These echo commands will help with debugging and will only print + #if the test fails + echo "Captured stdout:" + echo "Output: $output" + echo "Exit Status: $status" + echo "${stripped_output} -> ${expected_output}" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] +} diff --git a/4-ShellP2/bats/student_test.sh b/4-ShellP2/bats/student_test.sh new file mode 100644 index 0000000000000000000000000000000000000000..638bc341446f7580a80c2aff52971b8023407ea8 --- /dev/null +++ b/4-ShellP2/bats/student_test.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bats + +# File: student_tests.sh +# +# Create your unit tests suit in this file + +@test "Example: check ls runs without errors" { + run ./dsh <<EOF +ls +EOF + + # Assertions + [ "$status" -eq 0 ] +} diff --git a/4-ShellP2/dragon.c b/4-ShellP2/dragon.c new file mode 100644 index 0000000000000000000000000000000000000000..b5ac87f2c407c82436f71ae95472bccb5ff0b9be --- /dev/null +++ b/4-ShellP2/dragon.c @@ -0,0 +1,37 @@ +#include <stdio.h> +#include <stdlib.h> +#include "dshlib.h" + +// EXTRA CREDIT - print the drexel dragon from the readme.md +extern void print_dragon(){ + // TODO implement + FILE *dragon = fopen("dragon.txt", "r"); + if (dragon == NULL) { + return; + } + + char *s = NULL; + size_t nbyte; + ssize_t nchar; + + while (1) { + nchar = getline(&s, &nbyte, dragon); + if (nchar == -1) { // end of file reached + break; + } + if (nchar == 0) { + continue; + } + if (s == NULL) { // out of memory + exit(1); + } + if (s[nchar - 1] == '\n') { + s[nchar - 1] = '\0'; // remove newline + nchar--; // newline removed + } + + printf("%s\n", s); + } + free(s); + fclose(dragon); +} diff --git a/4-ShellP2/dragon.txt b/4-ShellP2/dragon.txt new file mode 100644 index 0000000000000000000000000000000000000000..a9177fd41bb14d2fa0fd439742e74d3f61f60d2d --- /dev/null +++ b/4-ShellP2/dragon.txt @@ -0,0 +1,38 @@ + @%%%% + %%%%%% + %%%%%% + % %%%%%%% @ + %%%%%%%%%% %%%%%%% + %%%%%%% %%%%@ %%%%%%%%%%%%@ %%%%%% @%%%% + %%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%% + %%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%% %%%%%%%%%%%%%%% + %%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%% %%% + %%%%%%%%%%%%%%%%%%%%%%%%%%%%@ @%%%%%%%%%%%%%%%%%% %% + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%% + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%@ + %%%%%%%%@ %%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%% %% + %%%%%%%%%%%%% %%@%%%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%%% @% + %%%%%%%%%% %%% %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%% + %%%%%%%%% % %%%%%%%%%%%%% %%%%%%%%%%%%@%%%%%%%%%%% +%%%%%%%%%@ % %%%%%%%%%%%%% @%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%@ %%@%%%%%%%%%%%% @%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%@ %%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%%%%%%%%%% %%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% +%%%%%%%%%@ @%%%%%%%%%%%%%% %%%%%%%%%%%%@ %%%% %%%%%%%%%%%%%%%%% %%%%%%%% +%%%%%%%%%% %%%%%%%%%%%%%%%%% %%%%%%%%%%%%% %%%%%%%%%%%%%%%%%% %%%%%%%%% +%%%%%%%%%@%%@ %%%%%%%%%%%%%%%%@ %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%% %% + %%%%%%%%%% % %%%%%%%%%%%%%%@ %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%% %% + %%%%%%%%%%%% @ %%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% + %%%%%%%%%%%%% %% % %@ %%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% + %%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%% @%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%% + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% + @%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%% + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%% %%%%%%% + %%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%% @%%%%%%%%% + %%%%%%%%%%%%%%%%%%%% @%@% @%%%%%%%%%%%%%%%%%% %%% + %%%%%%%%%%%%%%% %%%%%%%%%% %%%%%%%%%%%%%%% % + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%% + %%%%%%%%%%%%%%%%%%%%%%%%%% %%%% %%% %%%%%%%%%% %%%@ + %%%%%%%%%%%%%%%%%%% %%%%%% %% %%%%%%%%%%%%%@ + %%%%%%%@ diff --git a/4-ShellP2/dsh b/4-ShellP2/dsh new file mode 100755 index 0000000000000000000000000000000000000000..bc71f23f06921448398c90d8c8ef9609de2dbfd4 Binary files /dev/null and b/4-ShellP2/dsh differ diff --git a/4-ShellP2/dsh_cli.c b/4-ShellP2/dsh_cli.c new file mode 100644 index 0000000000000000000000000000000000000000..ef8029b132addbe6e3428c7547bfc37796e15669 --- /dev/null +++ b/4-ShellP2/dsh_cli.c @@ -0,0 +1,13 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "dshlib.h" + +/* DO NOT EDIT + * main() logic moved to exec_local_cmd_loop() in dshlib.c +*/ +int main(){ + int rc = exec_local_cmd_loop(); + printf("cmd loop returned %d\n", rc); +} diff --git a/4-ShellP2/dshlib.c b/4-ShellP2/dshlib.c new file mode 100644 index 0000000000000000000000000000000000000000..c168f6dc0858734439c9fa1cbd0fd50290b4fcea --- /dev/null +++ b/4-ShellP2/dshlib.c @@ -0,0 +1,275 @@ +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <ctype.h> +#include <stdbool.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/wait.h> +#include <errno.h> +#include "dshlib.h" + +int numInstanceOf(char *str, const char c) { + int count = 0; + while (*str != '\0') { + if (*str == c) count++; + str++; + } + return count; +} + +int alloc_cmd_buff(cmd_buff_t *cmd_buff) { + if (cmd_buff == NULL) { + return ERR_MEMORY; + } + cmd_buff->argc = 0; + + cmd_buff->argv = (char**)malloc(CMD_ARGV_MAX * sizeof(char *)); + if (cmd_buff->argv == NULL) { + free(cmd_buff); + return ERR_MEMORY; + } + + for (int i = 0; i < CMD_ARGV_MAX; i++) { + cmd_buff->argv[i] = (char *)malloc(ARG_MAX); + if (cmd_buff->argv[i] == NULL) { + for (int j = 0; j < i; j++) { + free(cmd_buff->argv[j]); + } + free(cmd_buff->argv); + return ERR_MEMORY; + } + } + + cmd_buff->_cmd_buffer = (char *)malloc(SH_CMD_MAX); + if (cmd_buff->_cmd_buffer == NULL) { + free(cmd_buff->_cmd_buffer); + for (int i = 0; i < CMD_ARGV_MAX; i++) { + free(cmd_buff->argv[i]); + } + free(cmd_buff->argv); + return ERR_MEMORY; + } + + + return OK_EXIT; +} + +int free_cmd_buff(cmd_buff_t *cmd_buff) { + free(cmd_buff->_cmd_buffer); + + for (int i = 0; i < CMD_ARGV_MAX - 1; i++) free(cmd_buff->argv[i]); + free(cmd_buff); + return OK_EXIT; +} + +int clear_cmd_buff(cmd_buff_t *cmd_buff) { + cmd_buff->argc = 0; + free(cmd_buff->_cmd_buffer); + + for (int i = 0; i < CMD_ARGV_MAX; i++) cmd_buff->argv[i] = NULL; + return OK_EXIT; +} + +char* trim_whitespace(char *str) { + int start = 0; + while (isspace((unsigned char)str[start])) { + start++; + } + + int end = strlen(str) - 1; + while (end > start && isspace((unsigned char)str[end])) { + end--; + } + + int j = 0; + for (int i = start; i <= end; i++) { + str[j++] = str[i]; + } + str[j] = '\0'; + + return str; +} + +int build_cmd_buff(char *cmd_line, cmd_buff_t *cmd_buff) { + if ((int)strlen(cmd_line) > SH_CMD_MAX) return ERR_CMD_OR_ARGS_TOO_BIG; + + if ((int)strlen(cmd_line) == 0) return WARN_NO_CMDS; + + cmd_buff->_cmd_buffer = strdup(trim_whitespace(cmd_line)); + if (cmd_buff->_cmd_buffer == NULL) { + free(cmd_buff); + return ERR_MEMORY; + } + + char *token = cmd_buff->_cmd_buffer; + bool quotes = false; + char *p = NULL; + + while (*token) { + if (*token == DOUBLE_QUOTE_CHAR) { + quotes = !quotes; + if (quotes) p = token + 1; + else *token = '\0'; + } else if (!quotes && (*token == SPACE_CHAR || *token == '\t')) { + *token = '\0'; + + if (p != NULL) { + cmd_buff->argv[cmd_buff->argc++] = p; + p = NULL; + } + } else if (p == NULL) { + p = token; + } + token++; + } + + if (p != NULL) cmd_buff->argv[cmd_buff->argc++] = p; + + cmd_buff->argv[cmd_buff->argc] = NULL; + return OK_EXIT; +} +/* + * Implement your exec_local_cmd_loop function by building a loop that prompts the + * user for input. Use the SH_PROMPT constant from dshlib.h and then + * use fgets to accept user input. + * + * while(1){ + * printf("%s", SH_PROMPT); + * if (fgets(cmd_buff, ARG_MAX, stdin) == NULL){ + * printf("\n"); + * break; + * } + * //remove the trailing \n from cmd_buff + * cmd_buff[strcspn(cmd_buff,"\n")] = '\0'; + * + * //IMPLEMENT THE REST OF THE REQUIREMENTS + * } + * + * Also, use the constants in the dshlib.h in this code. + * SH_CMD_MAX maximum buffer size for user input + * EXIT_CMD constant that terminates the dsh program + * SH_PROMPT the shell prompt + * OK the command was parsed properly + * WARN_NO_CMDS the user command was empty + * ERR_TOO_MANY_COMMANDS too many pipes used + * ERR_MEMORY dynamic memory management failure + * + * errors returned + * OK No error + * ERR_MEMORY Dynamic memory management failure + * WARN_NO_CMDS No commands parsed + * ERR_TOO_MANY_COMMANDS too many pipes used + * + * console messages + * CMD_WARN_NO_CMD print on WARN_NO_CMDS + * CMD_ERR_PIPE_LIMIT print on ERR_TOO_MANY_COMMANDS + * CMD_ERR_EXECUTE print on execution failure of external command + * + * Standard Library Functions You Might Want To Consider Using (assignment 1+) + * malloc(), free(), strlen(), fgets(), strcspn(), printf() + * + * Standard Library Functions You Might Want To Consider Using (assignment 2+) + * fork(), execvp(), exit(), chdir() + */ +int exec_local_cmd_loop() +{ + char *cmd_buff = malloc(ARG_MAX * sizeof(char)); + int rc = 0; + cmd_buff_t *cmd = malloc(CMD_ARGV_MAX * sizeof(char *)); + + if ((rc = alloc_cmd_buff(cmd)) != OK_EXIT) exit(rc); + + // TODO IMPLEMENT MAIN LOOP + while(1){ + printf("%s", SH_PROMPT); + if (fgets(cmd_buff, ARG_MAX, stdin) == NULL){ + printf("\n"); + break; + } + //remove the trailing \n from cmd_buff + cmd_buff[strcspn(cmd_buff,"\n")] = '\0'; + + //IMPLEMENT THE REST OF THE REQUIREMENTS + if (strcmp(cmd_buff, EXIT_CMD) == 0) { + exit(rc); + } + + if (strcmp(cmd_buff, "rc") == 0) { + printf("%d\n", rc); + if ((rc = clear_cmd_buff(cmd)) != OK_EXIT) exit(rc); + continue; + } + + if (strcmp(cmd_buff, "\0") == 0) { + rc = WARN_NO_CMDS; + printf(CMD_WARN_NO_CMD); + //if ((rc = clear_cmd_buff(cmd)) != OK_EXIT) exit(rc); + continue; + } + + // TODO IMPLEMENT parsing input to cmd_buff_t *cmd_buff + if ((rc = build_cmd_buff(cmd_buff, cmd)) != OK_EXIT) { + exit(rc); + rc = 0; + } + /* + if (strcmp(cmd->argv[0], "echo") == 0) { + for (int i = 1; i < CMD_ARGV_MAX; i++) { + printf("%s ", cmd->argv[i]); + } + printf("\n"); + if ((rc = clear_cmd_buff(cmd)) != OK_EXIT) exit(rc); + continue; + }*/ + + // TODO IMPLEMENT if built-in command, execute builtin logic for exit, cd (extra credit: dragon) + if (strcmp(cmd_buff, "dragon") == 0) { + print_dragon(); + if ((rc = clear_cmd_buff(cmd)) != OK_EXIT) exit(rc); + rc = 0; + continue; + } + + // the cd command should chdir to the provided directory; if no directory is provided, do nothing + if (strcmp(cmd->argv[0], "cd") == 0) { + if (cmd->argc < 2) { + if ((rc = clear_cmd_buff(cmd)) != OK_EXIT) exit(rc); + rc = 0; + continue; + } + if (chdir(cmd->argv[1]) != 0) { + perror("chdir failed"); + rc = 1; + } + if ((rc = clear_cmd_buff(cmd)) != OK_EXIT) exit(rc); + rc = 0; + continue; + } + + // TODO IMPLEMENT if not built-in command, fork/exec as an external command + // for example, if the user input is "ls -l", you would fork/exec the command "ls" with the arg "-l" + pid_t pid = fork(); + if (pid < 0) { + printf("fork failed"); + if ((rc = clear_cmd_buff(cmd)) != OK_EXIT) exit(rc); + continue; + } else if (pid == 0) { + execvp(cmd->argv[0], cmd->argv); + perror(CMD_ERR_EXECUTE); + exit(1); + } else { + int status; + waitpid(pid, &status, 0); + if (WIFEXITED(status)) { + rc = WEXITSTATUS(status); + } else { + rc = 1; + } + } + if ((rc = clear_cmd_buff(cmd)) != OK_EXIT) exit(rc); + } + free(cmd_buff); + free_cmd_buff(cmd); + return OK; +} diff --git a/4-ShellP2/dshlib.h b/4-ShellP2/dshlib.h new file mode 100644 index 0000000000000000000000000000000000000000..df2f9a4d0061aee7f57b947ccba254178756c4b4 --- /dev/null +++ b/4-ShellP2/dshlib.h @@ -0,0 +1,85 @@ +#ifndef __DSHLIB_H__ + #define __DSHLIB_H__ + + +//Constants for command structure sizes +#define EXE_MAX 64 +#define ARG_MAX 256 +#define CMD_MAX 8 +#define CMD_ARGV_MAX (CMD_MAX + 1) +// Longest command that can be read from the shell +#define SH_CMD_MAX EXE_MAX + ARG_MAX + +typedef struct cmd_buff +{ + int argc; + char **argv; + char *_cmd_buffer; +} cmd_buff_t; + +/* WIP - Move to next assignment +#define N_ARG_MAX 15 //MAX number of args for a command +typedef struct command{ + char exe [EXE_MAX]; + char args[ARG_MAX]; + int argc; + char *argv[N_ARG_MAX + 1]; //last argv[LAST] must be \0 +}command_t; +*/ + + +//Special character #defines +#define SPACE_CHAR ' ' +#define SPACE_STRING " " +#define PIPE_CHAR '|' +#define PIPE_STRING "|" +#define DOUBLE_QUOTE_CHAR '"' + +#define SH_PROMPT "dsh2> " +#define EXIT_CMD "exit" + +//Standard Return Codes +#define OK 0 +#define WARN_NO_CMDS -1 +#define ERR_TOO_MANY_COMMANDS -2 +#define ERR_CMD_OR_ARGS_TOO_BIG -3 +#define ERR_CMD_ARGS_BAD -4 //for extra credit +#define ERR_MEMORY -5 +#define ERR_EXEC_CMD -6 +#define OK_EXIT -7 + +//prototypes +int alloc_cmd_buff(); +int free_cmd_buff(cmd_buff_t *); +int clear_cmd_buff(cmd_buff_t *); +int build_cmd_buff(char *, cmd_buff_t *); +void exeCD(char *path); +void execFork(char *); +extern void print_dragon(); + +//built in command stuff +typedef enum { + BI_CMD_EXIT, + BI_CMD_DRAGON, + BI_CMD_CD, + BI_NOT_BI, + BI_EXECUTED, + BI_RC, +} Built_In_Cmds; +Built_In_Cmds match_command(const char *input); +Built_In_Cmds exec_built_in_cmd(cmd_buff_t *cmd); + +//main execution context +int exec_local_cmd_loop(); +int exec_cmd(cmd_buff_t *cmd); + + + + +//output constants +#define CMD_OK_HEADER "PARSED COMMAND LINE - TOTAL COMMANDS %d\n" +#define CMD_WARN_NO_CMD "warning: no commands provided\n" +#define CMD_ERR_PIPE_LIMIT "error: piping limited to %d commands\n" +#define CMD_ERR_EXECUTE "Command not found in PATH\n" + +#endif diff --git a/4-ShellP2/makefile b/4-ShellP2/makefile new file mode 100644 index 0000000000000000000000000000000000000000..a079ef4946ce8578836606ceab3f553b305b1875 --- /dev/null +++ b/4-ShellP2/makefile @@ -0,0 +1,31 @@ +# Compiler settings +CC = gcc +CFLAGS = -Wall -Wextra -g + +# Target executable name +TARGET = dsh + +# Find all source and header files +SRCS = $(wildcard *.c) +HDRS = $(wildcard *.h) + +# Default target +all: $(TARGET) + +# Compile source to executable +$(TARGET): $(SRCS) $(HDRS) + $(CC) $(CFLAGS) -o $(TARGET) $(SRCS) + +# Clean up build files +clean: + rm -f $(TARGET) + +test: + bats $(wildcard ./bats/*.sh) + +valgrind: + echo "pwd\nexit" | valgrind --leak-check=full --show-leak-kinds=all --error-exitcode=1 ./$(TARGET) + echo "pwd\nexit" | valgrind --tool=helgrind --error-exitcode=1 ./$(TARGET) + +# Phony targets +.PHONY: all clean test