diff --git a/assignments/assignment-3/questions.md b/assignments/assignment-3/questions.md index d1c7801727bd1471627b798f0ee880abdf45aec4..1a7511a820c5ffa1c3ce6bae4a84827e55d66aa2 100644 --- a/assignments/assignment-3/questions.md +++ b/assignments/assignment-3/questions.md @@ -1,15 +1,15 @@ 1. In this assignment I suggested you use `fgets()` to get user input in the main while loop. Why is `fgets()` a good choice for this application? - > **Answer**: _start here_ + > **Answer**: fgets() is a good choice for this application since it is ideal for reading input line by line. It will read up to size - 1 characters from the stream and stops when it encounters either an EOF or a newline. If a newline is encountered, it will store it into the buffer then add a terminating null byte after the last character in the buffer. 2. You needed to use `malloc()` to allocte memory for `cmd_buff` in `dsh_cli.c`. Can you explain why you needed to do that, instead of allocating a fixed-size array? - > **Answer**: _start here_ + > **Answer**: Using malloc() instead of just having a fixed size for the array will give us memory management since we aren't sure exactly how much memory to allocate except the max. However, if we only allocate the max, then it will become inefficient since we are allocating more memory than we may need. Also, a fixed size array may lead to a buffer overflow if the input exceeds the array's capacity. If we needed more memory in the future, then we already have malloc coded instead of going back to recode it. 3. In `dshlib.c`, the function `build_cmd_list(`)` must trim leading and trailing spaces from each command before storing it. Why is this necessary? If we didn't trim spaces, what kind of issues might arise when executing commands in our shell? - > **Answer**: _start here_ + > **Answer**: Incase the commands have trailing spaces inputted by user error, then this will negate those variables and make the program more consistent. 4. For this question you need to do some research on STDIN, STDOUT, and STDERR in Linux. We've learned this week that shells are "robust brokers of input and output". Google _"linux shell stdin stdout stderr explained"_ to get started. @@ -27,4 +27,4 @@ - How should our custom shell handle errors from commands that fail? Consider cases where a command outputs both STDOUT and STDERR. Should we provide a way to merge them, and if so, how? - > **Answer**: _start here_ \ No newline at end of file + > **Answer**: _start here_ diff --git a/assignments/assignment-4/fork-exec-1.png b/assignments/assignment-4/fork-exec-1.png new file mode 100644 index 0000000000000000000000000000000000000000..56186a921c27a47641e54efbb72397c544f93a32 Binary files /dev/null and b/assignments/assignment-4/fork-exec-1.png differ diff --git a/assignments/assignment-4/fork-exec-2.png b/assignments/assignment-4/fork-exec-2.png new file mode 100644 index 0000000000000000000000000000000000000000..30e5f4ff67c5baaaa99bf7fee75e4f2a7b802bde Binary files /dev/null and b/assignments/assignment-4/fork-exec-2.png differ diff --git a/assignments/assignment-4/questions.md b/assignments/assignment-4/questions.md new file mode 100644 index 0000000000000000000000000000000000000000..7a375bc0084b833cdbd20f65e01c0ca794f03b85 --- /dev/null +++ b/assignments/assignment-4/questions.md @@ -0,0 +1,41 @@ +1. Can you think of why we use `fork/execvp` instead of just calling `execvp` directly? What value do you think the `fork` provides? + + > **Answer**: We use fork/execvp instead of calling execvp directly because fork will make a new process that is a copy of the parent process, so when we put in a command, that child process will start doing it while the parent process is still running unaffected by anything. If we didn't do this, our parent process would just be replaced by the thing we are calling execvp on, so our shell would terminate. + +2. What happens if the fork() system call fails? How does your implementation handle this scenario? + + > **Answer**: If the fork() call fails, then that means that the OS wasn't able to create a new process for some reason. My code checks if the fork fails, and if it does, it writes a perror "Fork failed" and returns -1. + +3. How does execvp() find the command to execute? What system environment variable plays a role in this process? + + > **Answer**: execvp finds the command to execute by using the system's PATH environment variable. + +4. What is the purpose of calling wait() in the parent process after forking? What would happen if we didn’t call it? + + > **Answer**: The purpose of calling wait() is so that the parent process is able to synchronize with the child process and wait for it to exit before continuing its execution. If we did not call wait(), then we run into the risk of the parent continuint execution without knowing whether the child has finished its own process or not. + +5. In the referenced demo code we used WEXITSTATUS(). What information does this provide, and why is it important? + + > **Answer**: WEXITSTATUS() gives us the exit code of the child process. It is important because it lets the shell check if something executed successfully or has failed. + +6. Describe how your implementation of build_cmd_buff() handles quoted arguments. Why is this necessary? + + > **Answer**: My implementation parses through each character individually. Once it hits a ", it will then check whether the inside_quotes flag is set to 0 or 1. If it is set to 0, it will change the flag to 1, to indicate we are now inside of quotes, then iterate our i variable ++ to change to the next letter on the next runthrough, change start to i to indicate that we are at the start of the quote, and continue the loop. It will keep going, and eventually it will hit another ", in which it will then null terminate the current token and store it into the array, then toggle inside_quotes back to 0. Eventually, it will finishing going through all the letters, and then it will double check to make sure there's no more remaining tokens. Then it will set argc to the argument count and set the last entry in the array to NULL. This was necessary to make sure that no spaces inside of the quotes would get trimmed, and everything passes through as a proper element in the array. Like we don't want the input inside the quotes to be split up, so this will make sure that it won't. + +7. What changes did you make to your parsing logic compared to the previous assignment? Were there any unexpected challenges in refactoring your old code? + + > **Answer**: The biggest difference is that we have to parse through our input character by character instead of splitting the inputs based on where the pipes were. For some reason, I just couldn't get the parsing implementation to work. It kept making echo " Hello world " as [echo, ", hello, world] and would trim the spaces even though it was in double quotes. You'll probably see by my upload time, but I was up researching and figuring out how to fix it for 3 - 4 ish hours. + +8. For this quesiton, you need to do some research on Linux signals. You can use [this google search](https://www.google.com/search?q=Linux+signals+overview+site%3Aman7.org+OR+site%3Alinux.die.net+OR+site%3Atldp.org&oq=Linux+signals+overview+site%3Aman7.org+OR+site%3Alinux.die.net+OR+site%3Atldp.org&gs_lcrp=EgZjaHJvbWUyBggAEEUYOdIBBzc2MGowajeoAgCwAgA&sourceid=chrome&ie=UTF-8) to get started. + +- What is the purpose of signals in a Linux system, and how do they differ from other forms of interprocess communication (IPC)? + + > **Answer**: The purpose of signals is to inform a process about certain events or to control the behavior in a specific way. They differ from other IPC's because they are primarily meant to be an asynchronous notification that is used to alert a process about something rather than doing something large like transferring data or have synchronous communication. + +- Find and describe three commonly used signals (e.g., SIGKILL, SIGTERM, SIGINT). What are their typical use cases? + + > **Answer**: SIGKILL terminates a process immediately, and it can't be caught or ignored. This is usually used when a process is unresponsive or not terminating through normal means. SIGTERM is a request for the termination of a process, it can be blocked, handled, and ignored. It is sort of like a more polite way of asking the program to terminate. It is commonly used when you want a program to terminate gracefully. SIGINT is generated when someone types CTRL+C in the terminal, and it is used to interrupt a running process. It is commonly used for when you may have a long script that is printing a lot of statements and you want it to stop, instead of closing out of the terminal, you can just press CTRL + C and it will stop. + +- What happens when a process receives SIGSTOP? Can it be caught or ignored like SIGINT? Why or why not? + + > **Answer**: When a program receives SIGSTOP, it will be paused by the OS. It cannot be caught or ignored like SIGINT because it's meant for when you want to stop a program forcefully, but you don't want to outright kill it like SIGKILL, you just want it to pause for a bit before you resume it later. diff --git a/assignments/assignment-4/readme.md b/assignments/assignment-4/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..a0a38162c5704952d2bb1be57e4872f648c42858 --- /dev/null +++ b/assignments/assignment-4/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. + + diff --git a/assignments/assignment-4/starter/bats/assignment_tests.sh b/assignments/assignment-4/starter/bats/assignment_tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..253df7c5580fa9a2cbe5c7aab898ba96b7d4956b --- /dev/null +++ b/assignments/assignment-4/starter/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" ] +} diff --git a/assignments/assignment-4/starter/bats/student_tests.sh b/assignments/assignment-4/starter/bats/student_tests.sh new file mode 100755 index 0000000000000000000000000000000000000000..7478f37468da7715e6252ecb4ba20b171501c947 --- /dev/null +++ b/assignments/assignment-4/starter/bats/student_tests.sh @@ -0,0 +1,94 @@ +#!/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 "Echo command" { + run ./dsh <<EOF +echo CS283 +EOF + + expected_output="CS283" + + [ "$status" -eq 0 ] + [[ "$output" == *"$expected_output"* ]] +} + +@test "Quoted arguments" { + run ./dsh <<EOF +echo "Hello, my name is Ansh!" +EOF + + expected_output="Hello, my name is Ansh!" + + [ "$status" -eq 0 ] + [[ "$output" == *"$expected_output"* ]] +} + +@test "Change directory and Check pwd" { + run ./dsh <<EOF +cd /tmp +pwd +EOF + + expected_output="/tmp" + + + [ "$status" -eq 0 ] + [[ "$output" == *"$expected_output"* ]] +} + +@test "Starts and Exits" { + run ./dsh <<EOF +exit +EOF + [ "$status" -eq 0 ] +} + +@test "Check environment variables are listed" { + run ./dsh <<EOF +env +EOF + + expected_output="HOME=" + + [ "$status" -eq 0 ] + [[ "$output" == *"$expected_output"* ]] +} + +@test "Error rc command" { + run ./dsh <<EOF +cd /invalid/path +rc +EOF + [ "$status" -eq 0 ] + [[ "$output" == *"2"* ]] +} + +@test "Success rc command" { + run ./dsh <<EOF +ls +rc +EOF + [ "$status" -eq 0 ] + [[ "$output" == *"0"* ]] +} + +@test "Unknown command" { + run ./dsh <<EOF +unknown +EOF + + [ "$status" -eq 0 ] + [[ "$output" == *"$expected_output"* ]] +} diff --git a/assignments/assignment-4/starter/dragon.c b/assignments/assignment-4/starter/dragon.c new file mode 100644 index 0000000000000000000000000000000000000000..83d01f243de7c7c98767c7dfd328588ec1f4b292 --- /dev/null +++ b/assignments/assignment-4/starter/dragon.c @@ -0,0 +1,6 @@ +#include <stdio.h> + +// EXTRA CREDIT - print the drexel dragon from the readme.md +extern void print_dragon(){ + // TODO implement +} diff --git a/assignments/assignment-4/starter/dsh b/assignments/assignment-4/starter/dsh new file mode 100755 index 0000000000000000000000000000000000000000..bd10688d34360b8801fc74dcae28b4d45917c695 Binary files /dev/null and b/assignments/assignment-4/starter/dsh differ diff --git a/assignments/assignment-4/starter/dsh_cli.c b/assignments/assignment-4/starter/dsh_cli.c new file mode 100644 index 0000000000000000000000000000000000000000..9262cf457e6b235b19999efa04153e1de0962255 --- /dev/null +++ b/assignments/assignment-4/starter/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/assignments/assignment-4/starter/dshlib.c b/assignments/assignment-4/starter/dshlib.c new file mode 100644 index 0000000000000000000000000000000000000000..f67703bab7ad4cb7df6e48d6a04df12386bb7213 --- /dev/null +++ b/assignments/assignment-4/starter/dshlib.c @@ -0,0 +1,261 @@ +#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 "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() + */ + + +Built_In_Cmds match_command(const char *input) { + if(strcmp(input, EXIT_CMD) == 0) { + return BI_CMD_EXIT; + } + else if(strcmp(input, "dragon") == 0) { + return BI_CMD_DRAGON; + } + else if(strcmp(input, "cd") == 0) { + return BI_CMD_CD; + } + else if(strcmp(input, "rc") == 0) { + return BI_RC; + } + else { + return BI_NOT_BI; + } +} + +Built_In_Cmds exec_built_in_cmd(cmd_buff_t *cmd) { + if(cmd->argc == 0) { + return BI_NOT_BI; + } + + if(strcmp(cmd->argv[0], "cd") == 0) { + if(cmd->argc == 2) { + if(chdir(cmd->argv[1]) == -1) { + perror("cd failed"); + } + } + + return BI_CMD_CD; + } + + return BI_NOT_BI; +} + +void trim_spaces(char *str) { + int start = 0; + int end = strlen(str) - 1; + int inside_quotes = 0; + + while(str[start] == ' ' && start <= end) { + start++; + } + + while(str[end] == ' ' && end >= start) { + end--; + } + + if(start > end) { + str[0] = '\0'; + return; + } + + int i = start; + int j = start; + + while(i <= end) { + if(str[i] == '"') { + inside_quotes = !inside_quotes; + } + + if(!inside_quotes && str[i] == ' ' && (i == 0 || str[i - 1] == ' ')) { + i++; + continue; + } + + str[j++] = str[i++]; + + } + + str[j] = '\0'; + +} + +int parse_cmd_buff(char *cmd_buff, cmd_buff_t *cmd_buff_struct) { + int i = 0; + int j = 0; + int start = -1; + int inside_quotes = 0; + int length = strlen(cmd_buff); + + cmd_buff_struct->_cmd_buffer = cmd_buff; + + while(i < length) { + char current_char = cmd_buff[i]; + + if(current_char == '"') { + if(inside_quotes) { + + cmd_buff[i] = '\0'; + + if(start != -1) { + cmd_buff_struct->argv[j] = &cmd_buff[start]; + j++; + } + + inside_quotes = 0; + start = -1; + + } + else { + inside_quotes = 1; + i++; + start = i; + continue; + } + } + else if(inside_quotes || current_char != ' ') { + if(start == -1) { + start = i; + } + } + else if(!inside_quotes && current_char == ' ') { + if(start != -1) { + cmd_buff[i] = '\0'; + cmd_buff_struct->argv[j] = &cmd_buff[start]; + j++; + start = -1; + } + } + + i++; + + } + + if(start != -1) { + cmd_buff_struct->argv[j++] = &cmd_buff[start]; + } + + cmd_buff_struct->argv[j] = NULL; + + cmd_buff_struct->argc = j; + + return 0; +} + +int exec_cmd(cmd_buff_t *cmd) { + pid_t pid = fork(); + + if(pid == -1) { + perror("Fork failed"); + return -1; + } + + if(pid == 0) { + if(execvp(cmd->argv[0], cmd->argv) == -1) { + perror("execvp failed"); + exit(1); + } + } + else { + int status; + waitpid(pid, &status, 0); + } + + return OK; +} + + +int exec_local_cmd_loop() +{ + char *cmd_buff = malloc(SH_CMD_MAX * sizeof(char)); + int rc = 0; + cmd_buff_t cmd_buff_struct; + + cmd_buff_struct.argc = 0; + + while(1) { + printf("%s", SH_PROMPT); + + if(fgets(cmd_buff, ARG_MAX, stdin) == NULL) { + printf("\n"); + break; + } + + cmd_buff[strcspn(cmd_buff, "\n")] = '\0'; + + trim_spaces(cmd_buff); + + if (strcmp(cmd_buff, EXIT_CMD) == 0) { + free(cmd_buff); + exit(0); + } + + rc = parse_cmd_buff(cmd_buff, &cmd_buff_struct); + + switch (rc) { + case OK: + if(exec_built_in_cmd(&cmd_buff_struct) == BI_NOT_BI) { + exec_cmd(&cmd_buff_struct); + } + break; + case WARN_NO_CMDS: + printf(CMD_WARN_NO_CMD); + break; + case ERR_TOO_MANY_COMMANDS: + printf(CMD_ERR_PIPE_LIMIT, CMD_MAX); + break; + } + } + + free(cmd_buff); + + return OK; +} diff --git a/assignments/assignment-4/starter/dshlib.h b/assignments/assignment-4/starter/dshlib.h new file mode 100644 index 0000000000000000000000000000000000000000..32059ccede7dec47a785b55185b1f5dda105145d --- /dev/null +++ b/assignments/assignment-4/starter/dshlib.h @@ -0,0 +1,79 @@ +#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); + +//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/assignments/assignment-4/starter/makefile b/assignments/assignment-4/starter/makefile new file mode 100644 index 0000000000000000000000000000000000000000..b14b0723e23ea2036f0220406b40a93e6a0cd283 --- /dev/null +++ b/assignments/assignment-4/starter/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/assignments/assignment-4/starter/shell_roadmap.md b/assignments/assignment-4/starter/shell_roadmap.md new file mode 100644 index 0000000000000000000000000000000000000000..9af14341f8cd435e39cfa8c1bc6666428be203dd --- /dev/null +++ b/assignments/assignment-4/starter/shell_roadmap.md @@ -0,0 +1,13 @@ +## Proposed Shell Roadmap + +Week 5: Shell CLI Parser Due + +Week 6: Execute Basic Commands + +Week 7: Add support for pipes (extra-credit for file redirection) + +Week 8: Basic socket assignment + +Week 9: Remote Shell Extension + +Week 11: Add support for something like `nsenter` to enter a namespace \ No newline at end of file