basic-shell / shell_helper.c
shell_helper.c
Raw
#define  _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <stdbool.h>
#include "tokenize.h"
#include "shell_helper.h"

/* This is a shell helper class that contains the main functionality of
 * our shell and performs built-in commands and operations. */

/* Closes a file descriptor, and if there was an error, halts the program. */
int assert_close(int fd) {
	if(close(fd) == -1) {
		perror("Error closing file descriptor");
		exit(1);
	}
}

/* Free all of the data within the command array. */
void free_data(char** cmd, int* cmdlen) {
	for (int i = 0; i < *cmdlen; i++) {
		free(cmd[i]);
		i++;
	}
}

/* Exits the program by printing message and frees data. */
void bye_bye(char** cmd, int* cmdlen) {
	printf("Bye bye.\n");
	free_data(cmd, cmdlen);
	exit(0);
}

/* Traverses given input and then adds each in to the cmd vector for later execution. */
void store_cmds(char* in, char** cmd, int* cmdlen) {
	// remove new line character
	in[strcspn(in, "\n")] = 0;
	vect_t* temp = create_tokens(in);
	for (int i = 0; i < vect_size(temp); i++) {
		cmd[i] = vect_get_copy(temp, i);
	}
	cmd[vect_size(temp)] = NULL;
	*cmdlen = vect_size(temp);
	vect_delete(temp);
}

/* Sends invalid argument message for requested command. */
void invalid_args() {
	printf("Given command has an invalid number of arguments\n");
}

/* Updates the previous command using current input. */
void change_prev_cmd(char* in, char* prev) {
	for (int i = 0; i < MAX_SIZE; i++) {
		prev[i] = in[i];
	}
}

/* In a child process, attempt to execute the commands. */
void exec_chld(int current_cmd, int last_cmd, char*** cmd_sequence, char* file_to_read, char* file_to_write, int* parent_pipe_fd) {
	pid_t pid = fork();
	if (pid == 0) {
		// If there are more commands in the pipe chain, do the recursive piping
		if (current_cmd > 0) {
			int pipe_fd[2];
			pipe(pipe_fd);
			exec_chld(current_cmd - 1, last_cmd, cmd_sequence, file_to_read, file_to_write, pipe_fd);
			assert_close(STDIN_FILENO);
			dup2(pipe_fd[0], STDIN_FILENO);
			assert_close(pipe_fd[0]);
			assert_close(pipe_fd[1]);
		}
		// If this is a child, send its output to the pipe of the parent above
		if (parent_pipe_fd != NULL) {
			assert_close(STDOUT_FILENO);
			dup2(parent_pipe_fd[1], STDOUT_FILENO);
			assert_close(parent_pipe_fd[0]);
			assert_close(parent_pipe_fd[1]);
		}
		// If this is command1 in the pipe chain, it may be able to read from a file
		if (current_cmd == 0 && file_to_read != NULL) {
			int fd_in = open(file_to_read, O_RDONLY);
			assert_close(STDIN_FILENO);
			dup2(fd_in, STDIN_FILENO);
			assert_close(fd_in);
		}
		// If this is the last command in the pipe chain, it may be able to write to a file
		if (current_cmd == last_cmd && file_to_write != NULL) {
			int fd_out = open(file_to_write, O_WRONLY | O_CREAT | O_TRUNC, 0644);
			assert_close(STDOUT_FILENO);
			dup2(fd_out, STDOUT_FILENO);
			assert_close(fd_out);
		}
		// Execute this command
		if (execvp(cmd_sequence[current_cmd][0], cmd_sequence[current_cmd]) == -1) {
			printf("%s: command could not be executed properly\n", cmd_sequence[current_cmd][0]);
			exit(1);
		} 
	}
	else {
		waitpid(pid, NULL, 0);
	}
}

/* Changes current working directory of the shell to the path specified. */
void change_directory(char** cmd) {
	char* new_dir = malloc(MAX_SIZE * sizeof(char));
	new_dir = strncpy(new_dir, cmd[1], strlen(cmd[1]) + 1);
	// shift directories to d
	chdir(new_dir);
	free(new_dir);
}

