diff --git a/3-ShellP1/dsh b/3-ShellP1/dsh index bf8a6f9f628431b104fe182d5354b5deba6d7493..cf84894e331d54e3344d4f81291f8a18e41419fd 100755 Binary files a/3-ShellP1/dsh and b/3-ShellP1/dsh differ diff --git a/4-ShellP2/.gitignore b/4-ShellP2/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..eb47a8e82ad127f89b6fdbbf0f531cb8865b7bf8 --- /dev/null +++ b/4-ShellP2/.gitignore @@ -0,0 +1 @@ +dsh \ No newline at end of file diff --git a/4-ShellP2/bats/assignment_tests.sh b/4-ShellP2/bats/assignment_tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..f62bbeb0645a7d5cce0f5d3503c637a993c6db6b --- /dev/null +++ b/4-ShellP2/bats/assignment_tests.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" ] +} \ No newline at end of file diff --git a/4-ShellP2/bats/student_tests.sh b/4-ShellP2/bats/student_tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..73b17557a4796a54428956bd693c15f828c1f9d9 --- /dev/null +++ b/4-ShellP2/bats/student_tests.sh @@ -0,0 +1,98 @@ +#!/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 ] +} + +# Test: Check if the 'exit' built-in command exits the shell +@test "Check exit command exits the shell" { + 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="dsh2>cmdloopreturned-7" + + # 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: Check handling of empty input (should warn and continue) +@test "Check empty input handling" { + run ./dsh <<EOF + +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="dsh2>warning:nocommandsprovideddsh2>cmdloopreturned0" + + # These echo commands will help with debugging and will only print + echo "Captured stdout:" + #if the test fails + echo "Output: $output" + echo "Exit Status: $status" + echo "${stripped_output} -> ${expected_output}" + + # Check exact match + [ "$stripped_output" = "$expected_output" ] +} + +# Test: Check 'cd' command changes directory +@test "Check cd command changes directory" { + run ./dsh <<EOF +cd / +pwd +EOF + stripped_output=$(echo "$output" | tr -d '[:space:]') + expected_output="/dsh2>dsh2>dsh2>cmdloopreturned0" + echo "Captured stdout:" + echo "Output: $output" + echo "Exit Status: $status" + echo "${stripped_output} -> ${expected_output}" + [ "$stripped_output" = "$expected_output" ] +} + +# Test: Check if 'rc' command returns last exit status +@test "Check rc command returns last exit status" { + run ./dsh <<EOF +cd +rc +EOF + [ "$status" -eq 0 ] + echo "Captured stdout: $output" + stripped_output=$(echo "$output" | tr -d '[:space:]') + expected_output="dsh2>dsh2>0dsh2>cmdloopreturned0" + [ "$stripped_output" = "$expected_output" ] +} + +# Test: Check handling of non-existent command +@test "Check handling of non-existent command" { + run ./dsh <<EOF +nocommand +rc +EOF + echo "Captured stdout: $output" + stripped_output=$(echo "$output" | tr -d '[:space:]') + expected_output="Errorexecutingnocommand:Nosuchfileordirectorydsh2>dsh2>dsh2>2dsh2>cmdloopreturned0" + [ "$stripped_output" = "$expected_output" ] +} \ No newline at end of file diff --git a/4-ShellP2/convert.py b/4-ShellP2/convert.py new file mode 100644 index 0000000000000000000000000000000000000000..d9259b3b59ece4e449d1f38bac3bcad2bedd5126 --- /dev/null +++ b/4-ShellP2/convert.py @@ -0,0 +1,24 @@ +import sys + +# Input binary file and output header file +input_file = "dragon.bin" +output_file = "dragon_data.h" + +try: + with open(input_file, "rb") as f: + binary_content = f.read() +except FileNotFoundError: + print(f"Error: {input_file} not found.", file=sys.stderr) + sys.exit(1) + +# Convert binary content into a C array +hex_data = ', '.join(f'0x{byte:02x}' for byte in binary_content) + +# Write the header file +with open(output_file, "w", encoding="utf-8") as f: + f.write(f'#ifndef DRAGON_DATA_H\n#define DRAGON_DATA_H\n\n') + f.write(f'const unsigned char DRAGON_BIN[] = {{ {hex_data} }};\n') + f.write(f'const unsigned int DRAGON_BIN_SIZE = {len(binary_content)};\n\n') + f.write(f'#endif // DRAGON_DATA_H\n') + +print(f"Generated {output_file}") diff --git a/4-ShellP2/dragon.bin b/4-ShellP2/dragon.bin new file mode 100644 index 0000000000000000000000000000000000000000..cce2d5afea01f25637612a3ea87396c38ff931a1 --- /dev/null +++ b/4-ShellP2/dragon.bin @@ -0,0 +1,3 @@ +x���A�� �=�`�{����J +�L���Ek!�HDm�5��F��#�HB<���\ƌ�ÈXs=_ 6����[�w�-�`ز1�J��'Y~R�D����ڹW�@2AϺ���@�4X*����'rUP!bA Q�c�gj� +�{c�"d �+�)#+{�>dN��`P4!�`?$\wH���$t�Pz>RA� �J��O���IJ@D�3!e�Ʉ�n�ܧk�m0���|���W��U��W�@܃a��k��[r<�<|����Ʀr���,�� ��f_�=y��uE"e\\+'P��![>#Ф�(!�4�_{��}y��?����h&{ )�����q[!DZ&��}a"L+�_�3�v�G \ No newline at end of file diff --git a/4-ShellP2/dragon.c b/4-ShellP2/dragon.c new file mode 100644 index 0000000000000000000000000000000000000000..255710fd60a16a0a5eb672723639f237d945537a --- /dev/null +++ b/4-ShellP2/dragon.c @@ -0,0 +1,27 @@ +#include <stdio.h> +#include <stdlib.h> +#include <zlib.h> +#include "dragon_data.h" // Include the embedded binary data + +#define DRAGON_BUFFER_SIZE 100000 + +// EXTRA CREDIT - print the Drexel dragon from embedded binary +extern void print_dragon() { + unsigned char *decompressed_data = (unsigned char *)malloc(DRAGON_BUFFER_SIZE); + if (!decompressed_data) { + fprintf(stderr, "Memory allocation failed!\n"); + return; + } + + uLongf decompressed_size = DRAGON_BUFFER_SIZE; + + // Ensure correct decompression + if (uncompress(decompressed_data, &decompressed_size, DRAGON_BIN, DRAGON_BIN_SIZE) != Z_OK) { + fprintf(stderr, "Decompression fail\n"); + free(decompressed_data); + return; + } + + printf("%s", decompressed_data); + free(decompressed_data); +} \ No newline at end of file diff --git a/4-ShellP2/dragon_data.h b/4-ShellP2/dragon_data.h new file mode 100644 index 0000000000000000000000000000000000000000..5888b1c72210c61c030fb58e79954dfec1e8cfd8 --- /dev/null +++ b/4-ShellP2/dragon_data.h @@ -0,0 +1,7 @@ +#ifndef DRAGON_DATA_H +#define DRAGON_DATA_H + +const unsigned char DRAGON_BIN[] = { 0x78, 0x9c, 0xb5, 0xd6, 0x41, 0xa2, 0x83, 0x20, 0x0c, 0x04, 0xd0, 0x3d, 0xa7, 0x60, 0x93, 0x7b, 0xe4, 0xfe, 0xa7, 0xea, 0xaf, 0x4a, 0x0a, 0xc9, 0x4c, 0x82, 0xad, 0x9f, 0x45, 0x6b, 0x21, 0xf8, 0x48, 0x44, 0x6d, 0xeb, 0x0f, 0x35, 0x95, 0xbf, 0x46, 0xc6, 0xda, 0x23, 0x82, 0x48, 0x42, 0x3c, 0x84, 0x14, 0xc6, 0x03, 0x88, 0x5c, 0xc6, 0x8c, 0xe8, 0xc3, 0x88, 0x58, 0x73, 0x3d, 0x5f, 0x20, 0x36, 0xf3, 0xfd, 0xa9, 0xbe, 0x5b, 0xac, 0x77, 0x84, 0x2d, 0xfb, 0x60, 0x03, 0x11, 0xd8, 0xb2, 0x31, 0x9f, 0x4a, 0x89, 0xa4, 0x27, 0x59, 0x7e, 0x52, 0xa3, 0x44, 0xb2, 0xa5, 0x86, 0xf3, 0xda, 0xb9, 0x57, 0xa3, 0x40, 0x32, 0x41, 0xcf, 0xba, 0xe3, 0xe5, 0xdf, 0x40, 0xd2, 0x34, 0x58, 0x2a, 0x12, 0xef, 0x98, 0xdf, 0x10, 0xae, 0xff, 0x27, 0x72, 0x55, 0x50, 0x21, 0x62, 0x41, 0x09, 0x51, 0xd3, 0x63, 0x1c, 0x96, 0x0b, 0x67, 0x6a, 0xeb, 0x0a, 0x15, 0x98, 0x7b, 0x63, 0x88, 0x22, 0x64, 0x09, 0xa2, 0x2b, 0xde, 0x29, 0x23, 0x2b, 0x7b, 0xeb, 0x3e, 0x64, 0x4e, 0x85, 0x9d, 0x60, 0x1e, 0x50, 0x16, 0x34, 0x21, 0x9f, 0x60, 0x3f, 0x24, 0x5c, 0x01, 0x77, 0x48, 0xc6, 0x18, 0x12, 0x8c, 0xe4, 0x9a, 0x24, 0x08, 0x74, 0x06, 0x12, 0x8d, 0x50, 0x7a, 0x3e, 0x52, 0x41, 0x8d, 0x0d, 0xa4, 0x4a, 0x89, 0xcc, 0x4f, 0xe9, 0xf4, 0x9a, 0xc4, 0xb2, 0x40, 0x44, 0xcf, 0x33, 0x21, 0x65, 0x1c, 0xde, 0xc9, 0x84, 0xec, 0x6e, 0x96, 0xdc, 0xa7, 0x6b, 0xca, 0x04, 0x6d, 0x30, 0xd7, 0x14, 0xf6, 0xa7, 0x15, 0x7c, 0x8f, 0xb4, 0xee, 0x82, 0x57, 0xc4, 0xcd, 0x55, 0xac, 0xa7, 0x57, 0xe9, 0x40, 0xdc, 0x83, 0x61, 0xce, 0x06, 0xad, 0x6b, 0xfb, 0xca, 0x5b, 0x1e, 0x72, 0x3c, 0xbb, 0x3c, 0x7c, 0xdc, 0xf5, 0xd7, 0x05, 0xc5, 0xc6, 0xa6, 0x72, 0x05, 0x82, 0x07, 0xe4, 0xa0, 0xd8, 0x2c, 0xbc, 0xf1, 0x18, 0x20, 0x86, 0xec, 0x66, 0x5f, 0xec, 0x3d, 0x10, 0x79, 0x1e, 0xb6, 0xbd, 0x75, 0x45, 0x22, 0x65, 0x5c, 0x5c, 0x2b, 0x27, 0x50, 0x82, 0xcc, 0x02, 0x7f, 0x21, 0x5b, 0x3e, 0x23, 0x15, 0xd0, 0xa4, 0xb9, 0x28, 0x10, 0x21, 0xce, 0x34, 0xac, 0x12, 0x5f, 0x7b, 0xb0, 0xd6, 0x7d, 0x79, 0xfd, 0x81, 0x3f, 0x12, 0xc4, 0xc8, 0xf3, 0x02, 0x93, 0x68, 0x26, 0x7b, 0x1b, 0x0d, 0x29, 0xd9, 0xeb, 0xfa, 0x06, 0xd2, 0xed, 0x71, 0x5b, 0x21, 0xc7, 0xb1, 0x26, 0x08, 0x93, 0xba, 0x7d, 0x61, 0x22, 0x4c, 0x2b, 0x91, 0x5f, 0x9a, 0x33, 0xda, 0x0b, 0x76, 0x93, 0x04, 0x47 }; +const unsigned int DRAGON_BIN_SIZE = 365; + +#endif // DRAGON_DATA_H diff --git a/4-ShellP2/dsh_cli.c b/4-ShellP2/dsh_cli.c new file mode 100644 index 0000000000000000000000000000000000000000..9262cf457e6b235b19999efa04153e1de0962255 --- /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); +} \ No newline at end of file diff --git a/4-ShellP2/dshlib.c b/4-ShellP2/dshlib.c new file mode 100644 index 0000000000000000000000000000000000000000..1e9f5f6b448992d0d49dcb7fc28cbbce82676d6b --- /dev/null +++ b/4-ShellP2/dshlib.c @@ -0,0 +1,216 @@ +#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" + +/* + * 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() + */ + +static int last_exit_status = 0; // Store last exit code + +int alloc_cmd_buff(cmd_buff_t *cmd_buff) { + cmd_buff->_cmd_buffer = (char *)malloc(SH_CMD_MAX * sizeof(char)); + if (!cmd_buff->_cmd_buffer) { + return ERR_MEMORY; + } + memset(cmd_buff->_cmd_buffer, 0, SH_CMD_MAX); + return OK; +} + +int free_cmd_buff(cmd_buff_t *cmd_buff) { + if (cmd_buff->_cmd_buffer) { + free(cmd_buff->_cmd_buffer); + cmd_buff->_cmd_buffer = NULL; + } + return OK; +} + +int clear_cmd_buff(cmd_buff_t *cmd_buff) { + memset(cmd_buff, 0, sizeof(cmd_buff_t)); + return OK; +} + +int build_cmd_buff(char *cmd_line, cmd_buff_t *cmd_buff) { + clear_cmd_buff(cmd_buff); + + int i = 0; + char *cmd_start = cmd_line; + char *token_start = NULL; + bool in_quotes = false; + + while (*cmd_start) { + // Detect start of quoted argument + if (*cmd_start == '\"' && !in_quotes) { + in_quotes = true; + token_start = ++cmd_start; + } + // Detect end of quoted argument + else if (*cmd_start == '\"' && in_quotes) { + in_quotes = false; + *cmd_start = '\0'; + cmd_buff->argv[i++] = token_start; + } + // Handle normal argument (non-space outside quotes) + else if (!in_quotes && (*cmd_start != ' ' && *cmd_start != '\t')) { + token_start = cmd_start; + while (*cmd_start != ' ' && *cmd_start != '\t' && *cmd_start != '\0') { + cmd_start++; + } + if (*cmd_start != '\0') { + *cmd_start = '\0'; + cmd_start++; + } + cmd_buff->argv[i++] = token_start; + } + else { + cmd_start++; + } + } + + cmd_buff->argv[i] = NULL; + cmd_buff->argc = i; + + if (i > 0) { + return OK; + } else { + return WARN_NO_CMDS; +} +} + +Built_In_Cmds match_command(const char *input) { + if (strcmp(input, "exit") == 0) { + return BI_CMD_EXIT; + } else if (strcmp(input, "cd") == 0) { + return BI_CMD_CD; + } else if (strcmp(input, "dragon") == 0) { + return BI_CMD_DRAGON; + } else if (strcmp(input, "rc") == 0) { + return BI_RC; + } + return BI_NOT_BI; +} + +Built_In_Cmds exec_built_in_cmd(cmd_buff_t *cmd) { + Built_In_Cmds cmd_type = match_command(cmd->argv[0]); + if (cmd_type == BI_CMD_EXIT) { + return BI_CMD_EXIT; + } + if (cmd_type == BI_CMD_CD) { + if (cmd->argc > 1) { + if (chdir(cmd->argv[1]) != 0) { + fprintf(stderr, "cd: %s: No such file or directory\n", cmd->argv[1]); + return ERR_EXEC_CMD; + } + } + return BI_EXECUTED; + } + if (cmd_type == BI_CMD_DRAGON) { + print_dragon(); + return BI_EXECUTED; + } + if (cmd_type == BI_RC) { + printf("%d\n", last_exit_status); + return BI_EXECUTED; + } + return BI_NOT_BI; +} + +int exec_cmd(cmd_buff_t *cmd) { + pid_t pid = fork(); + if (pid == -1) { + perror("fork"); + return ERR_EXEC_CMD; + } else if (pid == 0) { + execvp(cmd->argv[0], cmd->argv); + fprintf(stderr, "Error executing %s: %s\n", cmd->argv[0], strerror(errno)); + exit(errno); + } else { + int status; + waitpid(pid, &status, 0); + if (WIFEXITED(status)) { + last_exit_status = WEXITSTATUS(status); + } else { + last_exit_status = -1; + } + return last_exit_status; + } +} + +int exec_local_cmd_loop() { + char cmd_line[SH_CMD_MAX]; + cmd_buff_t cmd; + Built_In_Cmds result; + + while (1) { + printf("%s", SH_PROMPT); + if (fgets(cmd_line, sizeof(cmd_line), stdin) == NULL) { + printf("\n"); + break; + } + cmd_line[strcspn(cmd_line, "\n")] = '\0'; + + if (build_cmd_buff(cmd_line, &cmd) == WARN_NO_CMDS) { + printf(CMD_WARN_NO_CMD); + continue; + } + + result = exec_built_in_cmd(&cmd); + if (result == BI_CMD_EXIT) { + free_cmd_buff(&cmd); + return OK_EXIT; + break; + } else if (result == BI_NOT_BI) { + last_exit_status = exec_cmd(&cmd); + } + } + + free_cmd_buff(&cmd); + return OK; +} \ No newline at end of file diff --git a/4-ShellP2/dshlib.h b/4-ShellP2/dshlib.h new file mode 100644 index 0000000000000000000000000000000000000000..e1d168fc87297d388aea97b687fd2819bcf5cad3 --- /dev/null +++ b/4-ShellP2/dshlib.h @@ -0,0 +1,80 @@ +#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[CMD_ARGV_MAX]; + 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 PIPE_CHAR '|' +#define PIPE_STRING "|" + +#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(cmd_buff_t *cmd_buff); +int free_cmd_buff(cmd_buff_t *cmd_buff); +int clear_cmd_buff(cmd_buff_t *cmd_buff); +int build_cmd_buff(char *cmd_line, cmd_buff_t *cmd_buff); +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" + +#endif \ No newline at end of file diff --git a/4-ShellP2/fork-exec-1.png b/4-ShellP2/fork-exec-1.png new file mode 100644 index 0000000000000000000000000000000000000000..56186a921c27a47641e54efbb72397c544f93a32 Binary files /dev/null and b/4-ShellP2/fork-exec-1.png differ diff --git a/4-ShellP2/fork-exec-2.png b/4-ShellP2/fork-exec-2.png new file mode 100644 index 0000000000000000000000000000000000000000..30e5f4ff67c5baaaa99bf7fee75e4f2a7b802bde Binary files /dev/null and b/4-ShellP2/fork-exec-2.png differ diff --git a/4-ShellP2/makefile b/4-ShellP2/makefile new file mode 100644 index 0000000000000000000000000000000000000000..0bb94789e9fa66834a72a42118d5664824a30a36 --- /dev/null +++ b/4-ShellP2/makefile @@ -0,0 +1,32 @@ +# Compiler settings +CC = gcc +CFLAGS = -Wall -Wextra -g +LDFLAGS = -lz + +# 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) $(LDFLAGS) + +# 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 \ No newline at end of file diff --git a/4-ShellP2/questions.md b/4-ShellP2/questions.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/4-ShellP2/readme.md b/4-ShellP2/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..a0a38162c5704952d2bb1be57e4872f648c42858 --- /dev/null +++ b/4-ShellP2/readme.md @@ -0,0 +1,192 @@ +# Assignment: Custom Shell Part 2 - Fork/Exec + +This week we will build on our `dsh` Drexel Shell by adding an implementation for the builtin command `cd`, and a `fork/exec` implementation to run "external commands". + +This content builds on the prior assignment; if you need a refresher on what a shell is or the difference between built-in and external commands, please review the readme from that assignment. + +# Reuse Prior Work! + +The `dsh` assignments are meant to be additive. Much of the parsing logic from the last assignement can be re-used in this assignement. The structures are a little different so you might have to refactor some of your code, but that's a great practical lesson in software engineering; the highest quality results come from frequent iteration. + +The next section highlights the differences (both conceptually and in file structure) from the prior assignement. + +# Differences from Part 1 Assignment + +- We've restructured the code slightly to move all implementation details into the lib file (`dshlib.c`) out of `dsh_cli.c`. **You do not need to write any code in `dsh_cli.c`!** +- This week we'll implement a fork/exec pattern to execute external commands; these commands should execute and behave just as they would if you ran them from your default shell; last week we only printed command lines that we parsed +- If you did the `dragon` extra credit, we moved the implementation into `dragon.c` +- We will NOT implement pipe splitting or multiple commands in one command line input; last week we implemented parsing the CLI by pipe just to print commands, but actually implementing pipes to execute commands is beyond the scope of this week's assignement - we will get to it but not this week! + - This week we work with a single `cmd_buff` type at a time; we will not use the `command_list_t` from last week + - This an example of some refactoring from last week's code, you can adapt your parsing logic but omit the pipe logic until we get to that in a future assignement + +# Fork / Exec + +Let's introduce two new system calls: fork() and exec(). These calls are fundamental to process creation and execution in all Unix-like operating systems. + +When a process calls fork(), the operating system creates a new child process that is an exact copy of the parent, inheriting its memory, file descriptors, and execution state. The child process receives a return value of 0 from fork(), while the parent receives the child's process ID. After forking, the child process often replaces its memory image with a new executable using one of the exec() family of functions (e.g., execl(), execv(), execvp()). + +Unlike fork(), exec() does not create a new process but instead replaces the calling process’s address space with a new program, preserving file descriptors unless explicitly changed. This mechanism allows Unix shells to execute new programs by first forking a child process and then using exec() to run the desired binary while the parent process waits for the child to complete using wait(). + +Recall the fork/exec pattern from lecture slides and demo - we are implementing this two-step process using system calls. + + + + + +Remember that the fork/exec pattern requires you to use conditional branching logic to implement the child path and the parent path in the code. We did a basic demo of this in class using this demo code https://github.com/drexel-systems/SysProg-Class/blob/main/demos/process-thread/2-fork-exec/fork-exec.c. In the demo we used `execv()`, which requires an absolute path to the binary. In this assignement you should use `execvp()`; `execvp()` will search the `PATH` variable locations for binaries. As with the demo, you can use `WEXITSTATUS` to extract the status code from the child process. + +# Assignment Details + +### Step 1 - Review [./starter/dshlib.h](./starter/dshlib.h) + +The file [./starter/dshlib.h](./starter/dshlib.h) contains some useful definitions and types. Review the available resources in this file before you start coding - these are intended to make your work easier and more robust! + +### Step 2 - Implement `cd` in [./starter/dshlib.c](./starter/dshlib.c) + +Building on your code from last week, implement the `cd` command. + +- when called with no arguments, `cd` does nothing (this is different than Linux shell behavior; shells implement `cd` as `cd ~1` or `cd $HOME`; we'll do that in a future assignement) +- when called with one argument, `chdir()` the current dsh process into the directory provided by argument + +### Step 3 - Re-implement Your Main Loop and Parsing Code in exec_local_cmd_loop() [./starter/dshlib.c](./starter/dshlib.c) + +Implement `exec_local_cmd_loop()` by refactoring your code from last week to use 1 `cmd_buff` type in the main loop instead of using a command list. + +On each line-of-input parsing, you should populate `cmd_buff` using these rules: + +- trim ALL leading and trailing spaces +- eliminate duplicate spaces UNLESS they are in a quoted string +- account for quoted strings in input; treat a quoted string with spaces as a single argument + - for example, given ` echo " hello, world" ` you would parse this as: `["echo", " hello, world"]`; note that spaces inside the double quotes were preserved + +`cmd_buff` is provided to get you started. You don't have to use this struct, but it is all that's required to parse a line of input into a `cmd_buff`. + +```c +typedef struct cmd_buff +{ + int argc; + char *argv[CMD_ARGV_MAX]; + char *_cmd_buffer; +} cmd_buff_t; +``` + +### Step 4 - Implement fork/exec pattern in [./starter/dshlib.c](./starter/dshlib.c) + +Implement fork/exec of external commands using `execvp()`. This is a pretty straight-forward task; once the command and it's arguments are parsed, you can pass them straight to `execvp()`. + +Don't forget to implement a wait of the return code, and extraction of the return code. We're not doing anything with the return code yet, unless you are doing extra credit. + +### Step 5 - Create BATS Tests + +So far we've provided pre-built a `test.sh` file with assigments. These files use the [bash-based BATS unit test framework](https://bats-core.readthedocs.io/en/stable/tutorial.html#your-first-test). + +Going forward, assignements will have a bats folder structure like this: + +- your-workspace-folder/ + - bats/assignement_tests.sh + - bats/student_tests.sh + +**bats/assignment_tests.sh** + +- DO NOT EDIT THIS FILE +- assignment_tests.sh contains tests that must pass to meet the requirements of the assignment +- it is run as part of `make test`; remember to run this to verify your code + +**bats/student_tests.sh** + +- this file must contain YOUR test suite to help verify your code +- for some assignments you will be graded on creation of the tests, and it is your responsibility to make sure the tests provide adequate coverage +- this file is also run with `make test` + +**About BATS** + +Key points of BATS testing - + +- file header is `#!/usr/bin/env bats` such that you can execute tests by simply running `./test_file.sh` +- incorrect `\r\n` can cause execution to fail - easiest way to avoid is use the [drexel-cci](https://marketplace.visualstudio.com/items?itemName=bdlilley.drexel-cci) extension to download assignement code; if you do not use this, make sure you do not copy any windows line endings into the file during a copy/paste +- assertions are in square braces + - example: check output `[ "$stripped_output" = "$expected_output" ]` + - example: check return code `$status` variable: `[ "$status" -eq 0 ]` + +Please review the BATS link above if you have questions on syntax or usage. You can also look at test files we provided with assignment for more examples. **You will be graded on the quality of breadth of your unit test suite.** + +What this means to you - follow these guidelines when writing tests: + +- cover every type of functionallity; for example, you need to cover built-in command and external commands +- test for all use cases / edge cases - for example, for the built-in `cd` command you might want to verify that: + - when called without arguments, the working dir doesn't change (you could verify with `pwd`) + - when called with one argument, it changes directory to the given argument (again, you can verify with `pwd`) +- be thorough - try to cover all the possible ways a user might break you program! +- write tests first; this is called "Test Driven Development" - to learn more, check out [Martin Fowler on TDD](https://martinfowler.com/bliki/TestDrivenDevelopment.html) + +### Step 6 - Answer Questions + +Answer the questions located in [./questions.md](./questions.md). + +### Sample Run with Sample Output +The below shows a sample run executing multiple commands and the expected program output: + +```bash +./dsh +dsh2> uname -a +Linux ubuntu 6.12.10-orbstack-00297-gf8f6e015b993 #42 SMP Sun Jan 19 03:00:07 UTC 2025 aarch64 aarch64 aarch64 GNU/Linux +dsh2> uname +Linux +dsh2> echo "hello, world" +hello, world +dsh2> pwd +/home/ben/SysProg-Class-Solutions/assignments/4-ShellP2/solution +dsh2> ls +dir1 dragon.c dragon.txt dsh dsh_cli.c dshlib.c dshlib.h fancy_code_do_not_use makefile shell_roadmap.md test.sh wip +dsh2> cd dir1 +dsh2> pwd +/home/ben/SysProg-Class-Solutions/assignments/4-ShellP2/solution/dir1 +dsh2> +``` + +### Extra Credit: +10 + +This week we're being naive about return codes from external commands; if there is any kind of failure, we just print the `CMD_ERR_EXECUTE` message. + +Implement return code handling for extra credit. Hint - check out `man execvp` and review the `errno` and return value information. + +Errno and value definitions are in `#include <errno.h>`. + +Tips: + +- in the child process, `errno` will contain the error value if there was an error; so return this from your child process +- the `WEXITSTATUS` macro will extract `errno` + +Requirements: + +- Check for all file-related status codes from `errno.h` that you might expect when trying to invoke a binary from $PATH; for example - `ENOENT` is file not found, `EACCES` is permission denied +- Print a suitable message for each error you detect +- Implement a "rc" builtin command that prints the return code of the last operation; for example, if the child process returns `-1`, `rc` should output `-1` +- **Don't forget to add unit tests in** `./bats/student_tests.sh`! + +Example run: + +```bash +./dsh +dsh2> not_exists +Command not found in PATH +dsh2> rc +2 +dsh2> +``` + +This extra credit is a precursor to implementing variables; shells set the variable `$?` to the return code of the last executed command. A full variable implementation is beyond the scope of this assignement, so we opted to create the `rc` builtin to mimic the behavior of the `$?` variable in other shells. + +#### Grading Rubric + +This assignment will be weighted 50 points. + +- 25 points: Correct implementation of required functionality +- 5 points: Code quality (how easy is your solution to follow) +- 15 points: Answering the written questions: [questions.md](./questions.md) +- 15 points: Quality and breadth of BATS unit tests +- 10 points: [EXTRA CREDIT] handle return codes for execvp + +Total points achievable is 70/60. + +