feat: implemented pipes and redirects + background operator

This commit is contained in:
Djairo Hougee 2024-03-16 23:07:19 +01:00
parent 6b9bedac3c
commit 7e6c1147db
10 changed files with 246 additions and 74 deletions

View File

@ -4,7 +4,7 @@ shell:
gcc -std=c99 -Wall -pedantic main.c scanner.c shell.c cmd.c -o shell
bonus:
gcc -std=c99 -Wall -DEXT_PROMPT -pedantic main.c scanner.c shell.c cmd.c -o shell
gcc -std=c99 -Wall -DEXT_PROMPT -pedantic main.c scanner.c shell.c cmd.c -o shell -lreadline
clean:
rm -f *~

View File

@ -1,3 +1,5 @@
Bonuses added for lab 3 are at the bottom of this document, starting [here](#navigable-arrow-keys).
# general code structure
The main objective of this assignment was to tokenize given inputs and process the resulting tokens according to the given grammar. We decided to seperate the two steps of tokenizing and processing almost entirely, which decouples the input parser from the execution very neatly. The one drawback of this method is that each input is, more or less, processed twice - but we considered this a fair trade-off, as the input for a shell can generally be expected to not be incredulously large.
@ -23,8 +25,6 @@ Builtin | Result
--- | ---
status | prints the return status of the last run command.
exit | exits the shell.
true | sets status to `0`.
false | sets status to `-1`.
cd | changes the current working directory.
debug | prints debugging information to `stdout`.
@ -65,11 +65,13 @@ typedef struct CommandList {
} CommandList;
```
The `Chain` struct models one complete chain to be executed. It is currently a bit barebones, but will be expanded when we implement piping, redirection, and running in the background.
The `Chain` struct models one complete chain to be executed. It conains a list of commands to execute, and extra information related to I/O - any redirects. These are set to `NULL` if no redirect was given.
```c
typedef struct Chain {
CommandList commands;
bool runInBackground;
char *in;
char *out;
} Chain;
```
@ -90,8 +92,6 @@ The execution is done within a while loop that runs until the `cpy` list is enti
- COMPOSITION
- BUILTIN
// TODO: discuss what the process is for each enum type.
If the code encounters any other types at this level, it means that the syntax of the input is incorrect. If this happens, an error message is printed out and `cpy` is set to NULL in order to exit the loop.
In the end, we use `free()` and `freeTokenList()` to free the memory.
@ -163,10 +163,40 @@ The end result looks like this:
![Our own (less impressive) prompt](./assets/prompt.png)
## true / false
The smallest of our extra implementations, we have added two `builtin`s which directly modify `status`.
* The builtin `true` will set `status` to `0`.
* The builtin `false` will set `status` to `-1`.
## navigable arrow keys
Another standard shell feature, available in everything from the modern fish and zsh, to shells as far back as sh, is that of the arrow key. Because our shell takes input using `getchar()` the user is unable to navigate back in an unfinished prompt. By switching to the GNU readline library, we can make use of the `readline()` function - allowing us to navigate an unfinished prompt in the expected manner.
Really this was more of an implementation to allow us to do proper debugging, but it's still something we added to the shell that was not specifically part of the assignment, so we decided we may as well document it here.
This is a bit hard to illustrate using text/images, so we recommend you build the shell yourself if you wish to play around with this a little bit.
## tab completion
Another perk of the GNU readline library is it quite neatly allows for tab completion, bash-style. Use tab while entering a command and watch as the shell tries its best to complete it for you.
Note that this only works on filenames - the bash-style tab-completion of variables and executable commands is not implemented. Included below is an example; after entering 'cat' we then pressed tab to showcase the completion, which prints all (matching) files in CWD to the shell.
![tab completion on filesnames](./assets/tab_completion.png)
## background operation
Everyone who has ever used a shell before will be familiar with the background operator, but here's a quick recap anyay: Whenever a command is suffixed with a single ampersand (`&`), it is run in the background and control of the shell is immediately returned to the user. The output streams of the command are still connected to the shell, (ergo, `echo 7 &` will still print '7' to stdout, which might mess with the prompt a little), but the command is run as non-blocking.
Because of the structured approach we took towards chain/command design, it was trivially easy for us to implement this bonus: a simple if statement (checking for the background operator) and we are good to go!
The relevant snippet of code lives in `executeChain` and reads as follows:
```c
// wait on children
int stat;
#if EXT_PROMPT
if (chain.runInBackground) return;
#endif
for (int i=0; i < numCommands; i++) {
waitpid(pids[i], &stat, 0);
if (WIFEXITED(stat)) {
*status = WEXITSTATUS(stat);
}
}
```
<!-- ## command history -->
<!-- Similarly, by using the GNU history header, we are able to implement a simple command history using a linked list structure. Try pressing the 'up' and 'down' arrow keys to navigate your command history. -->

BIN
assets/tab_completion.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

184
cmd.c
View File

@ -3,6 +3,8 @@
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <fcntl.h>
#include <unistd.h>
#include "scanner.h"
@ -35,6 +37,8 @@ Chain _newChain() {
Chain chain;
chain.commands = _newCommandList();
chain.runInBackground = false;
chain.in = NULL;
chain.out = NULL;
return chain;
}
@ -122,6 +126,21 @@ void _insertCommand(Command cmd, CommandList *list) {
list->commands[list->numCommands++] = cmd;
}
void addRedirect(List *lp, Chain *chain) {
// exit on invalid syntax; should be unreachable.
if ((*lp)->type != REDIRECT || (*lp)->next->type != FILENAME) exit(1);
if ((*lp)->t[0] == '>' ) {
// out redirect
chain->out = (*lp)->next->t;
} else {
// in redirect
chain->in = (*lp)->next->t;
}
*lp = (*lp)->next;
*lp = (*lp)->next;
}
/**
* This function build up one chain of commands from a given listPointer.
* It parses one 'unit' of executables, meaning any number of commands+options chained together by pipes and redirects.
@ -152,9 +171,10 @@ Chain buildChain(List *lp) {
*lp = (*lp)->next;
break;
// finding a redirect means a bunch of things we haven't thought out yet.
// finding a redirect means we need to take this into account during chain execution;
// add the redirect to the chain struct, and we can deal with it down the line.
case REDIRECT:
//TODO: implement redirect
addRedirect(lp, &chain);
break;
// finding a background operator means two things:
@ -179,48 +199,113 @@ Chain buildChain(List *lp) {
return chain;
}
/**
* This function executes a given command in a child process.
* To make this a blocking operation, the parent process will wait until the child exits - simultaneously updating the status variable.
* @param cmd the Command to execute.
* @param status variable which will contain the exit status of the command.
*/
void _executeCommand(Command cmd, int* status) {
pid_t pid = fork();
switch (pid) {
// fork failed.
case -1:
printf("ERROR: failed to create child!\n");
*status = -1;
break;
case 0:
// child process
execvp(cmd.command, cmd.arguments);
//this line is only ever reached if execvp fails (for example, when an executable can't be found).
exit(127);
break;
default:;
// parent process; wait for child and update status.
int stat;
wait(&stat);
if (WIFEXITED(stat)) {
*status = WEXITSTATUS(stat);
}
}
}
// TODO: extend to include I/O redirection and background operation
/**
* This function executes all commands in a given chain.
* It updates the status as it does so.
* This function is barebones at the moment, but will be expanded to include support for redirection and non-blocking execution.
* The function uses the variables chain.in and chain.out for redirection, if necessary.
* Additionally it supports non-blocking execution through the chain,runInBackground attribute.
* @param chain the Chain to execute.
* @param status variable which will contain the exit status after execution.
*/
void executeChain(Chain chain, int* status) {
for (int i=0; i < chain.commands.numCommands; i++) {
_executeCommand(chain.commands.commands[i], status);
// variables for clarity
int numCommands = chain.commands.numCommands;
int numPipes = chain.commands.numCommands-1;
pid_t pids[numCommands];
Command cmd;
// error if input and output are identical files.
if (chain.in != NULL && chain.out != NULL && !strcmp(chain.in, chain.out)) {
printf("Error: input and output files cannot be equal!\n");
*status = 2;
return;
}
// pipe creation
int *pipes = calloc(numPipes*2, sizeof(int));
for (int i=0; i < numPipes; i++) {
if (pipe(pipes + i*2) < 0) {
perror("ERROR: failed to create pipes!");
return;
}
}
// command execution
for (int i=0; i < numCommands; i++) {
cmd = chain.commands.commands[i];
pids[i] = fork();
switch (pids[i]) {
case -1:
printf("ERROR: failed to create child!\n");
*status =-1;
break;
case 0:
// child process
// if not first command, connect input to previous output
if (i > 0) {
if (dup2(pipes[(i-1)*2], 0) < 0) {
perror("ERROR: failed to connect pipes!");
return;
}
} else if (chain.in != NULL) {
// in redirection
int fd0 = open(chain.in, O_RDONLY, 0);
if (dup2(fd0, 0) < 0) {
perror("ERROR: failed to redirect input!");
return;
}
close(fd0);
}
// if not last command, connect output to next input
if (i+1 < numCommands) {
if (dup2(pipes[i*2+1], 1) < 0) {
perror("ERROR: failed to connect pipes!");
return;
}
} else if (chain.out != NULL) {
// output redirection
int fd1 = open(chain.out, O_WRONLY|O_CREAT, 0755);
if (dup2(fd1, 1) < 0) {
perror("ERROR: failed to redirect output!");
return;
}
close(fd1);
}
// close all pipes
for (int i = 0; i < numPipes; i++) {
close(pipes[i*2]);
close(pipes[i*2+1]);
}
execvp(cmd.command, cmd.arguments);
// this line is only ever reached if execvp fails (for example, when an executable can't be found).
exit(127);
break;
default:;
// parent process
break;
}
}
// close pipes
for (int i = 0; i < numPipes; i++) {
close(pipes[i*2]);
close(pipes[i*2+1]);
}
// wait on children
int stat;
#if EXT_PROMPT
if (chain.runInBackground) return;
#endif
for (int i=0; i < numCommands; i++) {
waitpid(pids[i], &stat, 0);
if (WIFEXITED(stat)) {
*status = WEXITSTATUS(stat);
}
}
}
@ -237,26 +322,27 @@ bool executeBuiltin(Command cmd, int *status, bool *debug) {
bool executeBuiltin(Command cmd, int *status) {
#endif
if (!strcmp(cmd.command, "status")) {
printf("The most recent exit code is: %d.\n", *status);
printf("The most recent exit code is: %d\n", *status);
} else if (!strcmp(cmd.command, "exit")) {
return false;
} else if (!strcmp(cmd.command, "true")) {
*status = 0;
} else if (!strcmp(cmd.command, "false")) {
*status = 1;
#if EXT_PROMPT
} else if (!strcmp(cmd.command, "debug")) {
*debug = ! *debug;
printf("Toggled debug to %d.\n", *debug);
} else if (!strcmp(cmd.command, "cd")) {
if (cmd.numArguments == 2) {
char *PWD = getenv("HOME");
*status = chdir(PWD);
printf("Error: cd requires folder to navigate to!\n");
*status = 2;
} else {
*status = chdir(cmd.arguments[1]);
if (*status) {
printf("Error: cd directory not found!\n");
*status = 2;
}
}
}
#if EXT_PROMPT
else if (!strcmp(cmd.command, "debug")) {
*debug = ! *debug;
printf("Toggled debug to %d.\n", *debug);
}
#endif
}// can be expanded by growing the if/else chain.
return true;
}

3
cmd.h
View File

@ -29,6 +29,8 @@ typedef struct CommandList {
typedef struct Chain {
CommandList commands;
bool runInBackground;
char *in;
char *out;
} Chain;
@ -36,6 +38,7 @@ void freeCommand(Command cmd);
void freeChain(Chain chain);
Command buildCommand(List *lp);
void addRedirect(List *lp, Chain *chain);
Chain buildChain(List *lp);
void executeChain(Chain chain, int* status);

55
main.c
View File

@ -1,4 +1,3 @@
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
@ -12,11 +11,15 @@
#include "cmd.h"
// general TODO:
// * properly bind stdin/stdout for piped commands
// * run chains in bg if given '&'
// * manage < and > redirects
// * signal handling
// * builtins - 'kill'
// * simple command history
// * run scripts with argv[]
// * redirect stderr with the n> operator
// * allow multiple redirects (turn chain.in and chain.out into char**)
//
// Big rewrite: create a 'shell' struct which is the actual program to run
// this struct would take care of things like command history, signal handling, etc
/**
* This function skips one COMMAND, including redirects and pipes (if any).
@ -44,7 +47,7 @@ void skipCommand(List *lp) {
}
/**
* This function checks whether the current COMPOSITION operator passes (and thus should execute the next function).
* This function checks whether the current COMPOSITION operator passes (and thus should execute the next command).
* @param s the COMPOSITION operator.
* @param status the exit status of the last ran command.
* @return whether the COMPOSITION succeeds/whether to execute the next command.
@ -57,17 +60,48 @@ bool compositionPasses(char *s, int status) {
return false;
}
#if EXT_PROMPT
int execute_script(char* script) {
FILE* file = fopen(script, "r");
if (file == NULL) {
printf("Error: provided Scriptfile, '%s', does not exist or is inaccessible!\n", script);
return 1;
}
char line[512];
while (fgets(line, sizeof(line), file)) {
printf("%s", line);
//TODO: continue working here on the script functionality later, after the Refactor
}
fclose(file);
return 0;
}
#endif
/**
* Driver function. Contains the main loop which collects and parses input, and decides what and how to execute + output.
*/
int main(int argc, char *argv[]) {
#if EXT_PROMPT
if (argc >= 2) {
if (argc > 2) {
printf("Error: unexpected number of arguments!\n");
return 0;
} else {
if (execute_script(argv[1])) return 1;
}
}
#endif
char *inputLine;
List tokenList;
int status = 0;
bool do_loop = true;
#if EXT_PROMPT
bool debug = false;
char cwd[101], *usr;
char cwd[128], *usr, prompt[256];
#endif
// Disable buffering so we don't have to deal with out-of-order prints.
@ -80,10 +114,13 @@ int main(int argc, char *argv[]) {
#if EXT_PROMPT
getcwd(cwd, sizeof(cwd));
usr = getlogin();
if (!status) printf("\x1b[33m%s \x1b[36m%s \x1b[32m>\x1b[0m ", usr, cwd);
if ( status) printf("\x1b[33m%s \x1b[36m%s \x1b[31m>\x1b[0m ", usr, cwd);
#endif
if (!status) snprintf(prompt, 200*sizeof(char), "\x1b[33m%s \x1b[36m%s \x1b[32m>\x1b[0m ", usr, cwd);
if ( status) snprintf(prompt, 200*sizeof(char), "\x1b[33m%s \x1b[36m%s \x1b[31m>\x1b[0m ", usr, cwd);
inputLine = readInputLine(prompt);
#else
inputLine = readInputLine();
#endif
// We have modified the readInputLine function to return NULL on EOF.
if (inputLine == NULL) {
do_loop = false;

View File

@ -7,7 +7,17 @@
#include "scanner.h"
//TODO: handle EOF more completely (currently only EOF at the start of the line is handled)
#if EXT_PROMPT
#include <readline/readline.h>
/**
* Reads an inputline from stdin.
* @return a string containing the inputline.
*/
char *readInputLine(char const *prompt) {
return readline(prompt);
}
#else
/**
* Reads an inputline from stdin.
* @return a string containing the inputline.
@ -43,6 +53,7 @@ char *readInputLine() {
s[i] = '\0';
return s;
}
#endif
/**
* The function isOperatorCharacter checks whether the input paramater \param c is an operator.

View File

@ -24,7 +24,11 @@ typedef struct ListNode {
} ListNode;
#if EXT_PROMPT
char *readInputLine(char const *prompt);
#else
char *readInputLine();
#endif
List getTokenList(char *s);

BIN
shell

Binary file not shown.

View File

@ -123,6 +123,9 @@ bool _parsePipeline(List *lp) {
* @return a bool denoting whether the filename was parsed successfully.
*/
bool _parseFilename(List *lp) {
if (*lp == NULL) return false;
//we run a POSIX compliant system, meaning all characters save '/' and NULL are allowed in filenames.
//NULL is already taken care of by the List library, and / just means the file is located in a directory.
//as a result the only limit we place on filenames are the reserved (operator) characters.
@ -178,11 +181,9 @@ bool _parseBuiltIn(List *lp) {
char *builtIns[] = {
"exit",
"status",
"true",
"false",
"cd",
#if EXT_PROMPT
"debug",
"cd",
#endif
NULL
};