From d438506981fbb43dc651d3ee7eeecb5a0dd62cbe Mon Sep 17 00:00:00 2001
From: wn73 <vythao.37cdc@gmail.com>
Date: Wed, 5 Mar 2025 23:22:07 -0500
Subject: [PATCH] update
---
5-Shell-Part3/bats/assignment_tests.sh | 36 +++
5-Shell-Part3/bats/student_tests.sh | 278 ++++++++++++++++++++
5-Shell-Part3/dsh_cli.c | 13 +
5-Shell-Part3/dshlib.c | 345 +++++++++++++++++++++++++
5-Shell-Part3/dshlib.h | 93 +++++++
5-Shell-Part3/makefile | 31 +++
5-Shell-Part3/questions.md | 65 +++++
7 files changed, 861 insertions(+)
create mode 100644 5-Shell-Part3/bats/assignment_tests.sh
create mode 100644 5-Shell-Part3/bats/student_tests.sh
create mode 100644 5-Shell-Part3/dsh_cli.c
create mode 100644 5-Shell-Part3/dshlib.c
create mode 100644 5-Shell-Part3/dshlib.h
create mode 100644 5-Shell-Part3/makefile
create mode 100644 5-Shell-Part3/questions.md
diff --git a/5-Shell-Part3/bats/assignment_tests.sh b/5-Shell-Part3/bats/assignment_tests.sh
new file mode 100644
index 0000000..ac34560
--- /dev/null
+++ b/5-Shell-Part3/bats/assignment_tests.sh
@@ -0,0 +1,36 @@
+#!/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 "Pipes" {
+ run "./dsh" <<EOF
+ls | grep dshlib.c
+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="dshlib.cdsh3>dsh3>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 ]
+}
diff --git a/5-Shell-Part3/bats/student_tests.sh b/5-Shell-Part3/bats/student_tests.sh
new file mode 100644
index 0000000..809a1c2
--- /dev/null
+++ b/5-Shell-Part3/bats/student_tests.sh
@@ -0,0 +1,278 @@
+#!/usr/bin/env bats
+
+# File: student_tests.sh
+#
+# Create your unit tests suit in this file
+
+# Helper function to strip shell prompt from output
+strip_prompt() {
+ echo "$1" | sed 's/dsh3> //g'
+}
+
+@test "Example: check ls runs without errors" {
+ run ./dsh <<EOF
+ls
+EOF
+
+ # Assertions
+ [ "$status" -eq 0 ]
+}
+
+# Test basic pipe functionality
+@test "Basic pipe: ls | grep .c" {
+ run ./dsh <<EOF
+ls | grep .c
+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_cli.cdshlib.cdsh3>dsh3>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}"
+ echo "${expected_output}"
+
+ # Check exact match
+ [ "$stripped_output" = "$expected_output" ]
+ [ "$status" -eq 0 ]
+}
+
+@test "Multiple pipes: ls | grep .c | wc -l" {
+ run ./dsh <<EOF
+ls | grep .c | wc -l
+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="2dsh3>dsh3>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}"
+ echo "${expected_output}"
+
+ # Check exact match
+ [ "$stripped_output" = "$expected_output" ]
+ [ "$status" -eq 0 ]
+}
+
+# Test built-in commands
+@test "Built-in: exit command" {
+ run ./dsh <<< "exit"
+ [ "$status" -eq 0 ]
+}
+
+
+@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-testdsh3>dsh3>dsh3>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="/tmpdsh3>dsh3>dsh3>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/whichdsh3>dsh3>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 "Built-in: dragon command" {
+ run ./dsh <<< "dragon"
+ [ "$status" -eq 0 ]
+ stripped_output=$(strip_prompt "$output")
+ [[ "$stripped_output" =~ "@%%%%" ]]
+}
+
+# Test error cases
+@test "Error: empty command" {
+ run ./dsh <<< ""
+ [ "$status" -eq 0 ]
+ [[ "$output" =~ "warning: no commands provided" ]]
+}
+
+@test "Error: too many pipes" {
+ run ./dsh <<< "ls | grep .c | wc -l | cat | grep 1 | cat | grep 2 | cat | grep 3"
+ [ "$status" -eq 0 ]
+ [[ "$output" =~ "error: piping limited to" ]]
+}
+
+# Test complex pipe scenarios
+@test "Complex pipe: ls -l | grep .c | sort | wc -l" {
+ run ./dsh <<EOF
+ls -l | grep .c | sort | wc -l
+EOF
+ [ "$status" -eq 0 ]
+ stripped_output=$(echo "$output" | tr -d '[:space:]')
+ expected_output="2dsh3>dsh3>cmdloopreturned0"
+ echo "${stripped_output}"
+ echo "${expected_output}"
+ [ "$stripped_output" = "$expected_output" ]
+}
+
+# Test command not found
+@test "Error: command not found" {
+ run ./dsh <<< "nonexistentcommand"
+ [ "$status" -eq 0 ]
+ [[ "$output" =~ "execvp" ]]
+}
+
+# Test command with arguments
+@test "Command with arguments" {
+ run ./dsh <<< "ls -l | grep .c"
+ [ "$status" -eq 0 ]
+ stripped_output=$(strip_prompt "$output")
+ [[ "$stripped_output" =~ "dshlib.c" ]]
+ [[ "$stripped_output" =~ "dsh_cli.c" ]]
+}
+
+# Test output redirection
+@test "Output redirection" {
+ # Create a temporary file for output
+ temp_file=$(mktemp)
+
+ # Run command with output redirection
+ run ./dsh <<EOF
+ls -l | grep .c > $temp_file
+cat $temp_file
+EOF
+
+ # Clean up
+ rm -f $temp_file
+
+ # Check if output contains the expected files
+ stripped_output=$(strip_prompt "$output")
+ [[ "$stripped_output" =~ "dshlib.c" ]]
+ [[ "$stripped_output" =~ "dsh_cli.c" ]]
+ [ "$status" -eq 0 ]
+}
+
+# Test input redirection
+@test "Input redirection" {
+ # Create a temporary file with test content
+ temp_file=$(mktemp)
+ echo "dshlib.c" > $temp_file
+ echo "dsh_cli.c" >> $temp_file
+
+ # Run command with input redirection
+ run ./dsh <<EOF
+grep .c < $temp_file
+EOF
+
+ # Clean up
+ rm -f $temp_file
+
+ # Check if output contains the expected files
+ stripped_output=$(strip_prompt "$output")
+ [[ "$stripped_output" =~ "dshlib.c" ]]
+ [[ "$stripped_output" =~ "dsh_cli.c" ]]
+ [ "$status" -eq 0 ]
+}
+
+# Test append redirection
+@test "Append redirection" {
+ # Create a temporary file for output
+ temp_file=$(mktemp)
+
+ # First command with output redirection
+ run ./dsh <<EOF
+ls -l | grep dshlib.c > $temp_file
+EOF
+
+ # Second command with append redirection
+ run ./dsh <<EOF
+ls -l | grep dsh_cli.c >> $temp_file
+cat $temp_file
+EOF
+
+ # Clean up
+ rm -f $temp_file
+
+ # Check if output contains both files
+ stripped_output=$(strip_prompt "$output")
+ [[ "$stripped_output" =~ "dshlib.c" ]]
+ [[ "$stripped_output" =~ "dsh_cli.c" ]]
+ [ "$status" -eq 0 ]
+}
diff --git a/5-Shell-Part3/dsh_cli.c b/5-Shell-Part3/dsh_cli.c
new file mode 100644
index 0000000..9262cf4
--- /dev/null
+++ b/5-Shell-Part3/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/5-Shell-Part3/dshlib.c b/5-Shell-Part3/dshlib.c
new file mode 100644
index 0000000..ddcfe90
--- /dev/null
+++ b/5-Shell-Part3/dshlib.c
@@ -0,0 +1,345 @@
+#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"
+
+void printDragon(){
+ const char dragon[39][103] = {
+" @%%%% \n",
+" %%%%%% \n",
+" %%%%%% \n",
+" % %%%%%%% @ \n",
+" %%%%%%%%%% %%%%%%% \n",
+" %%%%%%% %%%%@ %%%%%%%%%%%%@ %%%%%% @%%%% \n",
+" %%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%% \n",
+" %%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%% %%%%%%%%%%%%%%% \n",
+" %%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%% %%% \n",
+" %%%%%%%%%%%%%%%%%%%%%%%%%%%%@ @%%%%%%%%%%%%%%%%%% %% \n",
+" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%% \n",
+" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \n",
+" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%@%%%%%%@ \n",
+" %%%%%%%%@ %%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%% %% \n",
+" %%%%%%%%%%%%% %%@%%%%%%%%%%%% %%%%%%%%%%% %%%%%%%%%%%% @% \n",
+" %%%%%%%%%% %%% %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%% \n",
+" %%%%%%%%% % %%%%%%%%%%%%% %%%%%%%%%%%%@%%%%%%%%%%% \n",
+"%%%%%%%%%@ % %%%%%%%%%%%%% @%%%%%%%%%%%%%%%%%%%%%%%%% \n",
+"%%%%%%%%@ %%@%%%%%%%%%%%% @%%%%%%%%%%%%%%%%%%%%%%%%%%%% \n",
+"%%%%%%%@ %%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \n",
+"%%%%%%%%%% %%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%% \n",
+"%%%%%%%%%@ @%%%%%%%%%%%%%% %%%%%%%%%%%%@ %%%% %%%%%%%%%%%%%%%%% %%%%%%%%\n",
+"%%%%%%%%%% %%%%%%%%%%%%%%%%% %%%%%%%%%%%%% %%%%%%%%%%%%%%%%%% %%%%%%%%%\n",
+"%%%%%%%%%@%%@ %%%%%%%%%%%%%%%%@ %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%% %%\n",
+" %%%%%%%%%% % %%%%%%%%%%%%%%@ %%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%% %%\n",
+" %%%%%%%%%%%% @ %%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% \n",
+" %%%%%%%%%%%%% %% % %@ %%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% \n",
+" %%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%% @%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%% \n",
+" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% \n",
+" @%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%%%%%%%% \n",
+" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%%%% %%%%%%% \n",
+" %%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%%% @%%%%%%%%% \n",
+" %%%%%%%%%%%%%%%%%%%% @%@% @%%%%%%%%%%%%%%%%%% %%% \n",
+" %%%%%%%%%%%%%%% %%%%%%%%%% %%%%%%%%%%%%%%% % \n",
+" %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%%%%%%%%%%%%% \n",
+" %%%%%%%%%%%%%%%%%%%%%%%%%% %%%% %%% %%%%%%%%%% %%%@ \n",
+" %%%%%%%%%%%%%%%%%%% %%%%%% %% %%%%%%%%%%%%%@ \n",
+" %%%%%%%@ \n"
+ };
+
+
+ for (int i = 0; i < sizeof(dragon) / sizeof(dragon[0]); i++) {
+ for (int j = 0; j < sizeof(dragon[0]) / sizeof(dragon[0][0]); j++) {
+ if (dragon[i][j] == 0) break;
+ putchar((char)dragon[i][j]);
+ }
+ }
+}
+
+/*
+ * 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 build_cmd_list(char *cmd_line, command_list_t *clist)
+{
+ if (strlen(cmd_line) == 0) {
+ printf(CMD_WARN_NO_CMD);
+ return OK;
+ }
+
+ // Initialize command list
+ clist->num = 0;
+ memset(clist->commands, 0, sizeof(clist->commands));
+
+ // Split by pipe
+ char *saveptr1; // For the pipe tokenization
+ char *saveptr2; // For the argument tokenization
+ char *cmd_copy = strdup(cmd_line);
+ if (!cmd_copy) return ERR_MEMORY;
+
+ char *cmd = strtok_r(cmd_copy, "|", &saveptr1);
+ while (cmd != NULL) {
+ // Trim leading/trailing spaces
+ while (*cmd == ' ') cmd++;
+ char *end = cmd + strlen(cmd) - 1;
+ while (end > cmd && *end == ' ') {
+ *end = '\0';
+ end--;
+ }
+
+ if (strlen(cmd) == 0) {
+ cmd = strtok_r(NULL, "|", &saveptr1);
+ continue;
+ }
+
+ // Check command limit
+ if (clist->num >= CMD_MAX) {
+ free(cmd_copy);
+ return ERR_TOO_MANY_COMMANDS;
+ }
+
+ // Parse command and arguments
+ cmd_buff_t *curr_cmd = &clist->commands[clist->num];
+ curr_cmd->argc = 0;
+
+ // Split command into arguments
+ char *arg = strtok_r(cmd, " ", &saveptr2);
+ while (arg != NULL) {
+ if (curr_cmd->argc >= CMD_ARGV_MAX) {
+ free(cmd_copy);
+ return ERR_CMD_OR_ARGS_TOO_BIG;
+ }
+ curr_cmd->argv[curr_cmd->argc++] = strdup(arg);
+ arg = strtok_r(NULL, " ", &saveptr2);
+ }
+ curr_cmd->argv[curr_cmd->argc] = NULL;
+
+ clist->num++;
+ cmd = strtok_r(NULL, "|", &saveptr1);
+ }
+
+ free(cmd_copy);
+ return OK;
+}
+
+int exec_local_cmd_loop()
+{
+ char cmd_buff[SH_CMD_MAX];
+ int rc = 0;
+ command_list_t clist;
+ memset(&clist, 0, sizeof(command_list_t));
+
+ while(1) {
+ printf("%s", SH_PROMPT);
+
+ if (fgets(cmd_buff, SH_CMD_MAX, stdin) == NULL) {
+ printf("\n");
+ break;
+ }
+
+ // Remove trailing newline
+ cmd_buff[strcspn(cmd_buff, "\n")] = '\0';
+
+ // Skip empty commands
+ if (strlen(cmd_buff) == 0) {
+ printf(CMD_WARN_NO_CMD);
+ continue;
+ }
+
+ // Parse commands by pipe
+ rc = build_cmd_list(cmd_buff, &clist);
+ if (rc != OK) {
+ if (rc == ERR_TOO_MANY_COMMANDS) {
+ printf(CMD_ERR_PIPE_LIMIT, CMD_MAX);
+ }
+ continue;
+ }
+
+ // Handle built-in commands
+ if (clist.num == 1) {
+ if (strcmp(clist.commands[0].argv[0], EXIT_CMD) == 0) {
+ break;
+ } else if (strcmp(clist.commands[0].argv[0], "cd") == 0) {
+ if (clist.commands[0].argv[1]) {
+ if (chdir(clist.commands[0].argv[1]) != 0) {
+ perror("cd");
+ rc = ERR_CMD_ARGS_BAD;
+ }
+ }
+ continue;
+ } else if (strcmp(clist.commands[0].argv[0], "dragon") == 0) {
+ printDragon();
+ continue;
+ } else if (strcmp(clist.commands[0].argv[0], "rc") == 0) {
+ printf("%d\n", rc);
+ continue;
+ }
+ }
+
+ // Handle external commands with pipes
+ pid_t pids[CMD_MAX];
+ int pipes[CMD_MAX - 1][2];
+
+ // Create pipes
+ for (int i = 0; i < clist.num - 1; i++) {
+ if (pipe(pipes[i]) == -1) {
+ perror("pipe");
+ rc = ERR_MEMORY;
+ continue;
+ }
+ }
+
+ // Execute each command
+ for (int i = 0; i < clist.num; i++) {
+ pids[i] = fork();
+
+ if (pids[i] < 0) {
+ perror("fork");
+ rc = ERR_MEMORY;
+ break;
+ }
+
+ if (pids[i] == 0) { // Child process
+ // Set up pipe connections
+ if (i > 0) {
+ // Connect to previous pipe's read end
+ dup2(pipes[i-1][0], STDIN_FILENO);
+ }
+
+ if (i < clist.num - 1) {
+ // Connect to next pipe's write end
+ dup2(pipes[i][1], STDOUT_FILENO);
+ }
+
+ // Handle redirections by checking arguments
+ char *new_argv[CMD_ARGV_MAX];
+ int new_argc = 0;
+
+ for (int j = 0; j < clist.commands[i].argc; j++) {
+ char *arg = clist.commands[i].argv[j];
+
+ if (strcmp(arg, "<") == 0 && j + 1 < clist.commands[i].argc) {
+ // Input redirection
+ int fd = open(clist.commands[i].argv[j + 1], O_RDONLY);
+ if (fd == -1) {
+ perror("open input file");
+ exit(1);
+ }
+ dup2(fd, STDIN_FILENO);
+ close(fd);
+ j++; // Skip the filename
+ continue;
+ }
+
+ if (strcmp(arg, ">") == 0 && j + 1 < clist.commands[i].argc) {
+ // Output redirection (overwrite)
+ int fd = open(clist.commands[i].argv[j + 1], O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (fd == -1) {
+ perror("open output file");
+ exit(1);
+ }
+ dup2(fd, STDOUT_FILENO);
+ close(fd);
+ j++; // Skip the filename
+ continue;
+ }
+
+ if (strcmp(arg, ">>") == 0 && j + 1 < clist.commands[i].argc) {
+ // Output redirection (append)
+ int fd = open(clist.commands[i].argv[j + 1], O_WRONLY | O_CREAT | O_APPEND, 0644);
+ if (fd == -1) {
+ perror("open output file");
+ exit(1);
+ }
+ dup2(fd, STDOUT_FILENO);
+ close(fd);
+ j++; // Skip the filename
+ continue;
+ }
+
+ // If not a redirection operator, add to new argv
+ new_argv[new_argc++] = arg;
+ }
+ new_argv[new_argc] = NULL;
+
+ // Close all pipe ends in child
+ for (int j = 0; j < clist.num - 1; j++) {
+ close(pipes[j][0]);
+ close(pipes[j][1]);
+ }
+
+ // Execute the command with cleaned up arguments
+ execvp(new_argv[0], new_argv);
+ perror("execvp");
+ exit(errno);
+ }
+ }
+
+ // Parent process: close all pipe ends
+ for (int i = 0; i < clist.num - 1; i++) {
+ close(pipes[i][0]);
+ close(pipes[i][1]);
+ }
+
+ // Wait for all children to complete and get the last command's status
+ for (int i = 0; i < clist.num; i++) {
+ int status;
+ waitpid(pids[i], &status, 0);
+ if (i == clist.num - 1) { // Only use the last command's status
+ if (WIFEXITED(status)) {
+ rc = WEXITSTATUS(status);
+ } else {
+ rc = ERR_EXEC_CMD;
+ }
+ }
+ }
+
+ }
+
+ return OK;
+}
diff --git a/5-Shell-Part3/dshlib.h b/5-Shell-Part3/dshlib.h
new file mode 100644
index 0000000..fe3b246
--- /dev/null
+++ b/5-Shell-Part3/dshlib.h
@@ -0,0 +1,93 @@
+#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 command
+{
+ char exe[EXE_MAX];
+ char args[ARG_MAX];
+} command_t;
+
+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;
+*/
+
+typedef struct command_list{
+ int num;
+ cmd_buff_t commands[CMD_MAX];
+}command_list_t;
+
+//Special character #defines
+#define SPACE_CHAR ' '
+#define PIPE_CHAR '|'
+#define PIPE_STRING "|"
+
+#define SH_PROMPT "dsh3> "
+#define EXIT_CMD "exit"
+#define EXIT_SC 99
+
+//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);
+int close_cmd_buff(cmd_buff_t *cmd_buff);
+int build_cmd_list(char *cmd_line, command_list_t *clist);
+int free_cmd_list(command_list_t *cmd_lst);
+
+//built in command stuff
+typedef enum {
+ BI_CMD_EXIT,
+ BI_CMD_DRAGON,
+ BI_CMD_CD,
+ BI_NOT_BI,
+ BI_EXECUTED,
+} 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);
+int execute_pipeline(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
\ No newline at end of file
diff --git a/5-Shell-Part3/makefile b/5-Shell-Part3/makefile
new file mode 100644
index 0000000..b14b072
--- /dev/null
+++ b/5-Shell-Part3/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
\ No newline at end of file
diff --git a/5-Shell-Part3/questions.md b/5-Shell-Part3/questions.md
new file mode 100644
index 0000000..df8d14f
--- /dev/null
+++ b/5-Shell-Part3/questions.md
@@ -0,0 +1,65 @@
+1. Your shell forks multiple child processes when executing piped commands. How does your implementation ensure that all child processes complete before the shell continues accepting user input? What would happen if you forgot to call waitpid() on all child processes?
+
+Answer: My shell uses waitpid() in a loop to wait for each child process to finish before moving on. Basically, after forking all the processes needed for the pipe commands, the parent shell waits for each child to complete using their PIDs. This is important because:
+
+1. It stops zombie processes from building up - without waitpid(), dead processes would stick around as zombies
+2. It keeps things in order - the shell won't ask for new commands until the current ones are done
+3. It cleans up properly - all process resources get freed up when they're done
+
+If I forgot to use waitpid():
+- Zombie processes would pile up and waste system resources
+- The shell might ask for new commands while old ones are still running, which would be messy
+- Memory and other resources wouldn't get cleaned up properly
+- We wouldn't know if the commands actually worked or failed
+
+2. The dup2() function is used to redirect input and output file descriptors. Explain why it is necessary to close unused pipe ends after calling dup2(). What could go wrong if you leave pipes open?
+
+Answer: We need to close unused pipe ends after dup2() for a few key reasons:
+
+1. System Resources: Each open pipe takes up a file descriptor. If we don't close them, we're just wasting these limited resources.
+
+2. Avoiding Deadlocks: If we don't close read ends, processes will hang waiting for EOF since the write end is still open. Like if you do "cmd1 | cmd2" and don't close cmd1's write end, cmd2 will wait forever for more input.
+
+3. Memory Issues: Child processes inherit open file descriptors. Without closing them:
+ - Each new process gets copies of all previous open descriptors
+ - System resources get eaten up
+ - You could hit system limits
+
+4. Data Flow Problems: Leaving pipes open means:
+ - Programs never get their EOF signal
+ - Output gets stuck in buffers
+ - The pipeline gets clogged up
+
+So basically, closing unused pipe ends keeps things running smoothly and prevents the program from getting stuck or running out of resources.
+
+3. Your shell recognizes built-in commands (cd, exit, dragon). Unlike external commands, built-in commands do not require execvp(). Why is cd implemented as a built-in rather than an external command? What challenges would arise if cd were implemented as an external process?
+
+Answer: The cd command has to be built-in because it needs to change the shell's own working directory. Because:
+
+1. When you fork() a process, it gets its own copy of everything - including its current directory. If cd was an external command:
+ - It would change its own directory (the child process)
+ - But when it exits, the shell (parent process) would still be in the old directory
+ - So basically, cd wouldn't actually work!
+
+2. Other problems that would pop up:
+ - Relative paths would break since the shell wouldn't know where you cd'd to
+ - Scripts that use cd between commands wouldn't work right
+ - You'd have to do weird workarounds to tell the parent shell where you went
+
+4. Currently, your shell supports a fixed number of piped commands (CMD_MAX). How would you modify your implementation to allow an arbitrary number of piped commands while still handling memory allocation efficiently? What trade-offs would you need to consider?
+
+Answer: I'd switch from fixed arrays to dynamic memory allocation. Instead of:
+```c
+command_list_t commands[CMD_MAX];
+```
+I'd use:
+```c
+command_list_t *commands = malloc(size * sizeof(command_list_t));
+```
+
+Trade-offs:
+- Good: No more fixed limits on pipes
+- Bad: Slower (malloc/realloc overhead)
+- Bad: Harder to code (needs to manage memory)
+- Bad: Could eat up too much memory if there's way too many pipes
+
--
GitLab