diff --git a/README.org b/README.org index aae4ce0..6a1f18d 100644 --- a/README.org +++ b/README.org @@ -2,11 +2,13 @@ #+options: toc:nil Simple shell for Unix-like systems written in C, that has a funny name (for germans). * Features -- Can run one command at a given time (no | , &&, ||, functions, etc) -- ~cd~ builtin command +- Can run commands +- You can pipe stdout of one command to stdin of another (ex. ~cat main.c | wc --lines~) +- ~cd~ builtin command (~cd~ without arguments moves you to ~$HOME~) - ~exit~ builtin command - ~CTRL+C~ stops running command - Custom ~readline~ function +- Run scripts with ~arsh /path/to/script~ or by putting ~#!/usr/bin/env arsh~ at the first line of a script and making it executable. * Build To build it, you don't need any external dependencies. Example with ~gcc~: #+begin_src shell diff --git a/main.c b/main.c index 7133072..0b14fe0 100644 --- a/main.c +++ b/main.c @@ -7,8 +7,11 @@ #include #include +constexpr size_t max_lenght = 1024; +const char* token_separators = " \t\n\r"; + static sigjmp_buf restart_jmp_buf; -static const int restart_value = 69; +static constexpr int restart_value = 69; static volatile sig_atomic_t jump_active = 0; void sigint_handler(int signo) @@ -19,6 +22,19 @@ void sigint_handler(int signo) } siglongjmp(restart_jmp_buf, restart_value); } + +/*Checked calloc: if allocation fails -> exists*/ +void* ccalloc(size_t size, size_t n) +{ + void* ret = calloc(size, n); + if(!ret) + { + perror("calloc failed"); + exit(1); + } + return ret; +} + static constexpr size_t max_line = 1024; static char string_buffer[max_line]; char* readline(char* print, FILE* stream) @@ -51,27 +67,186 @@ char* readline(char* print, FILE* stream) return string_buffer; } -char** split_input(char* input) +/** + * Struct to represent a command and its arguments. + */ +struct command { - const size_t alloc_size = ((strlen(input) + 1) / 2) + 1; - char** command = malloc(alloc_size * sizeof(char*)); - if (command == NULL) + /** + * IO redirections; redirect[i] should be used as fd i in the child. + * A value of -1 indicates no redirect. + */ + int redirect[2]; + /** The arguments; must be NULL-terminated. */ + char* argv[]; +}; +/** The name of the executable. */ +char* command_name(struct command* cmd) +{ + return cmd->argv[0]; +} + +/** + * Struct to represent a pipeline of commands. The intention is that cmd[i]'s + * output goes to cmd[i+1]'s input. + */ +struct pipeline +{ + /** The total number of commands. */ + size_t n_cmds; + struct command* cmds[]; +}; + +void* parser_memory[max_lenght] = {0}; +size_t parser_memory_index = 0; +void parser_allocated(void* ptr) +{ + parser_memory[parser_memory_index] = ptr; + if(parser_memory_index == max_lenght) { - perror("malloc failed"); + fprintf(stderr, "Not enough memory for parser: Memory will leak!"); exit(1); } - const char* separator = " "; - char* parsed; - uint32_t index = 0; - parsed = strtok(input, separator); - while (parsed != NULL) + parser_memory_index++; +} + +void parser_free() +{ + for(size_t i = 0; i < parser_memory_index; i++) { - command[index] = parsed; - index++; - parsed = strtok(NULL, separator); + free(parser_memory[i]); + } + memset(parser_memory, 0, max_lenght); + parser_memory_index = 0; +} + +/* + * For internal use. Returns the next non-empty token according in *line, + * splitting on characters in TOKEN_SEP. Returns NULL if no token remains. + * Updates line to point to remainder after removal of the token. Modifies the + * string *line. + */ +char* next_non_empty(char **line) +{ + char *tok; + + /* Consume empty tokens. */ + while ((tok = strsep(line, token_separators)) && !*tok); + + return tok; +} + +/** + * Parses str into a freshly allocated command struct and returns a pointer to it. + * The redirects in the returned command will be set to -1, ie no redirect. + */ +struct command* parse_command(char* str) +{ + /* Copy the input line in case the caller wants it later. */ + char* copy = strndup(str, max_lenght); + parser_allocated(copy); + char* token; + int i = 0; + + /* + * Being lazy and allocating way too much memory for the args array. + * Using calloc to ensure it's zero-initialised, which is important because + * execvp expects a NULL-terminated array of arguments. + */ + struct command* ret = ccalloc(sizeof(struct command) + strlen(copy) * sizeof(char*), 1); + parser_allocated(ret); + + while ((token = next_non_empty(©))) + { + ret->argv[i++] = token; + } + ret->redirect[0] = ret->redirect[1] = -1; + return ret; +} + +/** + * Parses str into a freshly allocated pipeline_struct and returns a pointer to + * it. All commands in cmds will also be freshy allocated, and have their + * redirects set to -1, ie no redirect. + */ +struct pipeline* parse_pipeline(char *str) +{ + char* copy = strndup(str, max_lenght); + parser_allocated(copy); + char* cmd_str; + int n_cmds = 0; + int i = 0; + struct pipeline* ret; + + /* + * Count pipe characters that appear in pipeline to know how much space to + * allocate for the cmds array. + */ + for (char* cur = str; *cur; cur++) + { + if (*cur == '|') ++n_cmds; + } + + ++n_cmds; /* There is one more command than there are pipe characters. */ + + ret = ccalloc(sizeof(struct pipeline) + n_cmds * sizeof(struct command*), 1); + parser_allocated(ret); + ret->n_cmds = n_cmds; + + while((cmd_str = strsep(&str, "|"))) + { + ret->cmds[i++] = parse_command(cmd_str); + } + + return ret; +} + +void close_ALL_the_pipes(int n_pipes, int (*pipes)[2]) +{ + for (int i = 0; i < n_pipes; ++i) + { + close(pipes[i][0]); + close(pipes[i][1]); + } +} + +int exec_with_redir(struct command* command, int n_pipes, int (*pipes)[2]) +{ + int fd = -1; + if ((fd = command->redirect[0]) != -1) + { + dup2(fd, STDIN_FILENO); + } + if ((fd = command->redirect[1]) != -1) + { + dup2(fd, STDOUT_FILENO); + } + close_ALL_the_pipes(n_pipes, pipes); + return execvp(command_name(command), command->argv); +} + +static struct sigaction s_old; + +pid_t run_with_redir(struct command* command, int n_pipes, int (*pipes)[2]) +{ + pid_t child_pid = fork(); + + if(child_pid < 0) + { + perror("Fork failed"); + exit(1); + } + if (child_pid) + { /* We are the parent. */ + return child_pid; + } + else + { // We are the child. */ + sigaction(SIGINT, &s_old, NULL); + exec_with_redir(command, n_pipes, pipes); + perror(command_name(command)); + exit(1); } - command[index] = NULL; - return command; } int cd(char* path) @@ -97,7 +272,6 @@ int main(int argc, const char* argv[]) s.sa_handler = sigint_handler; sigemptyset(&s.sa_mask); s.sa_flags = SA_RESTART; - struct sigaction s_old; sigaction(SIGINT, &s, &s_old); int stat_loc; @@ -121,54 +295,61 @@ int main(int argc, const char* argv[]) } // After that line you need cleanup - char** command = split_input(input); - if(command[0] == NULL) - { - goto cleanup; - } - if(strlen(command[0]) >= 1 && command[0][0] == '#') - { - goto cleanup; - } + struct pipeline* pipeline = parse_pipeline(input); + int n_pipes = pipeline->n_cmds - 1; - if(strcmp(command[0], "cd") == 0) + /* pipes[i] redirects from pipeline->cmds[i] to pipeline->cmds[i+1]. */ + int (*pipes)[2] = ccalloc(sizeof(int[2]), n_pipes); + parser_allocated(pipes); + pid_t* pids = ccalloc(sizeof(pid_t), pipeline->n_cmds); + parser_allocated(pids); + for (int i = 1; i < pipeline->n_cmds; ++i) { - if (cd(command[1]) < 0) + pipe(pipes[i-1]); + pipeline->cmds[i]->redirect[STDIN_FILENO] = pipes[i-1][0]; + pipeline->cmds[i-1]->redirect[STDOUT_FILENO] = pipes[i-1][1]; + } + for (int i = 0; i < pipeline->n_cmds; ++i) + { + struct command* cmd = pipeline->cmds[i]; + char* cmd_name = command_name(cmd); + if(!cmd_name) { - perror(command[1]); + continue; } + if(strlen(cmd_name) >= 1 && cmd_name[0] == '#') + { + goto cleanup; + } + + if(strcmp(cmd_name, "cd") == 0) + { + char* path = cmd->argv[1] ? cmd->argv[1] : getenv("HOME"); + if (cd(path) < 0) + { + char err_buf[max_lenght] = {0}; + sprintf(err_buf, "cd: %s", path); + perror(err_buf); + } - goto cleanup; //skip fork() - } - if(strcmp(command[0], "exit") == 0) - { - int32_t status_code = command[1] != NULL ? atoi(command[1]) : 0; - exit(status_code); - } - - pid_t child_pid = fork(); - if (child_pid < 0) - { - perror("Fork failed"); - exit(1); - } - if (child_pid == 0) - { - sigaction(SIGINT, &s_old, NULL); - //never returns if call is successful - if(execvp(command[0], command) < 0) - { - perror(command[0]); - exit(1); + goto cleanup; //skip fork() } + if(strcmp(cmd_name, "exit") == 0) + { + int32_t status_code = cmd->argv[1] != NULL ? atoi(cmd->argv[1]) : 0; + exit(status_code); + } + pids[i] = run_with_redir(cmd, n_pipes, pipes); } - else + close_ALL_the_pipes(n_pipes, pipes); + + for(size_t i = 0; i < pipeline->n_cmds; i++) { - waitpid(child_pid, &stat_loc, WUNTRACED); + waitpid(pids[i], &stat_loc, WUNTRACED); } cleanup: - free(command); + parser_free(); } }