/* Explains all the built-in commands available in our shell. */
void explain_cmds() {
	printf("\'%s\'  :  %s\n", "cd", "Changes the current working directory of the shell to the path specified as the argument");
	printf("\'%s\'  :  %s\n", "source", "Executes a script, by taking filename as argument and then processing each line of the file as a command");
	printf("\'%s\'  :  %s\n", "prev", "Prints the previous command line and executes it again");
	printf("\'%s\'  :  %s\n", "help", "Explains all the built-in commands available in our shell");
}

/* In the event of invalid piping or redirection, print an error message and free the memory. */
void invalid_piping_redirection(char*** cmd_sequence, int current_cmd) {
	printf("Given command has an invalid number of arguments\n");
	for (int i = 0; i <= current_cmd; i++) {
		free(cmd_sequence[i]);
	}
}

/* Executes a "source" command (running a list of commands from a file). */
void execute_source_command(char** cmd) {
	// create holders
	char* n_cmd[MAX_SIZE];
	int n_cmdlen;
	char* line = malloc(MAX_SIZE * sizeof(char));
	char last_line[MAX_SIZE];
	
	// file pointer
	FILE *fp;
	// open given file in read mode
	fp = fopen(cmd[1], "r");

	// execute lines one by one
	size_t max_length = MAX_SIZE;
	while(getline(&line, &max_length, fp) != EOF) {
		store_cmds(line, n_cmd, &n_cmdlen);
		if (strcmp(n_cmd[0], "exit") == 0) {
			free_data(n_cmd, &n_cmdlen);
			break;
		}
		execute_command_helper(n_cmd, &n_cmdlen, line, last_line);
		free_data(n_cmd, &n_cmdlen);
	}
	fclose(fp);
	free(line);
}

/* Executes a system command (that is, all commands other than built-in commands). */
void execute_system_command(char** cmd, int num_args) {
	// setting up initial values for sequence of piping and redirection
	char** cmd_sequence[MAX_SIZE];
	char* file_to_read = NULL;
	char* file_to_write = NULL;
	int current_cmd = 0;
	cmd_sequence[0] = malloc(MAX_SIZE * sizeof(char*));
	int current_len = 0;
	// splitting cmd by "|" tokens into cmd_sequence,
	// and allowing ">" and "<" where they don't contradict "|"
	for (int i = 0; i < num_args; i++) {
		if (strcmp(cmd[i], "|") == 0) {
			if (current_len == 0) { // if you get 2 pipes in a row, or start with a pipe, error
				invalid_piping_redirection(cmd_sequence, current_cmd);
				return;
			}
			cmd_sequence[current_cmd][current_len] = NULL;
			current_cmd++;
			cmd_sequence[current_cmd] = malloc(MAX_SIZE * sizeof(char*));
			current_len = 0;
		}
		else if (strcmp(cmd[i], ">") == 0) {
			// Error if there's a ">" that's not the second-to-last token in the command
			if (i + 2 != num_args) {
				invalid_piping_redirection(cmd_sequence, current_cmd);
				return;
			}
			file_to_write = cmd[i + 1];
			i++;
		}
		else if (strcmp(cmd[i], "<") == 0) {
			// Error if there's a "<" that's not the second-to-last token in the first section of piping
			if (current_cmd != 0 || i + 2 > num_args || strcmp(cmd[i + 1], "|") == 0 || (i + 2 < num_args && strcmp(cmd[i + 2], "|") != 0)) {
				invalid_piping_redirection(cmd_sequence, current_cmd);
				return;
			}
			file_to_read = cmd[i + 1];
			i++;
		}
		else {
			cmd_sequence[current_cmd][current_len] = cmd[i];
			current_len++;
		}
	}
	if (current_len == 0) { // if you end with a pipe, error
		invalid_piping_redirection(cmd_sequence, current_cmd);
		return;
	}
	cmd_sequence[current_cmd][current_len] = NULL;
	exec_chld(current_cmd, current_cmd, cmd_sequence, file_to_read, file_to_write, NULL);
	for (int i = 0; i <= current_cmd; i++) {
		free(cmd_sequence[i]);
	}
}

/* Executes the given command and calls necessary helpers for further implementation.
 * Also returns a value representing whether the program should exit. */
