diff --git a/3-ShellP1/.dsh_cli.c.swp b/3-ShellP1/.dsh_cli.c.swp new file mode 100644 index 0000000000000000000000000000000000000000..bd2da2d883ffa99b5f61bda6b50285fee38b2780 Binary files /dev/null and b/3-ShellP1/.dsh_cli.c.swp differ diff --git a/3-ShellP1/dsh b/3-ShellP1/dsh new file mode 100755 index 0000000000000000000000000000000000000000..4439dfcd727ea2321e58bd7d1b48782c488683a7 Binary files /dev/null and b/3-ShellP1/dsh differ diff --git a/3-ShellP1/dsh_cli.c b/3-ShellP1/dsh_cli.c new file mode 100644 index 0000000000000000000000000000000000000000..e5a80e9654f45f74149d30c9d0cc7df6505bd2ef --- /dev/null +++ b/3-ShellP1/dsh_cli.c @@ -0,0 +1,107 @@ +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "dshlib.h" + +void printDragon() { + printf("[DRAGON for extra credit would print here]\n"); +} + +/* + * Implement your main 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. Since we want fgets to also handle + * end of file so we can run this headless for testing we need to check + * the return code of fgets. I have provided an example below of how + * to do this assuming you are storing user input inside of the cmd_buff + * variable. + * + * 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 + * + * Expected output: + * + * CMD_OK_HEADER if the command parses properly. You will + * follow this by the command details + * + * CMD_WARN_NO_CMD if the user entered a blank command + * CMD_ERR_PIPE_LIMIT if the user entered too many commands using + * the pipe feature, e.g., cmd1 | cmd2 | ... | + * + * See the provided test cases for output expectations. + */ +int main() +{ + char *cmd_buff = malloc(ARG_MAX * sizeof(char)); + int rc = OK; + command_list_t *clist = malloc(sizeof(command_list_t)); + clist->num = 0; + + 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, "dragon") == 0) { + printDragon(); + continue; + } + + if (strcmp(cmd_buff, "\0") == 0) { + rc = WARN_NO_CMDS; + printf(CMD_WARN_NO_CMD); + continue; + } + + rc = build_cmd_list(cmd_buff, clist); + + if (rc == ERR_TOO_MANY_COMMANDS) { + printf(CMD_ERR_PIPE_LIMIT, CMD_MAX); + continue; + } + + printf(CMD_OK_HEADER, clist->num); + + for (int i = 0; i < clist->num; i++) { + if (strlen(clist->commands[i].args) > 0) { + printf("<%d> %s [%s]\n", i + 1, clist->commands[i].exe, clist->commands[i].args); + } else { + printf("<%d> %s\n", i + 1, clist->commands[i].exe); + } + } + } + + free(cmd_buff); + free(clist); + return rc; +} + diff --git a/3-ShellP1/dshlib.c b/3-ShellP1/dshlib.c new file mode 100644 index 0000000000000000000000000000000000000000..4e3f473bae7df4287a178ce958816f1ba0f6e109 --- /dev/null +++ b/3-ShellP1/dshlib.c @@ -0,0 +1,94 @@ +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <ctype.h> + +#include "dshlib.h" + +int numInstanceOf(char *str, const char c) { + int count = 0; + while (*str != '\0') { + if (*str == c) count++; + str++; + } + + return count; +} + +/* + * build_cmd_list + * cmd_line: the command line from the user + * clist *: pointer to clist structure to be populated + * + * This function builds the command_list_t structure passed by the caller + * It does this by first splitting the cmd_line into commands by spltting + * the string based on any pipe characters '|'. It then traverses each + * command. For each command (a substring of cmd_line), it then parses + * that command by taking the first token as the executable name, and + * then the remaining tokens as the arguments. + * + * NOTE your implementation should be able to handle properly removing + * leading and trailing spaces! + * + * errors returned: + * + * OK: No Error + * ERR_TOO_MANY_COMMANDS: There is a limit of CMD_MAX (see dshlib.h) +strcat(command.args, inner_token); * commands. + * ERR_CMD_OR_ARGS_TOO_BIG: One of the commands provided by the user + * was larger than allowed, either the + * executable name, or the arg string. + * + * Standard Library Functions You Might Want To Consider Using + * memset(), strcmp(), strcpy(), strtok(), strlen(), strchr() + */ +int build_cmd_list(char *cmd_line, command_list_t *clist) +{ + if (numInstanceOf(cmd_line, PIPE_CHAR) > CMD_MAX-1) return ERR_TOO_MANY_COMMANDS; + + if ((int)strlen(cmd_line) > SH_CMD_MAX) return ERR_CMD_OR_ARGS_TOO_BIG; + + clist->num = 0; + + char *outer_saveptr = NULL; + char *inner_saveptr = NULL; + char *outer_token = strtok_r(cmd_line, PIPE_STRING, &outer_saveptr); + + while (outer_token != NULL) { + if (clist->num > CMD_MAX) return ERR_TOO_MANY_COMMANDS; + + command_t command; + memset(&command, 0, sizeof(command_t)); + + + + char *inner_token = strtok_r(outer_token, SPACE_STRING, &inner_saveptr); + + if (inner_token == NULL) { + outer_token = strtok_r(NULL, PIPE_STRING, &outer_saveptr); + continue; + } + + if (strlen(inner_token) > EXE_MAX) return ERR_CMD_OR_ARGS_TOO_BIG; + + strcpy(command.exe, inner_token); + command.args[0] = '\0'; + + inner_token = strtok_r(NULL, SPACE_STRING, &inner_saveptr); + while (inner_token != NULL) { + if (strlen(command.args) + strlen(inner_token) + 1 > ARG_MAX) return ERR_CMD_OR_ARGS_TOO_BIG; + + if (strlen(command.args) > 0) strcat(command.args, SPACE_STRING); + strcat(command.args, inner_token); + + inner_token = strtok_r(NULL, SPACE_STRING, &inner_saveptr); + } + + clist->commands[clist->num] = command; + clist->num++; + + outer_token = strtok_r(NULL, PIPE_STRING, &outer_saveptr); + } + + return OK; +} diff --git a/3-ShellP1/dshlib.h b/3-ShellP1/dshlib.h new file mode 100644 index 0000000000000000000000000000000000000000..d0dfe14393d88ccfadb5779a20666c0195efb429 --- /dev/null +++ b/3-ShellP1/dshlib.h @@ -0,0 +1,51 @@ +#ifndef __DSHLIB_H__ +#define __DSHLIB_H__ + +// Constants for command structure sizes +#define EXE_MAX 64 +#define ARG_MAX 256 +#define CMD_MAX 8 +// Longest command that can be read from the shell +#define SH_CMD_MAX EXE_MAX + ARG_MAX + +typedef struct command +{ + char exe[EXE_MAX]; + char args[ARG_MAX]; +} command_t; + +typedef struct command_list +{ + int num; + command_t commands[CMD_MAX]; +} command_list_t; + +// Special character #defines +#define SPACE_CHAR ' ' +#define SPACE_STRING " " +#define PIPE_CHAR '|' +#define PIPE_STRING "|" + +#define SH_PROMPT "dsh> " +#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 + +// starter code +#define M_NOT_IMPL "The requested operation is not implemented yet!\n" +#define EXIT_NOT_IMPL 3 +#define NOT_IMPLEMENTED_YET 0 + +// prototypes +int build_cmd_list(char *cmd_line, command_list_t *clist); + +// 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" + +#endif diff --git a/3-ShellP1/makefile b/3-ShellP1/makefile new file mode 100644 index 0000000000000000000000000000000000000000..d61ca771b4de9e9a5809e7cd82b747cfaa9c0bc6 --- /dev/null +++ b/3-ShellP1/makefile @@ -0,0 +1,27 @@ +# 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: + ./test.sh + +# Phony targets +.PHONY: all clean diff --git a/3-ShellP1/test.sh b/3-ShellP1/test.sh new file mode 100644 index 0000000000000000000000000000000000000000..676c62b6ddb3462151a1716ffaac75e0343381e9 --- /dev/null +++ b/3-ShellP1/test.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bats + +@test "Simple Command" { + run ./dsh <<EOF +test_command +exit +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="dsh>PARSEDCOMMANDLINE-TOTALCOMMANDS1<1>test_commanddsh>" + + # 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" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] + + # Assertions + [ "$status" -eq 0 ] + +} + +@test "Simple Command with Args" { + run ./dsh <<EOF +cmd -a1 -a2 +exit +EOF + + # Strip all whitespace (spaces, tabs, newlines) from the output + stripped_output=$(echo "$output" | tr -d '[:space:]') + + # Expected output + expected_output="dsh>PARSEDCOMMANDLINE-TOTALCOMMANDS1<1>cmd[-a1-a2]dsh>" + + # 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" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] + + # Assertions + [ "$status" -eq 0 ] + +} + + +@test "No command provided" { + run ./dsh <<EOF + +exit +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="dsh>warning:nocommandsprovideddsh>" + + # 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" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] + + # Assertions + [ "$status" -eq 0 ] + +} + +@test "Two commands" { + run ./dsh <<EOF +command_one | command_two +exit +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="dsh>PARSEDCOMMANDLINE-TOTALCOMMANDS2<1>command_one<2>command_twodsh>" + + # 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" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] + + # Assertions + [ "$status" -eq 0 ] + +} + +@test "three commands with args" { + run ./dsh <<EOF +cmd1 a1 a2 a3 | cmd2 a4 a5 a6 | cmd3 a7 a8 a9 +exit +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="dsh>PARSEDCOMMANDLINE-TOTALCOMMANDS3<1>cmd1[a1a2a3]<2>cmd2[a4a5a6]<3>cmd3[a7a8a9]dsh>" + + # 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" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] + + # Assertions + [ "$status" -eq 0 ] + +} + +@test "try max (8) commands" { + run ./dsh <<EOF +cmd1 | cmd2 | cmd3 | cmd4 | cmd5 | cmd6 | cmd7 | cmd8 +exit +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="dsh>PARSEDCOMMANDLINE-TOTALCOMMANDS8<1>cmd1<2>cmd2<3>cmd3<4>cmd4<5>cmd5<6>cmd6<7>cmd7<8>cmd8dsh>" + + # 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" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] + + # Assertions + [ "$status" -eq 0 ] + +} + +@test "try too many commands" { + run ./dsh <<EOF +cmd1 | cmd2 | cmd3 | cmd4 | cmd5 | cmd6 | cmd7 | cmd8 | cmd9 +exit +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="dsh>error:pipinglimitedto8commandsdsh>" + + # 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" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] + + # Assertions + [ "$status" -eq 0 ] + +} + +@test "kitchen sink - multiple commands" { + run ./dsh <<EOF +cmd1 +cmd2 arg arg2 +p1 | p2 +p3 p3a1 p3a2 | p4 p4a1 p4a2 +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="dsh>PARSEDCOMMANDLINE-TOTALCOMMANDS1<1>cmd1dsh>PARSEDCOMMANDLINE-TOTALCOMMANDS1<1>cmd2[argarg2]dsh>PARSEDCOMMANDLINE-TOTALCOMMANDS2<1>p1<2>p2dsh>PARSEDCOMMANDLINE-TOTALCOMMANDS2<1>p3[p3a1p3a2]<2>p4[p4a1p4a2]dsh>" + + # 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" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] + + # Assertions + [ "$status" -eq 0 ] + +}