int execute_command_helper(char** cmd, int* cmdlen, char* in, char* prev) {
	// keep track of number of arguments given
	int num_args = *cmdlen;

    // if there's no command, do nothing
    if (num_args == 0) {
		return 0;
    }
	// execute exit command
	else if (strcmp(cmd[0], "exit") == 0) {
		return 1;
	}
	// execute help command
	else if (strcmp(cmd[0], "help") == 0) {
		explain_cmds();
		// make sure to update our previous command to be help
		change_prev_cmd(in, prev);
	}
	// execute previous command
	else if (strcmp(cmd[0], "prev") == 0) {
		if (num_args != 1) {
			invalid_args();
		}
		else {
			// print previous command line
			printf("%s\n", prev);
			// now, execute it again
			char* p_cmd[MAX_SIZE];
			int p_cmdlen;
			store_cmds(prev, p_cmd, &p_cmdlen);
			execute_command_helper(p_cmd, &p_cmdlen, prev, prev);
			// free data once done
			free_data(p_cmd, &p_cmdlen);
			// don't update our previous command to be prev, because we can't prev prev
		}
	}
	// if cd command to change directory
	else if (strcmp(cmd[0], "cd") == 0) {
		// expects either 1 or 2 args
		if (num_args > 2) {
			invalid_args();
		} else if (num_args == 1) {
			chdir("..");
			change_prev_cmd(in, prev);
		} else {
			change_directory(cmd);
			// make sure to update our previous command to be cd
			change_prev_cmd(in, prev);
		}
	}
	// execute commands from file
	else if (strcmp(cmd[0], "source") == 0) {
		// we want exactly 2 args here (source, filename)
		if (num_args != 2) {
			invalid_args();
		} else {
			execute_source_command(cmd);
		}
		// make sure to update prev command to source
		change_prev_cmd(in, prev);
	}
	// execute a non-built-in command using the system path, with piping/redirection
	else {
		// piping/redirection do not need to support built-in commands, so we can implement them in here
		execute_system_command(cmd, num_args);
		// we update prev command normally with the assumption that we didn't use piping/redirection,
		// because that doesn't need to be supported
		change_prev_cmd(in, prev);
	}
	return 0;
}

/* Executes the given sequence of commands, by splitting the input on ";" tokens.
 * Also returns a value representing whether the program should exit. */
int execute_sequence(char** cmd, int* cmdlen, char* in, char* prev) {
	// if there's no command, do nothing
    if (*cmdlen == 0) {
		return 0;
    }
	// we don't need to create copies of in and prev, because sequencing doesn't need to support prev
	char** cmd_sequence[MAX_SIZE];
	int cmdlen_sequence[MAX_SIZE];
	// setting up initial values
	int current_cmd = 0;
	cmd_sequence[0] = malloc(MAX_SIZE * sizeof(char*));
	int current_len = 0;
	cmdlen_sequence[0] = 0;
	// splitting cmd by ";" tokens into cmd_sequence
	for (int i = 0; i < *cmdlen; i++) {
		if (strcmp(cmd[i], ";") == 0) {
			cmd_sequence[current_cmd][current_len] = NULL;
			current_cmd++;
			cmd_sequence[current_cmd] = malloc(MAX_SIZE * sizeof(char*));
			current_len = 0;
			cmdlen_sequence[current_cmd] = 0;
		}
		else {
			cmd_sequence[current_cmd][current_len] = cmd[i];
			current_len++;
			cmdlen_sequence[current_cmd]++;
		}
	}
	// if the last token isn't a semicolon, you still need to end the command properly
	if (strcmp(cmd[(*cmdlen) - 1], ";") != 0) {
		cmd_sequence[current_cmd][current_len] = NULL;
		current_cmd++;
		cmd_sequence[current_cmd] = malloc(MAX_SIZE * sizeof(char*));
		current_len = 0;
		cmdlen_sequence[current_cmd] = 0;
	}
	// executing each command separately
	for (int i = 0; i < current_cmd; i++) {
		// if a command is "exit", free all the remaining commands' memory and return 1 to exit
		if (execute_command_helper(cmd_sequence[i], cmdlen_sequence + i, in, prev) == 1) {
			while (i <= current_cmd) {
				free(cmd_sequence[i]);
				i++;
			}
			return 1;
		}
		free(cmd_sequence[i]);
	}
	free(cmd_sequence[current_cmd]);
	return 0;
}