diff --git a/.gitignore b/.gitignore index ccbc1e7..f9b5175 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Project specific 42sh +testsuite +debug # Prerequisites *.d @@ -132,3 +134,51 @@ $RECYCLE.BIN/ *.msm *.msp *.lnk + +# Autotools (since it disappeared for some reason) + +Makefile.in +/ar-lib +/mdate-sh +/py-compile +/test-driver +/ylwrap +.deps/ +.dirstamp + +autom4te.cache +/autoscan.log +/autoscan-*.log +/aclocal.m4 +/compile +/config.cache +/config.guess +/config.h.in +/config.log +/config.status +/config.sub +/configure +/configure.scan +/depcomp +/install-sh +/missing +/stamp-h1 + +/libtool +/ltmain.sh +.libs/ + +/texinfo.tex + +m4/libtool.m4 +m4/ltoptions.m4 +m4/ltsugar.m4 +m4/ltversion.m4 +m4/lt~obsolete.m4 + +Makefile +*.svg + + +.venv/ +*.py \ No newline at end of file diff --git a/README.md b/README.md index 87fe6e2..0b91b92 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,59 @@ # 42sh - A POSIX shell with a bad name -42sh is a shcool project aiming to implement a POSIX compliant shell in C. +42sh is a project aiming to implement a POSIX-compliant shell written in C with only the standard library. +Source de is fully documented with the doxygen format so you can easily understand how the project works by exploring it. + +> **Note** This is a school project, therefore it probably won't interest you if you are looking for something useful. ## Getting started -TODO - ### Build - -TODO +run this command: + `autoreconf --force --verbose --install` ### Test +run this command: + `./configure CFLAGS='-std=c99 -Werror -Wall -Wextra -Wvla'` +then: + `make` -TODO +#### Build with ASan +run this command: + `./configure CFLAGS='-std=c99 -Werror -Wall -Wextra -Wvla -g -fsanitize=address'` -## Authors - -- Matteo Flebus -- Jean Hérail -- William Valenduc -- Guillem George +or for MacOS (Jean Here): + `./configure CFLAGS='-std=c99 -Werror -Wall -Wextra -Wvla -I/opt/homebrew/include' LDFLAGS='-L/opt/homebrew/lib'` + +then: + `make check` ## Project status -WIP +### Implemented features -## TODO +* **Command Execution:** `$PATH` search and binary execution (via `fork` and `execvp`) with error return code handling. +* **Built-ins:** Native implementation of `echo`, `cd`, `exit`, `export`, `unset`, `set`, `.`, `true`, `false`, as well as loop management with `break` and `continue`. +* **Control Structures:** * Conditions: `if / then / elif / else / fi`. + * Loops: `while`, `until` and `for`. +* **Logical Operators:** Command chaining with `&&`, `||` and negation with `!`. +* **Pipelines and Redirections:** * Full management of pipes `|` to connect the output of one process to the input of another. + * Single and multiple redirections: `>`, `<`, `>>`, `>&`, `<&`, `<>`. +* **Variables Management:** Assignment, variable expansion, and special variables handling like `$?` (return code of the last command). +* **Command Grouping:** Execution blocks `{ ... }` and subshells creation `( ... )`. +* **Quoting:** Support for weak (`"`) and strong (`'`) quoting for special characters escaping. -# Autotools -implement functions in all .c files to see if everything compiles. +## Architecture + +The shell operates on a classic compilation/interpretation pipeline: + +1. **Lexer (Lexical Analysis):** Reads standard input (or script) character by character and generates a stream of "Tokens" (Words, Operators, Redirections). +2. **Parser (Syntax Analysis):** Syntax analyzer that transforms the token stream into a complex Abstract Syntax Tree (AST). This module strictly manages the nesting of control structures and enforces the rigid grammar of the Shell Command Language. +3. **Execution (AST Traversal):** The tree is traversed recursively. Redirections modify file descriptors (`dup2`), child processes are created (`fork`), and commands are executed. + + +## Authors + +- Guillem George +- Matteo Flebus +- Jean Herail +- William Valenduc diff --git a/build_flemme.sh b/build_flemme.sh new file mode 100755 index 0000000..e16b9f0 --- /dev/null +++ b/build_flemme.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +# build_only.sh - Automates build for 42sh with pretty output (no tests) + +set -e + +# Colors +GREEN="\033[1;32m" +RED="\033[1;31m" +YELLOW="\033[1;33m" +RESET="\033[0m" + +VERBOSE=0 +if [[ "$1" == "--verbose" ]]; then + VERBOSE=1 +fi + +run_cmd() { + local desc="$1" + shift + if [[ $VERBOSE -eq 1 ]]; then + echo -e "${YELLOW}${desc}...${RESET}" + "$@" + else + echo -ne "${YELLOW}${desc}...${RESET}" + "$@" &> /dev/null && echo -e " ${GREEN}done${RESET}" || { echo -e " ${RED}failed${RESET}"; exit 1; } + fi +} + +run_cmd "Running autoreconf" autoreconf --force --verbose --install + +if [[ "$(uname)" == "Darwin" ]]; then + run_cmd "Configuring for MacOS" ./configure CFLAGS='-std=c99 -Werror -Wall -Wextra -Wvla -I/opt/homebrew/include' LDFLAGS='-L/opt/homebrew/lib' +else + run_cmd "Configuring for Linux" ./configure CFLAGS='-std=c99 -Werror -Wall -Wextra -Wvla -g -fsanitize=address' +fi + +run_cmd "Cleaning build" make clean + +if [[ $VERBOSE -eq 1 ]]; then + echo -e "${YELLOW}Building...${RESET}" + make +else + echo -ne "${YELLOW}Building...${RESET}" + echo + make +fi diff --git a/check_flemme.sh b/check_flemme.sh new file mode 100755 index 0000000..0543001 --- /dev/null +++ b/check_flemme.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +# build_and_test.sh - Automates build and test for 42sh with pretty output + +set -e + +# Colors +GREEN="\033[1;32m" +RED="\033[1;31m" +YELLOW="\033[1;33m" +RESET="\033[0m" + +VERBOSE=0 +if [[ "$1" == "--verbose" ]]; then + VERBOSE=1 +fi + +run_cmd() { + local desc="$1" + shift + if [[ $VERBOSE -eq 1 ]]; then + echo -e "${YELLOW}${desc}...${RESET}" + "$@" + else + echo -ne "${YELLOW}${desc}...${RESET}" + "$@" &> /dev/null && echo -e " ${GREEN}done${RESET}" || { echo -e " ${RED}failed${RESET}"; exit 1; } + fi +} + +run_cmd "Running autoreconf" autoreconf --force --verbose --install + +if [[ "$(uname)" == "Darwin" ]]; then + run_cmd "Configuring for MacOS" ./configure CFLAGS='-std=c99 -Werror -Wall -Wextra -Wvla -I/opt/homebrew/include' LDFLAGS='-L/opt/homebrew/lib' +else + run_cmd "Configuring for Linux" ./configure CFLAGS='-std=c99 -Werror -Wall -Wextra -Wvla -g -fsanitize=address' +fi + +run_cmd "Cleaning build" make clean +if [[ $VERBOSE -eq 1 ]]; then + echo -e "${YELLOW}Running tests...${RESET}" + make check +else + echo -ne "${YELLOW}Running tests...${RESET}" + echo + make check +fi diff --git a/configure.ac b/configure.ac index 4a89a36..14a03fd 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,9 @@ # Init the 42sh project AC_INIT([42sh], [1.0], [matteo.flebus@epita.fr]) +# FLAGS="-std=c99 -pedantic -Werror -Wall -Wextra -Wvla" +# AC_SUBST([FLAGS]) + # Setup Automake AM_INIT_AUTOMAKE([subdir-objects] [foreign]) @@ -23,8 +26,15 @@ AC_PROG_CC AC_CONFIG_FILES([ Makefile src/Makefile - src/ast/Makefile src/parser/Makefile src/lexer/Makefile + src/io_backend/Makefile + src/execution/Makefile + src/expansion/Makefile + src/utils/Makefile ]) + # TODO add tests Makefile here + # tests/Makefile + # tests/unit/Makefile + # tests/unit/utils/Makefile AC_OUTPUT diff --git a/src/42sh.c b/src/42sh.c deleted file mode 100644 index e05c6f6..0000000 --- a/src/42sh.c +++ /dev/null @@ -1,6 +0,0 @@ -// all includes - -int main(int argc, char **argv) -{ - return 0; -} diff --git a/src/Makefile.am b/src/Makefile.am index 564dfaa..5f9fa1d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,25 +1,62 @@ -# define the subdirectories SUBDIRS = \ - ast \ - parser \ - lexer \ - io_backend \ - execution \ - expansion -# + utils if needed + parser \ + lexer \ + io_backend \ + execution \ + expansion \ + utils bin_PROGRAMS = 42sh -42sh_SOURCES = 42sh.c +42sh_CFLAGS = -std=c99 -Werror -Wall -Wextra -Wvla + +42sh_SOURCES = main.c 42sh_CPPFLAGS = -I%D% -42sh_CFLAGS = -std=c99 -pedantic -Werror -Wall -Wextra -Wvla - 42sh_LDADD = \ - ast/libast.a \ - parser/libparser.a \ - lexer/liblexer.a \ - io_backend/libio_backend.a \ - expansion/libexpansion.a \ - execution/libexecution.a + parser/libparser.a \ + lexer/liblexer.a \ + io_backend/libio_backend.a \ + utils/libutils.a \ + execution/libexecution.a \ + expansion/libexpansion.a + +# ================ TESTS ================ + +check: + ../tests/wrap.sh # "$(COVERAGE)" "$(OUTPUT_FILE)" + +# ------------- Unit tests -------------- + +check_PROGRAMS = testsuite + +#testsuite_CFLAGS = $(42sh_CFLAGS) +#testsuite_CFLAGS += $(CRITERION_CFLAGS) + +testsuite_SOURCES = ../tests/unit/utils/utils_tests.c \ + ../tests/unit/expansion/parse_var.c \ + ../tests/unit/utils/args.c \ + ../tests/unit/utils/hash_map.c \ + ../tests/unit/utils/insert_into.c + +# ../tests/unit/lexer/lexer_tests.c +# ../tests/unit/expansion/expand.c +# ../tests/unit/io_backend/io_backend.c + +testsuite_CPPFLAGS = $(42sh_CPPFLAGS) + +testsuite_LDADD = $(42sh_LDADD) -lcriterion + +# ------------- asan debug -------------- + +# check_PROGRAMS += debug + +#debug_CFLAGS = $(42sh_CFLAGS) +#debug_CFLAGS += -fsanitize=address -g + +# debug_SOURCES = $(42sh_SOURCES) +# +# debug_CPPFLAGS = $(42sh_CPPFLAGS) +# +# debug_LDADD = $(42sh_LDADD) diff --git a/src/ast/Makefile.am b/src/ast/Makefile.am deleted file mode 100644 index 9df484e..0000000 --- a/src/ast/Makefile.am +++ /dev/null @@ -1,11 +0,0 @@ -lib_LIBRARIES = libast.a - -libast_a_SOURCES = \ - ast.c \ - ast.h - -libast_a_CPPFLAGS = -I$(top_srcdir)/src - -libast_a_CFLAGS = -std=c99 -pedantic -Werror -Wall -Wextra -Wvla - -noinst_LIBRARIES = libast.a diff --git a/src/ast/ast.c b/src/ast/ast.c deleted file mode 100644 index e69de29..0000000 diff --git a/src/ast/ast.h b/src/ast/ast.h deleted file mode 100644 index e69de29..0000000 diff --git a/src/execution/Makefile.am b/src/execution/Makefile.am index f83e3ba..f061dfc 100644 --- a/src/execution/Makefile.am +++ b/src/execution/Makefile.am @@ -2,10 +2,10 @@ lib_LIBRARIES = libexecution.a libexecution_a_SOURCES = \ execution.c \ - execution.h + execution.h \ + execution_helpers.c \ + execution_helpers.h libexecution_a_CPPFLAGS = -I$(top_srcdir)/src -libexecution_a_CFLAGS = -std=c99 -pedantic -Werror -Wall -Wextra -Wvla - noinst_LIBRARIES = libexecution.a diff --git a/src/execution/execution.c b/src/execution/execution.c index e69de29..28726e5 100644 --- a/src/execution/execution.c +++ b/src/execution/execution.c @@ -0,0 +1,73 @@ +#define _POSIX_C_SOURCE 200809L +#include "execution.h" + +#include +#include +#include +#include +#include + +#include "../expansion/expansion.h" +#include "../utils/hash_map/hash_map.h" +#include "../utils/vars/vars.h" + +// Refactored: delegates to helpers in execution_helpers.c +#include "execution_helpers.h" + +int execution(struct ast *ast, struct hash_map *vars) +{ + if (!ast) + return 0; + + int res; + switch (ast->type) + { + case AST_VOID: + case AST_END: + res = 0; + break; + case AST_CMD: { + struct ast_command *command = ast_get_command(ast); + if (!expand(command, vars)) + fprintf(stderr, "Error: Variable expansion failed\n"); + + res = exec_ast_command(command, vars); + break; + } + case AST_IF: + res = exec_ast_if(ast_get_if(ast), vars); + break; + case AST_LIST: + res = exec_ast_list(ast_get_list(ast), vars); + break; + case AST_AND_OR: + res = exec_ast_and_or(ast_get_and_or(ast), vars); + break; + case AST_LOOP: + res = exec_ast_loop(ast_get_loop(ast), vars); + break; + case AST_SUBSHELL: + res = exec_ast_subshell(ast_get_subshell(ast), vars); + break; + default: + res = 127; + break; + } + + if (res == EXEC_SIGNAL_EXIT) + { + char *exit_val_str = get_var(vars, "EXIT_VALUE"); + if (exit_val_str == NULL) + { + fprintf( + stderr, + "Internal error: could not retrieve return value from exit\n"); + return 2; + } + return atoi(exit_val_str); + } + else + { + return res; + } +} diff --git a/src/execution/execution.h b/src/execution/execution.h index e69de29..c3e0080 100644 --- a/src/execution/execution.h +++ b/src/execution/execution.h @@ -0,0 +1,16 @@ +#ifndef EXECUTION_H +#define EXECUTION_H + +#include "../utils/ast/ast.h" +#include "../utils/hash_map/hash_map.h" +#include "../utils/lists/lists.h" + +/** + * @brief Execute the AST + * + * @param ast Pointer to the AST structure + * @return int Execution status code of the last command + */ +int execution(struct ast *ast, struct hash_map *vars); + +#endif /* ! EXECUTION_H */ diff --git a/src/execution/execution_helpers.c b/src/execution/execution_helpers.c new file mode 100644 index 0000000..12d4e1f --- /dev/null +++ b/src/execution/execution_helpers.c @@ -0,0 +1,597 @@ +#define _POSIX_C_SOURCE 200809L +#include "execution_helpers.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../utils/hash_map/hash_map.h" +#include "../utils/lists/lists.h" +#include "../utils/vars/vars.h" +#include "execution.h" + +// === Static functions + +static int open_redir_file(const struct ast_redir *redir, int *flags, int *mode) +{ + *mode = 0644; + if (redir->type == AST_REDIR_TYPE_GREAT + || redir->type == AST_REDIR_TYPE_CLOBBER) + { + *flags = O_WRONLY | O_CREAT | O_TRUNC; + return open(redir->filename, *flags, *mode); + } + else if (redir->type == AST_REDIR_TYPE_DGREAT) + { + *flags = O_WRONLY | O_CREAT | O_APPEND; + return open(redir->filename, *flags, *mode); + } + else if (redir->type == AST_REDIR_TYPE_LESS) + { + *flags = O_RDONLY; + return open(redir->filename, *flags); + } + return -3; // not a file open +} + +static int set_all_redir(struct list *redir_list) +{ + while (redir_list) + { + struct ast *redir_node = (struct ast *)redir_list->data; + struct ast_redir *redir = ast_get_redir(redir_node); + int target_fd; + if (redir->io_number != -1) + { + target_fd = redir->io_number; + } + else + { + if (redir->type == AST_REDIR_TYPE_LESS + || redir->type == AST_REDIR_TYPE_LESSGREAT + || redir->type == AST_REDIR_TYPE_LESSAND) + { + target_fd = 0; + } + else + { + target_fd = 1; + } + } + redir->saved_fd = dup(target_fd); + int new_fd = -1; + int flags = 0; + int mode = 0644; + if (redir->type == AST_REDIR_TYPE_GREAT + || redir->type == AST_REDIR_TYPE_CLOBBER + || redir->type == AST_REDIR_TYPE_DGREAT + || redir->type == AST_REDIR_TYPE_LESS) + { + new_fd = open_redir_file(redir, &flags, &mode); + if (new_fd == -1) + { + perror("open"); + return -1; + } + } + else if (redir->type == AST_REDIR_TYPE_GREATAND + || redir->type == AST_REDIR_TYPE_LESSAND) + { + new_fd = atoi(redir->filename); + } + if (dup2(new_fd, target_fd) == -1) + { + perror("dup2"); + if (new_fd != -1) + { + close(new_fd); + } + return -1; + } + if (new_fd != -1) + { + close(new_fd); + } + redir_list = redir_list->next; + } + return 0; +} + +static char **list_to_argv(struct list *command_list) +{ + size_t len = 0; + struct list *cur = command_list; + while (cur) + { + len++; + cur = cur->next; + } + char **argv = calloc(len + 1, sizeof(char *)); + if (!argv) + return NULL; + cur = command_list; + for (size_t i = 0; i < len; i++) + { + argv[i] = (char *)cur->data; + cur = cur->next; + } + argv[len] = NULL; + return argv; +} + +/* + * @brief parses string and returns the represented (unsigned) number + * @return the number contained by the string or -1 if number is invalid + */ +static int atou(char *str) +{ + if (str == NULL || *str == '\0') + return -1; + + int result = 0; + size_t i = 0; + while (str[i] != '\0') + { + if (str[i] < '0' || str[i] > '9') + return -1; + + result *= 10; + result += str[i] - '0'; + i++; + } + + return result; +} + +static int try_builtin(char **argv, struct hash_map *vars); + +static int builtin_break(char **argv); +static int builtin_continue(char **argv); +static int builtin_unset(char **argv, struct hash_map *vars); + +static int exec_assignment(struct list *assignment_list, struct hash_map *vars) +{ + while (assignment_list != NULL) + { + if (!ast_is_assignment(assignment_list->data)) + { + fprintf(stderr, "list of assignements contains something else"); + return 1; + } + struct ast_assignment *assignment = + ast_get_assignment(assignment_list->data); + + if (assignment->global) + { + setenv(assignment->name, assignment->value, 1); + } + set_var_copy(vars, assignment->name, assignment->value); + assignment_list = assignment_list->next; + } + return 0; +} + +// === Functions + +int exec_ast_command(struct ast_command *command, struct hash_map *vars) +{ + exec_assignment(command->assignments, vars); + set_all_redir(command->redirections); + + if (!command || !(command->command)) + { + return 1; + } + + char **argv = list_to_argv(command->command); + if (!argv || !(argv[0])) + { + free(argv); + unset_all_redir(command->redirections); + return 0; + } + + int builtin_ret = try_builtin(argv, vars); + if (builtin_ret != -1) + { + free(argv); + unset_all_redir(command->redirections); + return builtin_ret; + } + + pid_t pid = fork(); + if (pid < 0) + { + perror("fork"); + free(argv); + unset_all_redir(command->redirections); + return 1; + } + + if (pid == 0) + { + execvp(argv[0], argv); + perror("execvp"); + unset_all_redir(command->redirections); + _exit(127); + } + + int status = 0; + waitpid(pid, &status, 0); + free(argv); + unset_all_redir(command->redirections); + + if (WIFEXITED(status)) + { + return WEXITSTATUS(status); + } + + return 1; +} + +int exec_ast_if(struct ast_if *if_node, struct hash_map *vars) +{ + if (if_node == NULL) + return 2; + int cond = execution(if_node->condition, vars); + if (cond == EXEC_SIGNAL_BREAK || cond == EXEC_SIGNAL_CONTINUE + || cond == EXEC_SIGNAL_EXIT) + return cond; + if (cond == 0) + { + int r = execution(if_node->then_clause, vars); + return r; + } + else + { + int r = execution(if_node->else_clause, vars); + return r; + } +} + +int exec_ast_subshell(struct ast_subshell *subshell_node, + struct hash_map *vars) +{ + pid_t pid = fork(); + if (pid < 0) + { + perror("fork"); + return 1; + } + + if (pid == 0) + { + int res = execution(subshell_node->child, vars); + _exit(res); + } + + int status = 0; + waitpid(pid, &status, 0); + + if (WIFEXITED(status)) + { + return WEXITSTATUS(status); + } + + return 1; +} + +int exec_ast_list(struct ast_list *list_node, struct hash_map *vars) +{ + struct list *cur = list_node->children; + int ret = 0; + while (cur) + { + struct ast *child = (struct ast *)cur->data; + if (!ast_is_void(child)) + { + int child_ret = execution(child, vars); + if (child_ret == EXEC_SIGNAL_BREAK + || child_ret == EXEC_SIGNAL_CONTINUE + || child_ret == EXEC_SIGNAL_EXIT) + return child_ret; + ret = child_ret; + } + cur = cur->next; + } + return ret; +} + +int exec_ast_and_or(struct ast_and_or *ao_node, struct hash_map *vars) +{ + int left_ret = execution(ao_node->left, vars); + if (left_ret == EXEC_SIGNAL_BREAK || left_ret == EXEC_SIGNAL_CONTINUE + || left_ret == EXEC_SIGNAL_EXIT) + return left_ret; + if (ao_node->type == AST_AND_OR_TYPE_AND) + { + if (left_ret == 0) + { + int right_ret = execution(ao_node->right, vars); + return right_ret; + } + return left_ret; + } + else + { + if (left_ret != 0) + { + int right_ret = execution(ao_node->right, vars); + return right_ret; + } + return left_ret; + } +} + +void unset_all_redir(struct list *redir_list) +{ + while (redir_list) + { + struct ast *redir_node = (struct ast *)redir_list->data; + struct ast_redir *redir = ast_get_redir(redir_node); + int target_fd; + if (redir->io_number != -1) + { + target_fd = redir->io_number; + } + else + { + if (redir->type == AST_REDIR_TYPE_LESS + || redir->type == AST_REDIR_TYPE_LESSGREAT + || redir->type == AST_REDIR_TYPE_LESSAND) + { + target_fd = 0; + } + else + { + target_fd = 1; + } + } + if (redir->saved_fd != -1) + { + dup2(redir->saved_fd, target_fd); + close(redir->saved_fd); + redir->saved_fd = -1; + } + redir_list = redir_list->next; + } +} + +int exec_ast_loop(struct ast_loop *loop_node, struct hash_map *vars) +{ + int res = 0; + while (execution(loop_node->condition, vars) == 0) + { + res = execution(loop_node->body, vars); + if (res == EXEC_SIGNAL_BREAK) + { + res = 0; + break; + } + else if (res == EXEC_SIGNAL_CONTINUE) + { + res = 0; + continue; + } + } + + return res; +} + +// --- Builtins --- + +static void print_with_escapes(const char *str) +{ + while (*str) + { + if (*str == '\\') + { + str++; + if (*str == 'n') + putchar('\n'); + else if (*str == 't') + putchar('\t'); + else if (*str == '\\') + putchar('\\'); + else if (*str == 'a') + putchar('\a'); + else if (*str == 'b') + putchar('\b'); + else if (*str == 'f') + putchar('\f'); + else if (*str == 'r') + putchar('\r'); + else if (*str == 'v') + putchar('\v'); + else if (*str == 'c') + return; // stop printing + else + { + // unrecognized escape, print "\"" and the char + putchar('\\'); + if (*str) + putchar(*str); + } + } + else + { + putchar(*str); + } + str++; + } +} + +static int builtin_echo(char **argv) +{ + bool newline = true; + bool interpret_escapes = false; + int i = 1; + + // Parse options + while (argv[i] && argv[i][0] == '-') + { + char *opt = argv[i] + 1; // skip "-" + bool valid_option = false; + while (*opt) + { + if (*opt == 'n') + { + newline = false; + valid_option = true; + } + else if (*opt == 'e') + { + interpret_escapes = true; + valid_option = true; + } + else if (*opt == 'E') + { + interpret_escapes = false; + valid_option = true; + } + else + { + // invalid option so euh treat as regular argument + valid_option = false; + break; + } + opt++; + } + if (!valid_option) + break; // stop parsing options + i++; + } + + // Print arguments + for (; argv[i]; i++) + { + if (interpret_escapes) + print_with_escapes(argv[i]); + else + printf("%s", argv[i]); + if (argv[i + 1]) + printf(" "); + } + if (newline) + printf("\n"); + + fflush(stdout); + return 0; +} + +static int builtin_break(char **argv) +{ + (void)argv; + return EXEC_SIGNAL_BREAK; +} + +static int builtin_continue(char **argv) +{ + (void)argv; + return EXEC_SIGNAL_CONTINUE; +} + +static int builtin_true(char **argv) +{ + (void)argv; + return 0; +} + +static int builtin_false(char **argv) +{ + (void)argv; + return 1; +} + +static int builtin_exit(char **argv, struct hash_map *vars) +{ + int exit_val = 0; + if (argv[1]) + { + exit_val = atou(argv[1]); + if (exit_val == -1) + { + fprintf(stderr, "exit: Illegal number %s\n", argv[1]); + return 2; + } + } + + set_var_int(vars, "EXIT_VALUE", exit_val); + return EXEC_SIGNAL_EXIT; +} + +static int builtin_cd(char **argv, struct hash_map *vars) +{ + const char *path = argv[1]; + if (!path) + { + path = getenv("HOME"); + if (!path) + { + fprintf(stderr, "cd: HOME not set\n"); + return 1; + } + } + // char *pwd = getcwd("", ""); + char *pwd = get_var_or_env(vars, "PWD"); + if (chdir(path) != 0) + { + perror("cd"); + return 1; + } + set_var_copy(vars, "OLD_PWD", pwd); + set_var_copy(vars, "PWD", path); + return 0; +} + +static int builtin_unset(char **argv, struct hash_map *vars) +{ + if (!argv) + return 0; + for (int i = 1; argv[i]; i++) + { + const char *name = argv[i]; + if (name == NULL || name[0] == '\0') + continue; + // remove from shell variables + hash_map_remove(vars, name); + // remove from environment variables + unsetenv(name); + } + return 0; +} + +/** + * @brief Tries to execute a builtin command if the command matches a builtin + * + * @param argv Array of command arguments + * @return int Exit status of the builtin command, or -1 if not a builtin + */ +static int try_builtin(char **argv, struct hash_map *vars) +{ + if (!argv || !argv[0]) + return 0; + + if (strcmp(argv[0], "echo") == 0) + return builtin_echo(argv); + if (strcmp(argv[0], "unset") == 0) + return builtin_unset(argv, vars); + if (strcmp(argv[0], "true") == 0) + return builtin_true(argv); + if (strcmp(argv[0], "false") == 0) + return builtin_false(argv); + if (strcmp(argv[0], "break") == 0) + return builtin_break(argv); + if (strcmp(argv[0], "continue") == 0) + return builtin_continue(argv); + if (strcmp(argv[0], "exit") == 0) + return builtin_exit(argv, vars); + if (strcmp(argv[0], "cd") == 0) + return builtin_cd(argv, vars); + + return -1; +} diff --git a/src/execution/execution_helpers.h b/src/execution/execution_helpers.h new file mode 100644 index 0000000..8a56e18 --- /dev/null +++ b/src/execution/execution_helpers.h @@ -0,0 +1,21 @@ +#ifndef EXECUTION_HELPERS_H +#define EXECUTION_HELPERS_H + +#include "../utils/ast/ast.h" +#include "../utils/hash_map/hash_map.h" + +// Special execution signals used internally to implement loop control +#define EXEC_SIGNAL_CONTINUE (-2) +#define EXEC_SIGNAL_BREAK (-3) +#define EXEC_SIGNAL_EXIT (-4) + +int exec_ast_command(struct ast_command *command, struct hash_map *vars); +int exec_ast_if(struct ast_if *if_node, struct hash_map *vars); +int exec_ast_list(struct ast_list *list_node, struct hash_map *vars); +int exec_ast_and_or(struct ast_and_or *ao_node, struct hash_map *vars); +int exec_ast_loop(struct ast_loop *loop_node, struct hash_map *vars); +int exec_ast_subshell(struct ast_subshell *subshell_node, + struct hash_map *vars); + +void unset_all_redir(struct list *redir_list); +#endif // EXECUTION_HELPERS_H diff --git a/src/expansion/Makefile.am b/src/expansion/Makefile.am index 5074622..065f225 100644 --- a/src/expansion/Makefile.am +++ b/src/expansion/Makefile.am @@ -6,6 +6,4 @@ libexpansion_a_SOURCES = \ libexpansion_a_CPPFLAGS = -I$(top_srcdir)/src -libexpansion_a_CFLAGS = -std=c99 -pedantic -Werror -Wall -Wextra -Wvla - noinst_LIBRARIES = libexpansion.a diff --git a/src/expansion/expansion.c b/src/expansion/expansion.c index e69de29..e8f8577 100644 --- a/src/expansion/expansion.c +++ b/src/expansion/expansion.c @@ -0,0 +1,228 @@ +#define _POSIX_C_SOURCE 200809L +#include "expansion.h" + +#include +#include +#include +#include +#include + +#include "../utils/ast/ast.h" +#include "../utils/hash_map/hash_map.h" +#include "../utils/string_utils/string_utils.h" +#include "../utils/vars/vars.h" + +static bool is_var_start_char(char c) +{ + return isalpha(c) || c == '_'; +} + +static bool is_var_char(char c) +{ + return isalnum(c) || c == '_'; +} + +static bool is_special_var_char(char c) +{ + return c == '@' || c == '*' || c == '?' || c == '$' || isdigit(c) + || c == '#'; +} + +size_t parse_var_name(char *str, char **res) +{ + char *brace = NULL; + size_t i = 1; // skip the '$' + + if (str[i] == '{' && str[i + 1] != 0 && str[i + 1] != '}') + { + if (is_special_var_char(str[i + 1]) && str[i + 2] == '}') + { + // Special variable like ${1}, ${?} + *res = strndup(str + i + 1, 1); + return 4; // length of ${X} + } + + brace = str + i; + i++; // skip the '{' + } + else if (is_special_var_char(str[i])) + { + *res = strndup(str + i, 1); + return 2; // length of $X + } + + if (!is_var_start_char(str[i])) + { + // Not a valid variable start + *res = NULL; + return 0; + } + + while (1) + { + if (str[i] == '}' && *brace == '{') + { + *res = strndup(str + 2, i - 2); + return i + 1; + } + else if (!is_var_char(str[i])) + { + if (brace != NULL) + { + // Missing closing '}' + *res = NULL; + return 0; + } + break; + } + i++; + } + + *res = strndup(str + 1, i - 1); + return i; +} + +static bool expand_var(char **str, size_t pos, const struct hash_map *vars) +{ + char *var_name = NULL; + size_t r = parse_var_name(*str + pos, &var_name); + if (r > 0 && var_name != NULL) + { + char *value; + char rnd_str[10]; // max 5 digits + null terminator + if (strcmp(var_name, "RANDOM") == 0) + { + short rnd = short_random(); + snprintf(rnd_str, 10, "%d", rnd); + value = rnd_str; + } + else + { + value = get_var_or_env(vars, var_name); + if (value == NULL) + // Undefined variable: expand to empty string + value = ""; + } + + char *p = insert_into(*str, value, pos, r); + free(var_name); + if (p == NULL) + { + // error: insertion failed + return false; + } + *str = p; + return true; + } + return false; +} + +size_t parse_subshell_str(char *str, char **res) +{ + size_t i = 1; // skip the '(' + int paren_count = 1; + + if (str[i] == ')') + { + // empty subshell + *res = NULL; + return 0; + } + + while (str[i] != 0) + { + if (str[i] == '(') + paren_count++; + else if (str[i] == ')') + { + paren_count--; + + if (paren_count == 0) + { + *res = strndup(str + 1, i - 1); + return i + 1; + } + } + i++; + } + + // error: parenthesis not closed + *res = NULL; + return 0; +} + +bool expand(struct ast_command *command, const struct hash_map *vars) +{ + if (command == NULL) + return false; + + char *str; + size_t len; + enum quote_state quotes; + struct list *l = command->command; + + while (l != NULL) + { + quotes = NO_QUOTE; + str = (char *)l->data; + len = strlen(str); + + for (size_t i = 0; str[i] != 0; i++) + { + if (str[i] == '\'' || str[i] == '\"') + { + if (quotes == NO_QUOTE) + { + quotes = (str[i] == '\'') ? SINGLE_QUOTE : DOUBLE_QUOTE; + } + else if ((quotes == SINGLE_QUOTE && str[i] == '\'') + || (quotes == DOUBLE_QUOTE && str[i] == '\"')) + { + quotes = NO_QUOTE; + } + else + { + // inside the other quote type, do nothing + continue; + } + + // remove quote + memmove(str + i, str + i + 1, strlen(str + i + 1) + 1); + i--; + } + else if (quotes == SINGLE_QUOTE) + { + continue; // do nothing + } + else if (str[i] == '$' && str[i + 1] != 0 && !isspace(str[i + 1])) + { + // variable expansion + bool r = expand_var(&str, i, vars); + if (r == false || str == NULL) + return false; + + i--; // -1 because loop will increment i + } + } + + // if (quotes != NO_QUOTE) + // { + // // error: quote not closed + // fprintf(stderr, "Error: quote not closed in string: %s\n", str); + // return false; + // } + + if (len != strlen(str)) + { + char *new_str = realloc(str, strlen(str) + 1); + if (new_str == NULL) + // error: realloc fail + return false; + + l->data = new_str; + } + + l = l->next; + } + return true; +} diff --git a/src/expansion/expansion.h b/src/expansion/expansion.h index e69de29..ce22e38 100644 --- a/src/expansion/expansion.h +++ b/src/expansion/expansion.h @@ -0,0 +1,43 @@ +#ifndef EXPANSION_H +#define EXPANSION_H + +#include +#include + +#include "../utils/ast/ast.h" +#include "../utils/hash_map/hash_map.h" + +enum quote_state +{ + NO_QUOTE, + SINGLE_QUOTE, + DOUBLE_QUOTE +}; + +/** + * Parse a variable from a string starting with '$'. + * @param str The input string starting with '$'. It must start with '$'. + * @param res Pointer to a char pointer that will be set to the extracted + * variable name. + * @return The number of characters processed in the input string. + */ +size_t parse_var_name(char *str, char **res); + +/** + * Parse a subshell string enclosed in parentheses. + * @param str The input string starting with '('. It must start with '('. + * @param res Pointer to a char pointer that will be set to the extracted + * subshell string. + * @return The number of characters processed in the input string. + */ +size_t parse_subshell_str(char *str, char **res); + +/** + * Expand variables in an AST command using the provided variable map. + * @param command The AST command to expand. + * @param vars The hash map containing variables. + * @return A new AST command with variables expanded, or NULL on error. + */ +bool expand(struct ast_command *command, const struct hash_map *vars); + +#endif /* ! EXPANSION_H */ diff --git a/src/io_backend/Makefile.am b/src/io_backend/Makefile.am index 7f056b2..0cfadff 100644 --- a/src/io_backend/Makefile.am +++ b/src/io_backend/Makefile.am @@ -6,6 +6,4 @@ libio_backend_a_SOURCES = \ libio_backend_a_CPPFLAGS = -I$(top_srcdir)/src -libio_backend_a_CFLAGS = -std=c99 -pedantic -Werror -Wall -Wextra -Wvla - noinst_LIBRARIES = libio_backend.a diff --git a/src/io_backend/io_backend.c b/src/io_backend/io_backend.c index e69de29..1ce256e 100644 --- a/src/io_backend/io_backend.c +++ b/src/io_backend/io_backend.c @@ -0,0 +1,138 @@ +#define _POSIX_C_SOURCE 200809L + +#include "io_backend.h" + +#include +#include +#include + +// === Static variables + +#include "../utils/args/args.h" + +static struct iob_context context; +static FILE *input = NULL; +static char *stream_buf = NULL; +static size_t stream_buf_size = 0; +static enum iob_state state = IOB_STATE_NOT_INITIALIZED; + +// === Functions + +int iob_init(struct iob_context *ctx) +{ + if (state != IOB_STATE_NOT_INITIALIZED) + return IOB_ERROR_MODULE_ALREADY_INITIALIZED; + + context = *ctx; + + switch (context.mode) + { + case IOB_MODE_STDIN: + input = stdin; + state = IOB_STATE_READY; + return 0; + + case IOB_MODE_SCRIPT: + if (context.args == NULL) + return IOB_ERROR_BAD_ARG; + input = fopen(context.args, "r"); + if (input == NULL) + return IOB_ERROR_CANNOT_OPEN_FILE; + state = IOB_STATE_READY; + return 0; + + case IOB_MODE_CMD: + if (context.args == NULL) + return IOB_ERROR_BAD_ARG; + state = IOB_STATE_READY; + return 0; + + default: + return IOB_ERROR_BAD_ARG; + } +} + +void iob_close(void) +{ + fclose(input); + if ((context.mode == IOB_MODE_STDIN || context.mode == IOB_MODE_SCRIPT) + && stream_buf != NULL) + { + free(stream_buf); + stream_buf_size = 0; + } + state = IOB_STATE_NOT_INITIALIZED; +} + +ssize_t stream_read(char **stream) +{ + // Check args + if (stream == NULL) + return IOB_ERROR_BAD_ARG; + + // Check env + if (state == IOB_STATE_NOT_INITIALIZED) + return IOB_ERROR_MODULE_NOT_INITIALIZED; + if (state == IOB_STATE_FINISHED) + return 0; + if (state == IOB_STATE_ERROR) + return IOB_ERROR_GENERIC; + + // Use input + if (context.mode == IOB_MODE_STDIN || context.mode == IOB_MODE_SCRIPT) + { + ssize_t nread = getline(&stream_buf, &stream_buf_size, input); + if (nread == -1) + { + state = IOB_STATE_FINISHED; + // MAGNIFICO + // malloc(1); + *stream_buf = EOF; + *stream = stream_buf; + return 1; + } + else if (nread < 0) + state = IOB_STATE_ERROR; + + *stream = stream_buf; + return nread; + } + // Use args + else if (context.mode == IOB_MODE_CMD) + { + *stream = context.args; + size_t len = strlen(context.args); + context.args[len] = EOF; + return len + 1; + } + else + { + *stream = NULL; + return IOB_ERROR_GENERIC; + } +} + +int iob_config_from_args(struct args_options *args, struct iob_context *ctx) +{ + switch (args->type) + { + case INPUT_STDIN: + ctx->mode = IOB_MODE_STDIN; + ctx->args = NULL; + break; + + case INPUT_FILE: + ctx->mode = IOB_MODE_SCRIPT; + ctx->args = (char *)args->input_source; + break; + + case INPUT_CMD: + ctx->mode = IOB_MODE_CMD; + ctx->args = (char *)args->input_source; + break; + + default: + return IOB_ERROR_GENERIC; + } + return 0; +} diff --git a/src/io_backend/io_backend.h b/src/io_backend/io_backend.h index e69de29..686d604 100644 --- a/src/io_backend/io_backend.h +++ b/src/io_backend/io_backend.h @@ -0,0 +1,73 @@ +#ifndef IO_BACKEND_H +#define IO_BACKEND_H + +#include + +#include "../utils/args/args.h" + +// Error codes +#define IOB_ERROR_GENERIC -1 +#define IOB_ERROR_BAD_ARG -2 +#define IOB_ERROR_MODULE_NOT_INITIALIZED -3 +#define IOB_ERROR_MODULE_ALREADY_INITIALIZED -4 +#define IOB_ERROR_CANNOT_OPEN_FILE -5 + +enum iob_mode +{ + IOB_MODE_NULL = 0, + IOB_MODE_STDIN, + IOB_MODE_SCRIPT, + IOB_MODE_CMD +}; + +enum iob_state +{ + IOB_STATE_NOT_INITIALIZED, + IOB_STATE_READY, + IOB_STATE_FINISHED, + IOB_STATE_ERROR +}; + +/* @struct iob_context + * @var mode + * @var args contains + * the script name when mode is set to IOB_MODE_SCRIPT, + * the command to execute when mode is set to IOB_MODE_CMD + */ +struct iob_context +{ + enum iob_mode mode; + char *args; +}; + +/** + * @brief Converts struct arg_options to iob_context + * + * @param args The arguments options struct + * @param ctx The IO Backend context struct to populate + * @return int 0 on success, negative value on error + */ +int iob_config_from_args(struct args_options *args, struct iob_context *ctx); + +/* + * @brief Initializes the IO Backend module + * + * @param context contains the input mode and the args + * @return 0 on success, the corresponding error code otherwise + */ +int iob_init(struct iob_context *context); + +/* @brief Closes the opened buffers and the module gracefully + */ +void iob_close(void); + +/* @brief reads at most one line of the input and stores it into *stream + * + * @param stream is a pointer that will be set to a string to parse + * @return the number of read characters if positive, + * zero if finished (reached EOF), + * the error code otherwise + */ +ssize_t stream_read(char **stream); + +#endif /* ! IO_BACKEND_H */ diff --git a/src/lexer/Makefile.am b/src/lexer/Makefile.am index a113221..4dad036 100644 --- a/src/lexer/Makefile.am +++ b/src/lexer/Makefile.am @@ -2,10 +2,8 @@ lib_LIBRARIES = liblexer.a liblexer_a_SOURCES = \ lexer.c \ - lexer.h + lexer_utils.c liblexer_a_CPPFLAGS = -I$(top_srcdir)/src -liblexer_a_CFLAGS = -std=c99 -pedantic -Werror -Wall -Wextra -Wvla - noinst_LIBRARIES = liblexer.a diff --git a/src/lexer/lexer.c b/src/lexer/lexer.c index e69de29..fd5764d 100644 --- a/src/lexer/lexer.c +++ b/src/lexer/lexer.c @@ -0,0 +1,218 @@ +#include "lexer.h" + +#include +#include +#include +#include +#include +#include + +#include "../io_backend/io_backend.h" +#include "../utils/string_utils/string_utils.h" +#include "lexer_utils.h" + +/* @brief: sets the ctx->current_token to [tok]. + * this function is called by token_peek(). + */ +static void update_current_token(struct token *tok, struct lexer_context *ctx) +{ + ctx->current_token = tok; +} + +/* @brief: frees the last token and sets it to [tok]. + * Also sets ctx->current_token to NULL. + * this function is called by token_pop(). + */ +static void update_previous_token(struct token *tok, struct lexer_context *ctx) +{ + free_token(&ctx->previous_token); + ctx->previous_token = tok; +} +/* @brief: updates the current position in the stream. + * [stream] += [i] + * Also frees the last sent token, and sets it to ctx->current_token. + * Current token is then set to NULL. + * This function is called by token_pop(). + */ +static void save_state(char *stream, ssize_t i, struct lexer_context *ctx) +{ + ctx->remaining_chars -= i; + ctx->end_previous_token = stream + i; + + update_previous_token(ctx->current_token, ctx); + update_current_token(NULL, ctx); +} + +/* + * @brief: Updates the lexing_mode to LEXER_NORMAL + * if the SECOND quote is found at stream[i]. + * Updates the lexing_mode to the corresponding quote type + * if the FIRST quote of any type is found. + * + * @return: true if an update was done. false otherwise. + */ +static bool update_lexing_mode(char *stream, ssize_t i, + enum lexing_mode *lexing_mode) +{ + enum lexing_mode mode_before_update = *lexing_mode; + + // FIRST quote + if (*lexing_mode == LEXER_NORMAL) + { + if (stream[i] == '"') + *lexing_mode = LEXER_DOUBLE_QUOTE; + + if (stream[i] == '\'') + *lexing_mode = LEXER_QUOTE; + } + + // SECOND quote + else + { + if (*lexing_mode == LEXER_QUOTE && stream[i] == '\'') + *lexing_mode = LEXER_NORMAL; + if (*lexing_mode == LEXER_DOUBLE_QUOTE && stream[i] == '"') + *lexing_mode = LEXER_NORMAL; + } + + return *lexing_mode != mode_before_update; +} + +/* @brief: updates the flags only_digits and has_equal. + * according to the character at stream[i]. + */ +static void update_flags(char *stream, ssize_t i, struct token_info *info) +{ + if (stream[i] == '=' && !info->has_equal) + { + if (i == 0) + { + perror("Syntax error: word start with a '='"); + return; + } + else + info->has_equal = true; + } + else if (!isdigit(stream[i]) && info->only_digits) + { + info->only_digits = false; + } +} + +struct token *peek_token(struct lexer_context *ctx) +{ + stream_init(ctx); + + // Usefull to know if we are inside a quote or double quote + enum lexing_mode lexing_mode = LEXER_NORMAL; + struct token_info info = { true, 0 }; + char *stream = ctx->end_previous_token; + ssize_t i = 0; + + // we already created the upcoming token during the previous call to peek() + if (ctx->current_token != NULL) + { + return ctx->current_token; + } + + while (i < ctx->remaining_chars) + { + // true if we didn't encounter a quote of any type at stream[i] + // AND we are not inside quotes + if (!update_lexing_mode(stream, i, &lexing_mode) + && lexing_mode == LEXER_NORMAL) + { + update_flags(stream, i, &info); + if (is_special_char(stream, i)) + { + if (i == 0) // where we create spe_char token + i += len_op_sepchar(stream, i); + break; + } + if (isblank(stream[i])) + { + break; + } + } + else if (stream[i] == EOF) + { + fprintf(stderr, "Lexing error: unmatched quote\n"); + + // error handling + return NULL; + } + i++; + } + + struct token *tok = new_token(stream, i, &info); + // if token is comment, we don't want it + + if (tok->type == TOKEN_COMMENT) + { + // Find next newline or EOF. + go_end_of_line(ctx); + + free_token(&tok); + tok = peek_token(ctx); + } + update_current_token(tok, ctx); + return tok; +} + +struct token *pop_token(struct lexer_context *ctx) +{ + stream_init(ctx); + + // Usefull to know if we are inside a quote or double quote + enum lexing_mode lexing_mode = LEXER_NORMAL; + struct token_info info = { true, 0 }; + char *stream = ctx->end_previous_token; + ssize_t i = 0; + + if (ctx->current_token != NULL && ctx->current_token->type == TOKEN_EOF) + { + // we reached end of input, frees all the token still allocated. + free_token(&ctx->previous_token); + free_token(&ctx->current_token); + return NULL; + } + + while (i < ctx->remaining_chars) + { + // true if we didn't encounter a quote of any type at stream[i] + // AND we are not inside quotes + if (!update_lexing_mode(stream, i, &lexing_mode) + && lexing_mode == LEXER_NORMAL) + { + update_flags(stream, i, &info); + if (is_special_char(stream, i)) + { + if (i == 0) // where we create spe_char token + i += len_op_sepchar(stream, i); + break; + } + if (isblank(stream[i])) + { + break; + } + } + else if (stream[i] == EOF) + { + fprintf(stderr, "Lexing error: unmatched quote\n"); + + // error handling + return NULL; + } + i++; + } + + // just in case peek() was not called before poping. + // (this should never happen) + if (ctx->current_token == NULL) + { + ctx->current_token = new_token(stream, i, &info); + } + save_state(stream, i, ctx); + + return ctx->previous_token; +} diff --git a/src/lexer/lexer.h b/src/lexer/lexer.h index e69de29..8c5b93e 100644 --- a/src/lexer/lexer.h +++ b/src/lexer/lexer.h @@ -0,0 +1,37 @@ +#ifndef LEXER_H +#define LEXER_H + +#include + +#include "lexer_utils.h" + +/* + * @brief returns true if token is a redir type, false otherwise + */ +bool is_token_redir(struct token *token); + +/* + * @brief: returns the next (newly allocated) token without consuming it. + * if end of input is reached, returns a token of type TOKEN_EOF. + */ +struct token *peek_token(struct lexer_context *ctx); + +/* + * @brief: returns the next (newly allocated) token and consumes it. + * if end of input is reached, returns a token of type TOKEN_EOF. + * It also frees the last token created if there was one. + * + * @warning: if the last returned token was a token EOF, it frees it + * and returns NULL. This means that after peeking a token EOF + * in the parser, there must be EXACTLY ONE call to pop_token(). + */ +struct token *pop_token(struct lexer_context *ctx); + +/* @note: maybe usefull for subshells. + * + * @warning: NOT IMPLEMENTED. + */ + +struct token *get_token_str(void); + +#endif /* ! LEXER_H */ diff --git a/src/lexer/lexer_utils.c b/src/lexer/lexer_utils.c new file mode 100644 index 0000000..53c3603 --- /dev/null +++ b/src/lexer/lexer_utils.c @@ -0,0 +1,351 @@ +#include "lexer_utils.h" + +#include +#include + +#include "../io_backend/io_backend.h" +#include "../utils/string_utils/string_utils.h" + +/* @brief: if a special character is found at [begin], + * [tok->token_type] is set accordingly + */ +static void set_token_spechar(struct token *tok, char *begin, ssize_t size) +{ + if (size != 1) + return; + switch (begin[0]) + { + case EOF: + tok->type = TOKEN_EOF; + break; + case ';': + tok->type = TOKEN_SEMICOLON; + break; + case '\n': + tok->type = TOKEN_NEWLINE; + break; + case '`': + tok->type = TOKEN_GRAVE; + break; + case '#': + tok->type = TOKEN_COMMENT; + break; + case '\\': + tok->type = TOKEN_BACKSLASH; + break; + case '(': + tok->type = TOKEN_LEFT_PAREN; + break; + case ')': + tok->type = TOKEN_RIGHT_PAREN; + break; + case '{': + tok->type = TOKEN_LEFT_BRACKET; + break; + case '}': + tok->type = TOKEN_RIGHT_BRACKET; + break; + case '*': + tok->type = TOKEN_STAR; + break; + default: + break; + } +} + +/* @brief: if a keyword is found at [begin], + * [tok->token_type] is set accordingly + */ +static void set_token_keyword(struct token *tok, char *begin, ssize_t size) +{ + if (tok->type != TOKEN_NULL || size == 0) + return; + if (strncmp(begin, "!", size) == 0 && size == 1) + tok->type = TOKEN_NEGATION; + else if (strncmp(begin, "if", size) == 0 && size == 2) + tok->type = TOKEN_IF; + else if (strncmp(begin, "fi", size) == 0 && size == 2) + tok->type = TOKEN_FI; + else if (strncmp(begin, "then", size) == 0 && size == 4) + tok->type = TOKEN_THEN; + else if (strncmp(begin, "else", size) == 0 && size == 4) + tok->type = TOKEN_ELSE; + else if (strncmp(begin, "elif", size) == 0 && size == 4) + tok->type = TOKEN_ELIF; + else if (strncmp(begin, "for", size) == 0 && size == 3) + tok->type = TOKEN_FOR; + else if (strncmp(begin, "while", size) == 0 && size == 5) + tok->type = TOKEN_WHILE; + else if (strncmp(begin, "until", size) == 0 && size == 5) + tok->type = TOKEN_UNTIL; + else if (strncmp(begin, "do", size) == 0 && size == 2) + tok->type = TOKEN_DO; + else if (strncmp(begin, "done", size) == 0 && size == 4) + tok->type = TOKEN_DONE; + else if (strncmp(begin, "export", size) == 0 && size == 6) + tok->type = TOKEN_EXPORT; + + // no keywords found. + if (tok->type == TOKEN_NULL) + return; + + tok->data = calloc(size + 1, sizeof(char)); + if (tok->data == NULL) + { + perror("could not allocate memory in lexer"); + return; + } + strncpy(tok->data, begin, size); +} + +/* @brief: if an operator is found at [begin], + * [tok->token_type] is set accordingly + */ +static void set_token_operator(struct token *tok, char *begin, ssize_t size) +{ + if (tok->type != TOKEN_NULL) + return; + if (strncmp(begin, "&&", size) == 0 && size == 2) + { + tok->type = TOKEN_AND; + } + else if (strncmp(begin, "||", size) == 0 && size == 2) + { + tok->type = TOKEN_OR; + } + + else if (strncmp(begin, ">", size) == 0 && size == 1) + { + tok->type = TOKEN_REDIR_RIGHT; + } + else if (strncmp(begin, "<", size) == 0 && size == 1) + { + tok->type = TOKEN_REDIR_LEFT; + } + else if (strncmp(begin, ">>", size) == 0 && size == 2) + { + tok->type = TOKEN_REDIR_DOUBLE_RIGHT; + } + else if (strncmp(begin, ">&", size) == 0 && size == 2) + { + tok->type = TOKEN_REDIR_RIGHT_AMP; + } + else if (strncmp(begin, ">|", size) == 0 && size == 2) + { + tok->type = TOKEN_REDIR_RIGHT_PIPE; + } + else if (strncmp(begin, "<&", size) == 0 && size == 2) + { + tok->type = TOKEN_REDIR_LEFT_AMP; + } + else if (strncmp(begin, "<>", size) == 0 && size == 2) + { + tok->type = TOKEN_REDIR_LEFT_RIGHT; + } + else if (strncmp(begin, "|", size) == 0 && size == 1) + { + tok->type = TOKEN_PIPE; + } +} + +/* @brief: if token_type has not yet been set, then it is a TOKEN_WORD + * Also allocates the data and fills it. + */ +static void set_token_word(struct token *tok, char *begin, ssize_t size) +{ + if (tok->type == TOKEN_NULL && size != 0) + { + tok->type = TOKEN_WORD; + tok->data = calloc(size + 1, sizeof(char)); + if (tok->data == NULL) + return; + strncpy(tok->data, begin, size); + } +} + +/* @brief: Sets the token to an assignment_word + * Also allocates the data and fills it. + */ +static void set_token_assignment(struct token *tok, char *begin, ssize_t size) +{ + if (tok->type == TOKEN_NULL && size != 0) + { + tok->type = TOKEN_ASSIGNMENT_WORD; + tok->data = calloc(size + 1, sizeof(char)); + if (tok->data == NULL) + return; + strncpy(tok->data, begin, size); + } +} + +/* @brief: Sets the token to an IO number + * Also allocates the data and fills it. + */ +static void set_token_ION(struct token *tok, char *begin, ssize_t size) +{ + if (tok->type == TOKEN_NULL && size != 0) + { + tok->type = TOKEN_IONUMBER; + tok->data = calloc(size + 1, sizeof(char)); + if (tok->data == NULL) + return; + strncpy(tok->data, begin, size); + } +} + +/* @brief: check if [c] is a delimiter for end of line. + * @return: true if [c] == '\n' or EOF. false otherwise. + */ +static bool is_end_of_line(char c) +{ + return c == EOF || c == '\n'; +} + +// === Functions + +bool is_token_redir(struct token *token) +{ + switch (token->type) + { + case TOKEN_REDIR_LEFT: + case TOKEN_REDIR_RIGHT: + case TOKEN_REDIR_LEFT_RIGHT: + case TOKEN_REDIR_DOUBLE_RIGHT: + case TOKEN_REDIR_LEFT_AMP: + case TOKEN_REDIR_RIGHT_AMP: + case TOKEN_REDIR_RIGHT_PIPE: + return true; + default: + return false; + } +} + +bool is_special_char(char *stream, ssize_t i) +{ + char c = stream[i]; + if (c == EOF) + return true; + if (i > 0 && c == '#' && stream[i - 1] == '$') + return false; // the edge case of $# + if (i > 0 && stream[i - 1] == '\\') + return false; // TODO handle backslash better + // this doesnt work, ex : echo \\#comment + // (need to count the previous consequtive backslashes) + + char special_chars[] = "\n'\"`;#|&(){}<>*"; + return strchr(special_chars, c) != NULL; +} + +struct token *new_token(char *begin, ssize_t size, struct token_info *info) +{ + struct token *tok = calloc(1, sizeof(struct token)); + if (tok == NULL) + return NULL; + + if (info->only_digits) + set_token_ION(tok, begin, size); + else if (info->has_equal) + set_token_assignment(tok, begin, size); + else + { + set_token_keyword(tok, begin, size); + set_token_operator(tok, begin, size); + set_token_spechar(tok, begin, size); + set_token_word(tok, begin, size); + } + + return tok; +} + +void destroy_lexer_context(struct lexer_context *ctx) +{ + struct token *prev = ctx->previous_token; + struct token *cur = ctx->current_token; + if (ctx == NULL) + return; + if (prev != NULL) + free_token(&prev); + if (cur != NULL) + free_token(&cur); + free(ctx); +} + +void free_token(struct token **tok) +{ + if (tok == NULL || *tok == NULL) + return; + if ((*tok)->data != NULL) + free((*tok)->data); + free(*tok); + *tok = NULL; +} + +void stream_init(struct lexer_context *ctx) +{ + char *stream; + + if (ctx->remaining_chars == 0) // at the begining + { + ctx->remaining_chars = stream_read(&stream); + } + else + { + stream = ctx->end_previous_token; + } + + char *trimed_stream = trim_blank_left(stream); + ctx->remaining_chars -= trimed_stream - stream; + + ctx->end_previous_token = trimed_stream; +} + +ssize_t len_op_sepchar(char *stream, ssize_t i) +{ + if (!is_special_char(stream, i)) + return -1; // should never happen + + // OR + if (stream[i] == '|' && stream[i + 1] == '|') + return 2; + + // AND + if (stream[i] == '&' && stream[i + 1] == '&') + return 2; + + // special chars + if (stream[i] != '>' && stream[i] != '<') + return 1; + + // REDIRS + + if (stream[i] == '<') + { + if (stream[i + 1] == '&' || stream[i + 1] == '>') + return 2; // <&, <> + } + else if (stream[i + 1] == '>' || stream[i + 1] == '|' + || stream[i + 1] == '&') + return 2; // >>, >&, >| + + return 1; // >, < +} + +void go_end_of_line(struct lexer_context *ctx) +{ + if (ctx == NULL || ctx->end_previous_token == NULL) + return; + + ssize_t i = 0; + while (!is_end_of_line(ctx->end_previous_token[i])) + { + i++; + } + ctx->end_previous_token += i; + ctx->remaining_chars -= i; +} + +void get_next_stream(struct lexer_context *ctx) +{ + ctx->remaining_chars = 0; + stream_init(ctx); +} diff --git a/src/lexer/lexer_utils.h b/src/lexer/lexer_utils.h new file mode 100644 index 0000000..32b3793 --- /dev/null +++ b/src/lexer/lexer_utils.h @@ -0,0 +1,146 @@ +#ifndef LEXER_UTILS_H +#define LEXER_UTILS_H + +#include +#include +#include + +struct lexer_context +{ + char *end_previous_token; + ssize_t remaining_chars; + + struct token *previous_token; + struct token *current_token; +}; + +/* @brief: frees all fields of ctx and sets ctx to NULL. + */ +void destroy_lexer_context(struct lexer_context *ctx); + +enum lexing_mode +{ + LEXER_NORMAL, + LEXER_QUOTE, + LEXER_DOUBLE_QUOTE +}; + +enum token_type +{ + // Blanks + TOKEN_NULL = 0, + TOKEN_EOF, + TOKEN_NEWLINE, + + // words + TOKEN_WORD, + TOKEN_ASSIGNMENT_WORD, + + // Special characters + TOKEN_GRAVE, + TOKEN_SEMICOLON, + TOKEN_COMMENT, + TOKEN_STAR, + TOKEN_BACKSLASH, + TOKEN_DOLLAR, + TOKEN_LEFT_PAREN, + TOKEN_RIGHT_PAREN, + TOKEN_LEFT_BRACKET, + TOKEN_RIGHT_BRACKET, + TOKEN_PIPE, + TOKEN_NEGATION, + + // Redirections + TOKEN_REDIR_LEFT, + TOKEN_REDIR_RIGHT, + TOKEN_REDIR_LEFT_RIGHT, + TOKEN_REDIR_DOUBLE_RIGHT, + TOKEN_REDIR_LEFT_AMP, + TOKEN_REDIR_RIGHT_AMP, + TOKEN_REDIR_RIGHT_PIPE, + + TOKEN_IONUMBER, + + // Keywords + TOKEN_IF, + TOKEN_THEN, + TOKEN_ELSE, + TOKEN_FI, + TOKEN_ELIF, + TOKEN_AND, + TOKEN_OR, + TOKEN_FOR, + TOKEN_WHILE, + TOKEN_UNTIL, + TOKEN_CASE, + TOKEN_EXPORT, + TOKEN_DO, + TOKEN_DONE +}; + +struct token +{ + enum token_type type; + char *data; +}; + +// used to give info from lexing when creating a new token. +struct token_info +{ + // usefull to detect IO numbers. + // tells us if we only lexed digits in current token. + bool only_digits; + + // usefull to detect assignments, and syntax errors with '='. + bool has_equal; +}; + +/* @return: true if a special character from the grammar was found at stream[i], + * false otherwise. + */ +bool is_special_char(char *stream, ssize_t i); + +/* @brief: return a newly allocated token, with the type corresponding + * to the info given in arguments. + * The data contains [size] char, starting from [begin]. + * + * @return: NULL on error, a token otherwise. + */ +struct token *new_token(char *begin, ssize_t size, struct token_info *info); + +/* @brief: frees the token given in argument + */ +void free_token(struct token **tok); + +/* + * @brief: checks if the stream used for the last token creation is empty. + * If it is, it calls stream_read() from IO_backend, + * and sets [remaining_chars]. + * If not, it starts from the end of the last token. + * Also trims left blanks before returning. + * + * @return: char* stream from which we tokenise. + */ +void stream_init(struct lexer_context *ctx); + +/* @brief: finds the next '\n' or EOF character, + * starting at [ctx->end_previous_token], + * and updates the stream and remaining_chars accordingly. + * + * @note: Daft Punk. bang. + */ +void go_end_of_line(struct lexer_context *ctx); + +/* @brief: this function is called when we found a special character + * in the stream. This can either be an operator (ig '>>' or '<&' etc), + * or a special char (ig '\' or '#' etc). + * @return: the length of the operator/special char found (can be 1, 2 or 3). + * -1 on error. + */ +ssize_t len_op_sepchar(char *stream, ssize_t i); + +/* @brief: drops the current stream and asks IOB for a new one + */ +void get_next_stream(struct lexer_context *ctx); + +#endif /* LEXER_UTILS_H */ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..cf3b791 --- /dev/null +++ b/src/main.c @@ -0,0 +1,71 @@ +// === Includes +#include +#include + +#include "execution/execution.h" +#include "io_backend/io_backend.h" +#include "lexer/lexer.h" +#include "parser/parser.h" +#include "utils/args/args.h" +#include "utils/main_loop/main_loop.h" +#include "utils/vars/vars.h" + +int main(int argc, char **argv) +{ + struct hash_map *vars = vars_init(); + if (vars == NULL) + { + fprintf(stderr, "Error: Failed to initialize variables hash map\n"); + return ERR_MALLOC; + } + + // Create the options struct (with argument handler) + struct args_options options; + int return_code = args_handler(argc, argv, &options, vars); + if (return_code != 0) + { + print_usage(stderr, argv[0]); + return err_input(&vars, NULL); + } + // args_print(&options); + + // Initialize variables hash map + + // Create the IO-Backend context struct + struct iob_context io_context = { 0 }; + + // Convert args_options to iob_context + return_code = iob_config_from_args(&options, &io_context); + if (return_code != 0) + { + fprintf(stderr, + "Error: Failed to configure IO Backend from arguments\n"); + return err_input(&vars, NULL); + } + + // Init IO Backend (with the context struct) + return_code = iob_init(&io_context); + if (return_code != 0) + { + fprintf(stderr, + "Error: IO Backend initialization failed with code %d\n", + return_code); + return err_input(&vars, NULL); + } + + // init lexer context + struct lexer_context *ctx = calloc(1, sizeof(struct lexer_context)); + + // init parser + if (!parser_init()) + { + perror("parser initialization failed."); + return err_input(&vars, ctx); + } + + return_code = main_loop(ctx, vars); + + parser_close(); + + return return_code; +} diff --git a/src/parser/Makefile.am b/src/parser/Makefile.am index 51c8cb6..642414b 100644 --- a/src/parser/Makefile.am +++ b/src/parser/Makefile.am @@ -2,10 +2,10 @@ lib_LIBRARIES = libparser.a libparser_a_SOURCES = \ parser.c \ - parser.h + grammar.c \ + grammar_basic.c \ + grammar_advanced.c libparser_a_CPPFLAGS = -I$(top_srcdir)/src -libparser_a_CFLAGS = -std=c99 -pedantic -Werror -Wall -Wextra -Wvla - noinst_LIBRARIES = libparser.a diff --git a/src/parser/grammar.c b/src/parser/grammar.c new file mode 100644 index 0000000..e2a4887 --- /dev/null +++ b/src/parser/grammar.c @@ -0,0 +1,299 @@ +// === Includes +#include "grammar.h" + +#include +#include + +#include "grammar_basic.h" + +// === Static variables + +// rule-indexed array containing firsts +static struct firsts_list *firsts_map = NULL; + +// === Static functions + +/* @brief Add a token to a rule's firsts (in firsts_map) + * + * @arg rule the rule to which add a first + * @arg token the token to add to the rule's firsts + * @return true on success, false on error + */ +static bool add_first(enum rule rule, enum token_type token) +{ + struct firsts_list *item = &firsts_map[rule]; + if (item->tokens != NULL) + { + // Check for duplicates + for (size_t i = 0; i < item->list_length; i++) + { + if (item->tokens[i] == token) + return true; + } + + // Append + item->list_length++; + item->tokens = realloc(item->tokens, + (item->list_length) * sizeof(enum token_type)); + } + else + { + // Create entry + item->list_length = 1; + item->tokens = calloc(1, sizeof(enum token_type)); + } + + // Check for alloc error + if (item->tokens == NULL) + { + item->list_length = 0; + return false; + } + + // Fill + item->tokens[item->list_length - 1] = token; + + return true; +} + +/* @brief Add a list of tokens to a rule's firsts (in firsts_map) + * + * @arg rule the rule to which add a first + * @arg tokens_list the list of tokens to add to the rule's firsts + * @return true on success, false on error + */ +static bool add_firsts(enum rule rule, struct firsts_list *tokens_list) +{ + for (size_t i = 0; i < tokens_list->list_length; i++) + { + bool res = add_first(rule, tokens_list->tokens[i]); + if (!res) + return false; + } + return true; +} + +/* @brief initializes the firsts_map static variable (does not populate it) + * @return true on success, false on error + */ +static bool init_firsts_map(void) +{ + firsts_map = calloc(NUMBER_OF_RULES, sizeof(struct firsts_list)); + if (firsts_map == NULL) + { + perror("Internal error: couldn't create the firsts_map (is your memory " + "full ?)"); + return false; + } + + return true; +} + +static void add_first_redir(void) +{ + add_first(RULE_REDIRECTION, TOKEN_IONUMBER); + add_first(RULE_REDIRECTION, TOKEN_REDIR_LEFT); + add_first(RULE_REDIRECTION, TOKEN_REDIR_RIGHT); + add_first(RULE_REDIRECTION, TOKEN_REDIR_LEFT_RIGHT); + add_first(RULE_REDIRECTION, TOKEN_REDIR_DOUBLE_RIGHT); + add_first(RULE_REDIRECTION, TOKEN_REDIR_LEFT_AMP); + add_first(RULE_REDIRECTION, TOKEN_REDIR_RIGHT_AMP); + add_first(RULE_REDIRECTION, TOKEN_REDIR_RIGHT_PIPE); +} + +// Adds only direct tokens to rules firsts into the firsts map +static void add_firsts_tokens(void) +{ + // Redirection + add_first_redir(); + // %RIP Matteo 30/01/2026 + // %RAX Guillem 30/01/2026 hehe + + // If + add_first(RULE_IF, TOKEN_IF); + + // Else clause + add_first(RULE_ELSE_CLAUSE, TOKEN_ELSE); + add_first(RULE_ELSE_CLAUSE, TOKEN_ELIF); + + // For + add_first(RULE_FOR, TOKEN_FOR); + + // While + add_first(RULE_WHILE, TOKEN_WHILE); + + // Until + add_first(RULE_UNTIL, TOKEN_UNTIL); + + // Case + add_first(RULE_CASE, TOKEN_CASE); + + // Case item + add_first(RULE_CASE_ITEM, TOKEN_LEFT_PAREN); + add_first(RULE_CASE_ITEM, TOKEN_WORD); + + // Shell command + add_first(RULE_SHELL_COMMAND, TOKEN_LEFT_BRACKET); + add_first(RULE_SHELL_COMMAND, TOKEN_LEFT_PAREN); + + // Simple command + add_first(RULE_SIMPLE_COMMAND, TOKEN_WORD); + add_first(RULE_SIMPLE_COMMAND, TOKEN_EXPORT); + + // Element + add_first(RULE_ELEMENT, TOKEN_WORD); + add_first(RULE_ELEMENT, TOKEN_ASSIGNMENT_WORD); + + // Prefix + add_first(RULE_PREFIX, TOKEN_ASSIGNMENT_WORD); + + // Pipeline + add_first(RULE_PIPELINE, TOKEN_WORD); + + // Compound list + add_first(RULE_COMPOUND_LIST, TOKEN_NEWLINE); + + // Input + add_first(RULE_INPUT, TOKEN_NEWLINE); + add_first(RULE_INPUT, TOKEN_EOF); + + // Funcdec + add_first(RULE_FUNCDEC, TOKEN_WORD); +} + +// Adds only firsts that depend on other rules to the firsts map +// WARNING order matters +static void add_firsts_rec(void) +{ + // Case clause + add_firsts(RULE_CASE_CLAUSE, first(RULE_CASE_ITEM)); + + // Element + add_firsts(RULE_ELEMENT, first(RULE_REDIRECTION)); + + // Prefix + add_firsts(RULE_PREFIX, first(RULE_REDIRECTION)); + + // Shell command + add_firsts(RULE_SHELL_COMMAND, first(RULE_IF)); + add_firsts(RULE_SHELL_COMMAND, first(RULE_FOR)); + add_firsts(RULE_SHELL_COMMAND, first(RULE_WHILE)); + add_firsts(RULE_SHELL_COMMAND, first(RULE_UNTIL)); + add_firsts(RULE_SHELL_COMMAND, first(RULE_CASE)); + + // Simple command + add_firsts(RULE_SIMPLE_COMMAND, first(RULE_PREFIX)); + + // Command + add_firsts(RULE_COMMAND, first(RULE_SIMPLE_COMMAND)); + add_firsts(RULE_COMMAND, first(RULE_SHELL_COMMAND)); + add_firsts(RULE_COMMAND, first(RULE_FUNCDEC)); + + // Pipeline + add_firsts(RULE_PIPELINE, first(RULE_COMMAND)); + + // And Or + add_firsts(RULE_AND_OR, first(RULE_PIPELINE)); + + // Compound list + add_firsts(RULE_COMPOUND_LIST, first(RULE_AND_OR)); + + // List + add_firsts(RULE_LIST, first(RULE_AND_OR)); + + // Input + add_firsts(RULE_INPUT, first(RULE_LIST)); +} + +// === Functions + +int grammar_init(void) +{ + // Initialize the firsts map + bool success = init_firsts_map(); + if (success != true) + return false; + + // Populate the firsts map + add_firsts_tokens(); + add_firsts_rec(); + + return true; +} + +void grammar_close(void) +{ + // Deep free firsts map + for (int i = 0; i < NUMBER_OF_RULES; i++) + { + if (firsts_map[i].tokens != NULL) + { + free(firsts_map[i].tokens); + } + } + free(firsts_map); + firsts_map = NULL; +} + +struct firsts_list *first(enum rule rule) +{ + if (firsts_map == NULL || firsts_map[rule].tokens == NULL) + { + perror("Internal error: attempted to get the firsts of a rule without " + "properly initializing the firsts map"); + return NULL; + } + + return &firsts_map[rule]; +} + +bool is_first(struct token token, enum rule rule) +{ + struct firsts_list *firsts = &firsts_map[rule]; + for (size_t i = 0; i < firsts->list_length; i++) + { + if (firsts->tokens[i] == token.type) + return true; + } + + return false; +} + +struct ast *parse_input(struct lexer_context *ctx) +{ + struct token *token = PEEK_TOKEN(); + + if (token->type == TOKEN_EOF) + { + POP_TOKEN(); + return ast_create_end(); + } + + if (token->type == TOKEN_NEWLINE) + { + POP_TOKEN(); + return ast_create_list(NULL); + } + + struct ast *ast = parse_list(ctx); + token = PEEK_TOKEN(); + + if (ast == NULL) + { + if (token != NULL && token->type == TOKEN_EOF) + { + POP_TOKEN(); + } + return NULL; + } + + if (token->type == TOKEN_NEWLINE || token->type == TOKEN_EOF) + { + POP_TOKEN(); + return ast; + } + + perror("Syntax error: expected newline or EOF after list"); + ast_free(&ast); + return NULL; +} diff --git a/src/parser/grammar.h b/src/parser/grammar.h new file mode 100644 index 0000000..020d733 --- /dev/null +++ b/src/parser/grammar.h @@ -0,0 +1,103 @@ +#ifndef GRAMMAR_H +#define GRAMMAR_H + +#include + +#include "../lexer/lexer.h" + +// === Macros + +#define PEEK_TOKEN() \ + peek_token(ctx); \ + if (token == NULL) \ + { \ + perror("Internal error: cannot get the following token"); \ + return NULL; \ + } + +#define POP_TOKEN() \ + pop_token(ctx); \ + if (token == NULL) \ + { \ + perror("Internal error: cannot get the following token"); \ + return NULL; \ + } + +// === Structures + +enum rule +{ + RULE_NULL = 0, + RULE_INPUT, + RULE_LIST, + RULE_AND_OR, + RULE_PIPELINE, + RULE_COMMAND, + RULE_SIMPLE_COMMAND, + RULE_SHELL_COMMAND, + RULE_IF, + RULE_COMPOUND_LIST, + RULE_ELSE_CLAUSE, + RULE_ELEMENT, + RULE_REDIRECTION, + RULE_PREFIX, + RULE_FUNCDEC, + RULE_WHILE, + RULE_UNTIL, + RULE_FOR, + RULE_CASE, + RULE_CASE_CLAUSE, + RULE_CASE_ITEM, + NUMBER_OF_RULES +}; + +struct firsts_list +{ + enum token_type *tokens; // Heap allocated array + size_t list_length; +}; + +// === Functions + +/* + * @brief Initializes the grammar submodule + * @return PARSER_INIT_SUCCESS on success PARSER_INIT_ERROR on error + * @warning Do not use outside the parser + */ +int grammar_init(void); + +/* + * @brief Closes the grammar submodule + * @warning Do not use outside the parser + */ +void grammar_close(void); + +/* + * @brief get the first accepted tokens of a rule + * + * @arg r the rule + * @return the accepted tokens as a firsts_list struct + */ +struct firsts_list *first(enum rule r); + +/* + * @brief tells is token belong to the firsts of a specific rule + * + * @arg token + * @arg rule + * @return true if token belongs to rule's firsts, false otherwise + */ +bool is_first(struct token token, enum rule rule); + +/* @brief Acts as the entry point of the parser, calls parse_list + * + * @code input = list '\n' + * | list EOF + * | '\n' + * | EOF + * ; + * @first first(list), '\n', EOF + */ +struct ast *parse_input(struct lexer_context *ctx); + +#endif /* ! GRAMMAR_H */ diff --git a/src/parser/grammar_advanced.c b/src/parser/grammar_advanced.c new file mode 100644 index 0000000..d1b7efa --- /dev/null +++ b/src/parser/grammar_advanced.c @@ -0,0 +1,240 @@ +#define _POSIX_C_SOURCE 200809L + +#include "grammar_advanced.h" + +#include +#include +#include + +#include "grammar.h" +#include "grammar_basic.h" + +// === Static functions + +static enum ast_redir_type redir_tok_to_ast_type(enum token_type tok_type) +{ + switch (tok_type) + { + case TOKEN_REDIR_LEFT: + return AST_REDIR_TYPE_LESS; + case TOKEN_REDIR_RIGHT: + return AST_REDIR_TYPE_GREAT; + case TOKEN_REDIR_DOUBLE_RIGHT: + return AST_REDIR_TYPE_DGREAT; + case TOKEN_REDIR_LEFT_RIGHT: + return AST_REDIR_TYPE_LESSGREAT; + case TOKEN_REDIR_LEFT_AMP: + return AST_REDIR_TYPE_LESSAND; + case TOKEN_REDIR_RIGHT_AMP: + return AST_REDIR_TYPE_GREATAND; + case TOKEN_REDIR_RIGHT_PIPE: + return AST_REDIR_TYPE_CLOBBER; + default: + return AST_REDIR_TYPE_NULL; + } +} + +/* + * @brief parses a while/until loop starting with the condition + * (after the while/until keyword) + * @arg negate_condition Set to true for until loops, false for while loops + */ +static struct ast *parse_loop(struct lexer_context *ctx, bool negate_condition) +{ + // condition + struct ast *condition = parse_compound_list(ctx); + if (condition == NULL) + return NULL; + if (negate_condition) + { + condition = + ast_create_neg(true, condition); // TODO check result (beware to not + // exceed the function lines limit) + } + + struct token *token = PEEK_TOKEN(); + + // 'do' + if (token->type != TOKEN_DO) + { + ast_free(&condition); + perror("Syntax error: expected the 'do' keyowrd but got a different " + "token"); + return NULL; + } + POP_TOKEN(); + token = PEEK_TOKEN(); + + // body + struct ast *body = parse_compound_list(ctx); + if (body == NULL) + { + ast_free(&condition); + return NULL; + } + token = PEEK_TOKEN(); + + // 'done' + if (token->type != TOKEN_DONE) + { + ast_free(&condition); + perror("Syntax error: expected the 'done' keyowrd but got a different " + "token"); + return NULL; + } + POP_TOKEN(); + + struct ast *result = ast_create_loop(condition, body); + if (result == NULL) + { + ast_free(&condition); + ast_free(&body); + perror("Internal error: could not create ast node (is your memory full " + "?)"); + return NULL; + } + + return result; +} + +// === Functions + +struct ast *parse_redirection(struct lexer_context *ctx) +{ + struct token *token = PEEK_TOKEN(); + int io_number = -1; + if (token->type == TOKEN_IONUMBER) + { + io_number = atoi(token->data); + POP_TOKEN(); + token = PEEK_TOKEN(); + } + + if (!is_token_redir(token)) + { + perror("Syntax error: expected a redirection token but got something " + "else"); + return NULL; + } + + enum ast_redir_type redir_type = redir_tok_to_ast_type(token->type); + POP_TOKEN(); + + token = PEEK_TOKEN(); + if (token->type != TOKEN_WORD) + { + perror("Syntax error: expected a word after redirection"); + return NULL; + } + char *target = strdup(token->data); + POP_TOKEN(); + + return ast_create_redir(target, io_number, redir_type); +} + +struct ast *parse_prefix(struct lexer_context *ctx) +{ + struct token *token = PEEK_TOKEN(); + if (token->type == TOKEN_ASSIGNMENT_WORD) + { + token = POP_TOKEN(); + return ast_create_assignment(token->data, false); + } + else if (is_first(*token, RULE_REDIRECTION)) + return parse_redirection(ctx); + else + { + perror("Syntax error: expected a prefix (redirection or assignment)"); + return NULL; + } +} + +struct ast *parse_funcdec(struct lexer_context *ctx) +{ + struct token *token = PEEK_TOKEN(); + struct ast *value = NULL; + char *func_name = NULL; + + if (token->type != TOKEN_WORD) + { + return NULL; + } + + // word -> func name + POP_TOKEN(); + func_name = strdup(token->data); + + // ( + token = PEEK_TOKEN(); + if (token->type != TOKEN_LEFT_PAREN) + { + free(func_name); + return NULL; + } + POP_TOKEN(); + + // ) + token = PEEK_TOKEN(); + if (token->type != TOKEN_RIGHT_PAREN) + { + free(func_name); + return NULL; + } + POP_TOKEN(); + token = PEEK_TOKEN(); + + // { \n } + while (token->type == TOKEN_NEWLINE) + { + POP_TOKEN(); + token = PEEK_TOKEN(); + } + + // shell_command -> value + value = parse_shell_command(ctx); + if (value == NULL) + { + free(func_name); + return NULL; + } + return ast_create_function(func_name, value); +} + +struct ast *parse_for(struct lexer_context *ctx) +{ + (void)ctx; + perror("Error: usage of a not implemented function (parse_for)"); + return NULL; +} + +struct ast *parse_while(struct lexer_context *ctx) +{ + struct token *token = PEEK_TOKEN(); + + // 'while' + if (token->type != TOKEN_WHILE) + { + perror( + "Internal error: expected a TOKEN_WHILE but got a different type"); + return NULL; + } + POP_TOKEN(); + + return parse_loop(ctx, false); +} + +struct ast *parse_until(struct lexer_context *ctx) +{ + struct token *token = PEEK_TOKEN(); + + // 'until' + if (token->type != TOKEN_UNTIL) + { + perror( + "Internal error: expected a TOKEN_UNTIL but got a different type"); + return NULL; + } + POP_TOKEN(); + + return parse_loop(ctx, true); +} diff --git a/src/parser/grammar_advanced.h b/src/parser/grammar_advanced.h new file mode 100644 index 0000000..3829c49 --- /dev/null +++ b/src/parser/grammar_advanced.h @@ -0,0 +1,68 @@ +#ifndef GRAMMAR_ADVANCED_H +#define GRAMMAR_ADVANCED_H + +#include "grammar.h" + +// === Functions + +/* + * @brief parses a redirection rule + * + * @code redirection = [IONUMBER] ( '>' | '<' | '>>' | '>&' | '<&' | '>|' | '<>' + * ) WORD ; + * + * @first TOKEN_IONUMBER, TOKEN_REDIRECTION + */ +struct ast *parse_redirection(struct lexer_context *ctx); + +/* + * @brief parses a prefix rule + * + * @code prefix = redirection ; + * + * @first first(redirection) + */ +struct ast *parse_prefix(struct lexer_context *ctx); + +/* + * @brief parses a funcdec rule + * @warning Work in progress + * + * @code funcdec = WORD '(' ')' {'\n'} shell_command ; + * + * @first WORD + */ +struct ast *parse_funcdec(struct lexer_context *ctx); + +/* + * @brief parses a for rule + * + * @code rule_for = 'for' WORD + * ( [';'] | [ {'\n'} 'in' { WORD } ( ';' | '\n' ) ] ) + * {'\n'} 'do' compound_list 'done' ; + * + * @first TOKEN_FOR + */ +struct ast *parse_for(struct lexer_context *ctx); + +/* + * @brief parses a while rule + * @warning NOT IMPLEMENTED + * + * @code rule_while = 'while' compound_list 'do' compound_list 'done' ; + * + * @first TOKEN_WHILE + */ +struct ast *parse_while(struct lexer_context *ctx); + +/* + * @brief parses an until rule + * @warning NOT IMPLEMENTED + * + * @code rule_until = 'until' compound_list 'do' compound_list 'done' ; + * + * @first TOKEN_UNTIL + */ +struct ast *parse_until(struct lexer_context *ctx); + +#endif /* ! GRAMMAR_ADVANCED_H */ diff --git a/src/parser/grammar_basic.c b/src/parser/grammar_basic.c new file mode 100644 index 0000000..92113fe --- /dev/null +++ b/src/parser/grammar_basic.c @@ -0,0 +1,627 @@ +#define _POSIX_C_SOURCE 200809L + +#include "grammar_basic.h" + +#include +#include +#include + +#include "../utils/lists/lists.h" +#include "grammar.h" +#include "grammar_advanced.h" + +// === Static functions + +static enum ast_and_or_type and_or_tok_to_ast(enum token_type tok_type) +{ + switch (tok_type) + { + case TOKEN_AND: + return AST_AND_OR_TYPE_AND; + case TOKEN_OR: + return AST_AND_OR_TYPE_OR; + default: + fprintf(stderr, "and_or impossible to init, wrong token type"); + return AST_AND_OR_NULL; + } +} + +/* @brief: frees all the arguments. (helper func) + * @return: NULL. + */ +static void *err_if_rule(struct ast **cond, struct ast **then_clause, + struct ast **else_clause) +{ + ast_free(cond); + ast_free(then_clause); + ast_free(else_clause); + return NULL; +} + +/* @brief: frees command_elements and redirections lists (helper func) + * @return: NULL + */ +static void *err_s_com(struct list *command_elements, struct list *redirections, + struct list *assignments) +{ + list_deep_destroy(command_elements); + list_deep_destroy(redirections); + list_deep_destroy(assignments); + return NULL; +} + +/* @brief: used when export keyword is found, and expects an assignment after. + * @return: an ast_assignment with the field [global] set to true. + */ +static struct ast *parse_export(struct lexer_context *ctx) +{ + struct token *token = PEEK_TOKEN(); + if (token->type != TOKEN_EXPORT) + { + fprintf(stderr, "expected the export keyword in parse_export"); + return NULL; + } + // export + POP_TOKEN(); + + token = PEEK_TOKEN(); + + if (token->type != TOKEN_ASSIGNMENT_WORD) + { + fprintf(stderr, "in parser: export must be followed by 'x=y'"); + return NULL; + } + + // assignment + POP_TOKEN(); + + return ast_create_assignment(token->data, true); +} + +// === Functions + +struct ast *parse_list(struct lexer_context *ctx) +{ + struct list *result_list = NULL; + struct ast *current_node = NULL; + + struct token *token = PEEK_TOKEN(); + + // and_or + current_node = parse_and_or(ctx); + if (current_node == NULL) + return NULL; + result_list = list_append(result_list, current_node); + + // Following and_or commands + token = PEEK_TOKEN(); + while (token->type == TOKEN_SEMICOLON) + { + // Forward + POP_TOKEN(); + token = PEEK_TOKEN(); + + // TODO seems a little akward (not fully compliant with the grammar) + // but it's time consuming to rewrite to only cover edge cases. + // So it'll probably stay like that for now + if (is_first(*token, RULE_AND_OR)) + { + current_node = parse_and_or(ctx); + if (current_node == NULL) + { + struct ast *tmp = ast_create_list(result_list); + ast_free(&tmp); + return NULL; + } + result_list = list_append(result_list, current_node); + token = PEEK_TOKEN(); + } + } + + return ast_create_list(result_list); +} + +struct ast *parse_and_or(struct lexer_context *ctx) +{ + struct ast *result = parse_pipeline(ctx); + if (result == NULL) + return NULL; + struct token *token = PEEK_TOKEN(); + + while (token->type == TOKEN_AND || token->type == TOKEN_OR) + { + // Build AST (left part) + enum ast_and_or_type type = and_or_tok_to_ast(token->type); + struct ast *left = result; + + POP_TOKEN(); + token = PEEK_TOKEN(); + + // Skip newlines + while (token->type == TOKEN_NEWLINE) + { + token = POP_TOKEN(); + token = PEEK_TOKEN(); + } + + // Right part + struct ast *right = parse_pipeline(ctx); + if (right == NULL) + { + ast_free(&left); + return NULL; + } + token = PEEK_TOKEN(); + + result = ast_create_and_or(left, right, type); + if (result == NULL) + { + ast_free(&left); + ast_free(&right); + return NULL; + } + } + + return result; +} + +struct ast *parse_pipeline(struct lexer_context *ctx) +{ + bool negation = false; + struct token *token = PEEK_TOKEN(); + + // Eventual '!' + if (token->type == TOKEN_NEGATION) + { + negation = true; + POP_TOKEN(); + token = PEEK_TOKEN(); + } + + // command rule + struct ast *left = parse_command(ctx); + token = PEEK_TOKEN(); + if (negation) + { + left = ast_create_neg(negation, left); + } + + // Pipes + while (token->type == TOKEN_PIPE) + { + POP_TOKEN(); + token = PEEK_TOKEN(); + + // skip newlines + while (token->type == TOKEN_NEWLINE) + { + POP_TOKEN(); + token = PEEK_TOKEN(); + } + + // command rule + struct ast *right = parse_command(ctx); + token = PEEK_TOKEN(); + + // Create AST + left = ast_create_pipe(left, right); + } + + return left; +} + +struct ast *parse_command(struct lexer_context *ctx) +{ + struct token *token = PEEK_TOKEN(); + struct ast *result = NULL; + + if (is_first(*token, RULE_SIMPLE_COMMAND)) + { + result = parse_simple_command(ctx); + } + else if (is_first(*token, RULE_SHELL_COMMAND)) + { + result = parse_shell_command(ctx); + } + else if (is_first(*token, RULE_FUNCDEC)) + { + result = parse_funcdec(ctx); + } + else + { + perror("Syntax error: unexpected token"); + return NULL; + } + + return result; +} + +struct ast *parse_simple_command(struct lexer_context *ctx) +{ + struct list *command_elements = NULL; + struct list *redirections = NULL; // list of redirection ASTs + struct list *assignments = NULL; + + bool has_prefix = false; + struct token *token = PEEK_TOKEN(); + if (is_first(*token, RULE_PREFIX)) + { + has_prefix = true; + while (is_first(*token, RULE_PREFIX)) + { + struct ast *prefix = parse_prefix(ctx); + if (prefix == NULL) + { + return err_s_com(command_elements, redirections, assignments); + } + if (prefix->type == AST_ASSIGNMENT) + { + assignments = list_append(assignments, prefix); + } + else if (prefix->type == AST_REDIR) + { + redirections = list_append(redirections, prefix); + } + token = PEEK_TOKEN(); + } + } + + if (token->type != TOKEN_WORD) + { + if (!has_prefix && token->type != TOKEN_EXPORT) + { + perror("Expected a command but got a different token type"); + return err_s_com(command_elements, redirections, assignments); + } + if (token->type == TOKEN_EXPORT) + { + struct ast *assignment_export = parse_export(ctx); + if (assignment_export == NULL) + return err_s_com(command_elements, redirections, assignments); + + assignments = list_append(assignments, assignment_export); + } + } + else // TOKEN WORD + { + char *command = strdup(token->data); + command_elements = list_append(command_elements, command); + + POP_TOKEN(); + } + token = PEEK_TOKEN(); + // Eventual elements + while (is_first(*token, RULE_ELEMENT)) + { + // Get element + struct ast *element = parse_element(ctx); + if (element == NULL) + { + return err_s_com(command_elements, redirections, assignments); + } + + // Get element type + if (ast_is_word(element)) + { + // Extract word + struct ast_word *element_word = ast_get_word(element); + char *word = element_word->word; + element_word->word = NULL; // Prevents word to be freed + ast_free(&element); + command_elements = list_append(command_elements, word); + } + else if (ast_is_redir(element)) + { + // append redirections to the list of redirections + redirections = list_append(redirections, element); + } + else + { + perror("Internal error: unexpected return value from " + "parse_element in parse_simple_command"); + return err_s_com(command_elements, redirections, assignments); + } + + // Forward + token = PEEK_TOKEN(); + } + + struct ast *result = + ast_create_command(command_elements, redirections, assignments); + if (result == NULL) + { + return err_s_com(command_elements, redirections, assignments); + } + return result; +} + +struct ast *parse_element(struct lexer_context *ctx) +{ + struct token *token = PEEK_TOKEN(); + if (token->type == TOKEN_WORD || token->type == TOKEN_ASSIGNMENT_WORD) + { + POP_TOKEN(); + + struct ast *result = ast_create_word(token->data); + if (result == NULL) + { + perror("Internal error: could not create ast node (is your memory " + "full ?)"); + return NULL; + } + + return result; + } + else if (token->type == TOKEN_IONUMBER || is_token_redir(token)) + { + return parse_redirection(ctx); + } + else if (token->type == TOKEN_EXPORT) + { + return parse_export(ctx); + } + else + { + perror("Syntax error: unexpected token at parse_element"); + return NULL; + } +} + +struct ast *parse_shell_command(struct lexer_context *ctx) +{ + struct token *token = PEEK_TOKEN(); + struct ast *result = NULL; + + // '{' + if (token->type == TOKEN_LEFT_BRACKET) + { + POP_TOKEN(); + result = parse_compound_list(ctx); + if (result == NULL) + return NULL; + + // '}' + token = PEEK_TOKEN(); + if (token->type == TOKEN_LEFT_BRACKET) + { + ast_free(&result); + perror("Syntax error: bracket mismatch"); + return NULL; + } + + POP_TOKEN(); + return result; + } + // '(' + else if (token->type == TOKEN_LEFT_PAREN) + { + POP_TOKEN(); + result = parse_compound_list(ctx); + if (result == NULL) + return NULL; + + // ')' + token = PEEK_TOKEN(); + if (token->type == TOKEN_LEFT_PAREN) + { + ast_free(&result); + perror("Syntax error: parenthesis mismatch"); + return NULL; + } + POP_TOKEN(); + return ast_create_subshell(result); + } + else if (is_first(*token, RULE_IF)) + { + return parse_if_rule(ctx); + } + else if (is_first(*token, RULE_WHILE)) + { + return parse_while(ctx); + } + else if (is_first(*token, RULE_UNTIL)) + { + return parse_until(ctx); + } + // TODO for and case + else + { + perror("Syntax error: unexpected token in parse_shell_command"); + return NULL; + } +} + +struct ast *parse_if_rule(struct lexer_context *ctx) +{ + // If keyword + struct token *token = POP_TOKEN(); + if (token->type != TOKEN_IF) + { + perror("Internal error: expected a if rule but token has different " + "type"); + return NULL; + } + + // Condition content + struct ast *condition_content = parse_compound_list(ctx); + if (condition_content == NULL) + return NULL; + token = PEEK_TOKEN(); + + // Then keyword + if (token->type != TOKEN_THEN) + { + perror("Syntax error: Expected the 'then' keyword but token has " + "different type"); + return err_if_rule(&condition_content, NULL, NULL); + } + POP_TOKEN(); + + // Then content + struct ast *then_content = parse_compound_list(ctx); + if (then_content == NULL) + { + return err_if_rule(&condition_content, &then_content, NULL); + } + token = PEEK_TOKEN(); + + struct ast *else_content = NULL; + // Eventual else/elif clause(s) + if (is_first(*token, RULE_ELSE_CLAUSE)) + { + else_content = parse_else_clause(ctx); + if (else_content == NULL) + { + return err_if_rule(&condition_content, &then_content, NULL); + } + token = PEEK_TOKEN(); + } + + // Fi keyword + if (token->type != TOKEN_FI) + { + perror("Expected the 'fi' keyword but token has different type"); + return err_if_rule(&condition_content, &then_content, &else_content); + } + POP_TOKEN(); + + // Result + struct ast *result = + ast_create_if(condition_content, then_content, else_content); + if (result == NULL) + { + perror("Internal error: could not create a new AST (AST_IF)"); + return err_if_rule(&condition_content, &then_content, &else_content); + } + + return result; +} + +struct ast *parse_compound_list(struct lexer_context *ctx) +{ + struct list *result_list = NULL; // ast* list + struct ast *current_cmd = NULL; + struct token *token = PEEK_TOKEN(); + + // Skip newlines + while (token->type == TOKEN_NEWLINE) + { + POP_TOKEN(); + token = PEEK_TOKEN(); + } + + // And/or + current_cmd = parse_and_or(ctx); + if (current_cmd == NULL) + return NULL; + result_list = list_append(result_list, current_cmd); + token = PEEK_TOKEN(); + + // Following commands + while (token->type == TOKEN_SEMICOLON || token->type == TOKEN_NEWLINE) + { + POP_TOKEN(); + token = PEEK_TOKEN(); + + // Skip newlines + while (token->type == TOKEN_NEWLINE) + { + POP_TOKEN(); + token = PEEK_TOKEN(); + } + + // And/or + if (is_first(*token, RULE_AND_OR)) + { + current_cmd = parse_and_or(ctx); + if (current_cmd == NULL) + return NULL; + result_list = list_append(result_list, current_cmd); + token = PEEK_TOKEN(); + } + } + + // Eventual semicolon + if (token->type == TOKEN_SEMICOLON) + { + POP_TOKEN(); + token = PEEK_TOKEN(); + } + + // Skip newlines + while (token->type == TOKEN_NEWLINE) + { + POP_TOKEN(); + token = PEEK_TOKEN(); + } + + struct ast *result = ast_create_list(result_list); + return result; +} + +struct ast *parse_else_clause(struct lexer_context *ctx) +{ + struct token *token = PEEK_TOKEN(); + + // Eventual elif content + while (token->type == TOKEN_ELIF) + { + // Condition + token = POP_TOKEN(); + struct ast *condition = parse_compound_list(ctx); + + // Then keyword + token = POP_TOKEN(); + if (token->type != TOKEN_THEN) + { + perror("Expected the 'then' keyword but got a different token " + "type"); + return NULL; + } + + struct ast *then_content = parse_compound_list(ctx); + if (then_content == NULL) + { + ast_free(&condition); + return NULL; + } + token = PEEK_TOKEN(); + + // Eventual else clause (recursive) + struct ast *else_content = NULL; + if (token->type == TOKEN_ELSE || token->type == TOKEN_ELIF) + { + else_content = parse_else_clause(ctx); + if (else_content == NULL) + { + ast_free(&then_content); + ast_free(&condition); + return NULL; + } + } + else + { + else_content = ast_create_void(); + } + + return ast_create_if(condition, then_content, else_content); + } + + // Eventual else content + + struct ast *result = NULL; + + if (token->type == TOKEN_ELSE) + { + token = POP_TOKEN(); + result = parse_compound_list(ctx); + if (result == NULL) + return NULL; + } + else + { + result = ast_create_void(); + } + + return result; +} diff --git a/src/parser/grammar_basic.h b/src/parser/grammar_basic.h new file mode 100644 index 0000000..3ee5355 --- /dev/null +++ b/src/parser/grammar_basic.h @@ -0,0 +1,116 @@ +#ifndef GRAMMAR_BASIC_H +#define GRAMMAR_BASIC_H + +#include "../lexer/lexer.h" +#include "../utils/ast/ast.h" + +// === Functions + +/* + * @brief parses a list of [and_or] rules separated by semicolons and that + * ends by a newline + * + * @code list = and_or { ';' and_or } [ ';' ] ; + * + * @first first(and_or) + */ +struct ast *parse_list(struct lexer_context *ctx); + +/* + * @brief Only parses a pipeline rule for the moment + * + * @code and_or = pipeline { ( '&&' | '||' ) {'\n'} pipeline } ; + * + * @first first(pipeline) + */ +struct ast *parse_and_or(struct lexer_context *ctx); + +/* + * @brief Only parses a command rule for the moment + * + * @code pipeline = ['!'] command { '|' {'\n'} command } ; + * + * @first '!', first(command) + */ +struct ast *parse_pipeline(struct lexer_context *ctx); + +/* + * @brief Parses a simple command rule or a shell command rule depending on + * the first token. + * @note + * TOKEN_WORD => simple_command + * TOKEN_IF => shell_command + * + * @code command = simple_command + * | shell_command + * + * ; + * @first first(simple_command), first(shell_command) + */ +struct ast *parse_command(struct lexer_context *ctx); + +/* + * @brief Parses a simple list of words (command and arguments) + * ending by a separator + * + * @code simple_command = WORD { element } ; + * + * @first WORD + */ +struct ast *parse_simple_command(struct lexer_context *ctx); + +/* + * @brief Parses an element rule + * + * @code element = WORD + * | redirection + * ; + * + * @first WORD, first(redirection) + */ +struct ast *parse_element(struct lexer_context *ctx); + +/* + * @brief Only parses if rules for the moment + * + * @code shell_command = '{' compound_list '}' + * | '(' compound_list ')' + * | if_rule + * ; + * + * @first first(if_rule) + */ +struct ast *parse_shell_command(struct lexer_context *ctx); + +/* + * @brief Parses a if rule (condition, then-clause, elif-clause, else-clause) + * + * @code if_rule = 'if' compound_list 'then' compound_list [else_clause] 'fi' ; + * + * @first TOKEN_IF + */ +struct ast *parse_if_rule(struct lexer_context *ctx); + +/* + * @brief parses commands inside if/else clauses and returns the corresponding + * AST list + * + * @code compound_list = {'\n'} and_or { ( ';' | '\n' ) {'\n'} and_or } [';'] + * {'\n'} ; + * + * @first TOKEN_NEWLINE, first(and_or) + */ +struct ast *parse_compound_list(struct lexer_context *ctx); + +/* + * @brief parses an else clause rule (inside if) + * + * @code else_clause = 'else' compound_list + * | 'elif' compound_list 'then' compound_list [else_clause] + * ; + * + * @first TOKEN_ELSE, TOKEN_ELIF + */ +struct ast *parse_else_clause(struct lexer_context *ctx); + +#endif /* ! GRAMMAR_BASIC_H */ diff --git a/src/parser/parser.c b/src/parser/parser.c index e69de29..d1f0ebc 100644 --- a/src/parser/parser.c +++ b/src/parser/parser.c @@ -0,0 +1,67 @@ +#include "parser.h" + +#include + +#include "grammar.h" + +// === Static variables + +static enum parser_state state = PARSER_STATE_NOT_INITIALIZED; + +// === Functions + +int parser_init(void) +{ + if (state == PARSER_STATE_READY) + { + perror("Internal error: tried to initialize the parser module twice."); + return false; + } + int success = grammar_init(); + if (success == false) + return false; + + state = PARSER_STATE_READY; + return true; +} + +void parser_close(void) +{ + if (state != PARSER_STATE_READY) + { + perror("trying to close parser which was not opened"); + } + state = PARSER_STATE_CLOSED; + // TODO close grammar +} + +struct ast *get_ast(struct lexer_context *ctx) +{ + if (ctx == NULL) + { + perror("Internal error: called parser with no lexer context (NULL " + "pointer). Aborting."); + return NULL; + } + if (state == PARSER_STATE_NOT_INITIALIZED) + { + perror("Internal error: attempted to call parser without initializing " + "it. Aborting."); + return NULL; + } + if (state == PARSER_STATE_CLOSED) + { + perror("Internal error: attempted to call parser after closing it. " + "Aborting."); + return NULL; + } + + return parse_input(ctx); +} + +// TODO +struct ast *get_ast_str(char *command) +{ + (void)command; + return NULL; +} diff --git a/src/parser/parser.h b/src/parser/parser.h index e69de29..dc01a5e 100644 --- a/src/parser/parser.h +++ b/src/parser/parser.h @@ -0,0 +1,45 @@ +#ifndef PARSER_H +#define PARSER_H + +#include + +#include "../lexer/lexer.h" +#include "../utils/ast/ast.h" + +enum parser_state +{ + PARSER_STATE_NOT_INITIALIZED = 0, + PARSER_STATE_READY, + PARSER_STATE_CLOSED +}; + +/* @brief Initializes the parser module + * @warning parser needs to be closed after use with parser_close() + * + * @return Returns false on error and true on success + */ +int parser_init(void); + +/* @brief Closes the parser module after use + */ +void parser_close(void); + +/* @brief Builds the AST representation of the next command to execute. + * + * @return Returns the AST representation of the next command to execute. + * If there is no command left to execute, retuns an AST_END node. + * + * @warning NOT IMPLEMENTED + */ +struct ast *get_ast(struct lexer_context *ctx); + +/* @brief Builds the AST representation of the given command string. + * + * @return Returns the AST representation of the given command string. + * Returns an AST_END node if the given command is empty. + * + * @warning NOT IMPLEMENTED + */ +struct ast *get_ast_str(char *command); + +#endif /* ! PARSER_H */ diff --git a/src/utils/Makefile.am b/src/utils/Makefile.am new file mode 100644 index 0000000..e40612e --- /dev/null +++ b/src/utils/Makefile.am @@ -0,0 +1,30 @@ +lib_LIBRARIES = libutils.a + +libutils_a_SOURCES = \ + lists/lists1.c \ + lists/lists2.c \ + lists/lists3.c \ + hash_map/hash_map.c \ + string_utils/string_utils.c \ + ast/ast.c \ + ast/ast_if.c \ + ast/ast_command.c \ + ast/ast_list.c \ + ast/ast_and_or.c \ + ast/ast_redir.c \ + ast/ast_void.c \ + ast/ast_end.c \ + ast/ast_word.c \ + ast/ast_neg.c \ + ast/ast_pipe.c \ + ast/ast_loop.c \ + args/args.c \ + vars/vars.c \ + main_loop/main_loop.c \ + ast/ast_assignment.c \ + ast/ast_subshell.c \ + ast/ast_function.c + +libutils_a_CPPFLAGS = -I$(top_srcdir)/src + +noinst_LIBRARIES = libutils.a diff --git a/src/utils/args/args.c b/src/utils/args/args.c new file mode 100644 index 0000000..69374e2 --- /dev/null +++ b/src/utils/args/args.c @@ -0,0 +1,155 @@ +#define _POSIX_C_SOURCE 200809L +#include "./args.h" + +#include +#include +#include +#include + +#include "../lists/lists.h" +#include "../string_utils/string_utils.h" +#include "../vars/vars.h" + +static void strlen_acc(void *acc, void *data) +{ + size_t *len = (size_t *)acc; + char *str = (char *)data; + *len += strlen(str); +} + +static char *concat_list_str(struct list *list) +{ + char *res = NULL; + size_t total_len = list_length(list) + 1; // + spaces + null terminator + + list_fold(list, &total_len, strlen_acc); + + res = malloc(total_len); + if (res != NULL) + { + res[0] = 0; + for (struct list *it = list; it != NULL; it = it->next) + { + strcat(res, (char *)it->data); + if (it->next != NULL) + strcat(res, " "); + } + res[total_len - 1] = 0; + } + + return res; +} + +static void args_in_var(struct hash_map *vars, struct list *args_list) +{ + int arg_index = 1; + char index_str[11]; + + for (struct list *it = args_list; it != NULL; it = it->next) + { + int_to_str(arg_index++, index_str); + set_var_copy(vars, index_str, (char *)it->data); + } + + char *concated_args = concat_list_str(args_list); + set_var_copy(vars, "*", concated_args); + + char *key = strdup("@"); + set_var(vars, key, concated_args, NULL); + // key and concated_args consumed by hash_map + + int_to_str(arg_index - 1, index_str); + set_var_copy(vars, "#", index_str); +} + +int args_handler(int argc, char **argv, struct args_options *options, + struct hash_map *vars) +{ + options->type = INPUT_UNDEFINED; + options->input_source = NULL; + // options->pretty_print = false; + options->verbose = false; + + struct list *args_list = NULL; + + set_var_copy(vars, "0", argv[0]); + + for (int i = 1; i < argc; i++) + { + /* if (strcmp(argv[i], "--pretty-print") == 0) + { + options->pretty_print = true; + } */ + if (strcmp(argv[i], "--verbose") == 0) + { + options->verbose = true; + } + else if (strcmp(argv[i], "-c") == 0) + { + if (options->type != INPUT_UNDEFINED) + { + fprintf(stderr, "Multiple input sources specified: %s\n", + argv[i + 1]); + return 1; + } + else if (i + 1 >= argc) + { + fprintf(stderr, "No command provided after -c\n"); + return 1; + } + + options->type = INPUT_CMD; + options->input_source = argv[i + 1]; + i++; + } + else if (argv[i][0] == '-') + { + fprintf(stderr, "Unknown option: %s\n", argv[i]); + return 1; + } + else if (options->type == INPUT_UNDEFINED) + { + options->type = INPUT_FILE; + options->input_source = argv[i]; + } + else + { + // All remaining arguments are treated as additional arguments + args_list = list_append(args_list, argv[i]); + continue; + } + } + + args_in_var(vars, args_list); + list_destroy(&args_list); + + if (options->type == INPUT_UNDEFINED) + options->type = INPUT_STDIN; + + return 0; +} + +void args_print(struct args_options *options) +{ + printf("Input type: %s\n", + options->type == INPUT_CMD ? "COMMAND" + : options->type == INPUT_FILE ? "FILE" + : options->type == INPUT_STDIN ? "STDIN" + : "UNDEFINED"); + printf("Input source: %s\n", + options->input_source ? options->input_source : "NULL"); + // printf("Pretty print: %s\n", options->pretty_print ? "true" : "false"); + printf("Verbose: %s\n", options->verbose ? "true" : "false"); +} + +void print_usage(FILE *std, const char *program_name) +{ + fprintf(std, "Usage: %s [OPTIONS] [SCRIPT] [ARGUMENTS...]\n", program_name); + fprintf(std, "Options:\n"); + fprintf(std, " -c [SCRIPT] Execute the given command string.\n"); + // fprintf(std, " --pretty-print Enable pretty printing of + // outputs.\n"); + fprintf(std, " --verbose Enable verbose mode.\n"); + fprintf(std, + "If no SCRIPT is provided, input is read from standard input.\n"); +} diff --git a/src/utils/args/args.h b/src/utils/args/args.h new file mode 100644 index 0000000..3c02fc4 --- /dev/null +++ b/src/utils/args/args.h @@ -0,0 +1,52 @@ +#ifndef ARGS_H +#define ARGS_H + +#include +#include + +#include "../hash_map/hash_map.h" + +enum input_type +{ + INPUT_UNDEFINED, + INPUT_FILE, + INPUT_CMD, + INPUT_STDIN +}; + +struct args_options +{ + /** Source of the input, filename or command string depending on type, NULL + * if INPUT_STDIN */ + const char *input_source; + /** Type of the input source */ + enum input_type type; + /** Enable or disable pretty printing of outputs */ + // bool pretty_print; + /** Enable or disable verbose mode */ + bool verbose; +}; + +/** + * Handles command-line arguments and populates the args_options structure. + * @param argc The argument count. + * @param argv The argument vector. + * @param options Pointer to args_options structure to be populated. + * @param vars Pointer to the variables hash map. + * @return 0 on success, non-zero on failure. + */ +int args_handler(int argc, char **argv, struct args_options *options, + struct hash_map *vars); + +/** Prints the parsed arguments for debugging purposes. + * @param options Pointer to args_options structure containing parsed options. + */ +void args_print(struct args_options *options); + +/** Prints the usage information for the program. + * @param std The output stream to print to (e.g., stdout or stderr). + * @param program_name The name of the program. + */ +void print_usage(FILE *std, const char *program_name); + +#endif /* ARGS_H */ diff --git a/src/utils/ast/ast.c b/src/utils/ast/ast.c new file mode 100644 index 0000000..d16a9cb --- /dev/null +++ b/src/utils/ast/ast.c @@ -0,0 +1,174 @@ + +#define _POSIX_C_SOURCE 200809L +#include "ast.h" + +#include +#include +#include +#include + +void ast_free(struct ast **node) +{ + if (node == NULL || *node == NULL) + { + fprintf(stderr, + "WARNING: Internal error: failed to free AST node (NULL argument)"); + return; + } + + switch ((*node)->type) + { + case AST_IF: + ast_free_if(ast_get_if(*node)); + break; + case AST_CMD: + ast_free_command(ast_get_command(*node)); + break; + case AST_LIST: + ast_free_list(ast_get_list(*node)); + break; + case AST_AND_OR: + ast_free_and_or(ast_get_and_or(*node)); + break; + case AST_REDIR: + ast_free_redir(ast_get_redir(*node)); + break; + case AST_PIPE: + ast_free_pipe(ast_get_pipe(*node)); + break; + case AST_WORD: + ast_free_word(ast_get_word(*node)); + break; + case AST_ASSIGNMENT: + ast_free_assignment(ast_get_assignment(*node)); + break; + case AST_NEG: + ast_free_neg(ast_get_neg(*node)); + break; + case AST_LOOP: + ast_free_loop(ast_get_loop(*node)); + break; + case AST_FUNCTION: + ast_free_function(ast_get_function(*node)); + break; + case AST_SUBSHELL: + ast_free_subshell(ast_get_subshell(*node)); + break; + case AST_VOID: + case AST_END: + break; + + default: + fprintf(stderr, "WARNING: Internal error:" + " failed to free an AST node (Unknown type)"); + return; + } + + free(*node); + *node = NULL; +} + +struct ast *ast_create(enum ast_type type, void *data) +{ + struct ast *node = malloc(sizeof(struct ast)); + if (!node) + return NULL; + + node->type = type; + node->data = data; + + return node; +} + +/* // TODO handle new types (AST_WORD, AST_PIPE, etc.) +static void ast_print_dot_recursive(struct ast *node, FILE *out) +{ + if (!node) + return; + + switch (node->type) + { + case AST_LIST: { + struct ast_list *ast_list = ast_get_list(node); + fprintf(out, " node%p [label=\"LIST\"];\n", (void *)node); + + struct list *elt = ast_list->children; + while (elt != NULL) + { + struct ast *child = (struct ast *)elt->data; + fprintf(out, " node%p -> node%p;\n", (void *)node, (void *)child); + ast_print_dot_recursive(child, out); + elt = elt->next; + } + break; + } + case AST_IF: { + struct ast_if *if_data = ast_get_if(node); + fprintf(out, " node%p [label=\"IF\"];\n", (void *)node); + + if (if_data->condition) + { + fprintf(out, " node%p -> node%p;\n", (void *)node, + (void *)if_data->condition); + fprintf(out, + " node%p [fillcolor=\"lightyellow\", style=\"filled\"];\n", + (void *)if_data->condition); + ast_print_dot_recursive(if_data->condition, out); + + if (if_data->then_clause) + { + fprintf(out, " node%p -> node%p [label=\"true\"];\n", + (void *)if_data->condition, + (void *)if_data->then_clause); + ast_print_dot_recursive(if_data->then_clause, out); + } + + if (if_data->else_clause) + { + fprintf(out, " node%p -> node%p [label=\"false\"];\n", + (void *)if_data->condition, + (void *)if_data->else_clause); + ast_print_dot_recursive(if_data->else_clause, out); + } + } + break; + } + case AST_CMD: { + struct ast_command *command_data = ast_get_command(node); + fprintf(out, " node%p [label=\"", (void *)node); + struct list *l = command_data->command; + while (l) + { + fprintf(out, "%s", (char *)l->data); + if (l->next) + fprintf(out, " "); + l = l->next; + } + fprintf(out, "\"];\n"); + break; + } + case AST_END: + fprintf(out, " node%p [label=\"END\"];\n", (void *)node); + break; + default: + break; + } +} + +void ast_print_dot(struct ast *ast) +{ + if (!ast) + return; + + FILE *dot_pipe = popen("dot -Tsvg -o ast.svg", "w"); + if (!dot_pipe) + { + return; + } + + fprintf(dot_pipe, "digraph AST {\n"); + ast_print_dot_recursive(ast, dot_pipe); + fprintf(dot_pipe, "}\n"); + pclose(dot_pipe); +} + */ diff --git a/src/utils/ast/ast.h b/src/utils/ast/ast.h new file mode 100644 index 0000000..2eac62f --- /dev/null +++ b/src/utils/ast/ast.h @@ -0,0 +1,20 @@ +#ifndef AST_H +#define AST_H + +#include "ast_and_or.h" +#include "ast_assignment.h" +#include "ast_base.h" +#include "ast_command.h" +#include "ast_end.h" +#include "ast_function.h" +#include "ast_if.h" +#include "ast_list.h" +#include "ast_loop.h" +#include "ast_neg.h" +#include "ast_pipe.h" +#include "ast_redir.h" +#include "ast_void.h" +#include "ast_word.h" +#include "ast_subshell.h" + +#endif /* ! AST_H */ diff --git a/src/utils/ast/ast_and_or.c b/src/utils/ast/ast_and_or.c new file mode 100644 index 0000000..9dea5dd --- /dev/null +++ b/src/utils/ast/ast_and_or.c @@ -0,0 +1,37 @@ +#include "ast_and_or.h" + +#include + +bool ast_is_and_or(struct ast *node) +{ + return node != NULL && node->type == AST_AND_OR; +} + +struct ast_and_or *ast_get_and_or(struct ast *node) +{ + if (ast_is_and_or(node)) + return (struct ast_and_or *)node->data; + return NULL; +} + +struct ast *ast_create_and_or(struct ast *left, struct ast *right, + enum ast_and_or_type type) +{ + struct ast_and_or *and_or = malloc(sizeof(struct ast_and_or)); + if (!and_or) + return NULL; + and_or->left = left; + and_or->right = right; + and_or->type = type; + + return ast_create(AST_AND_OR, and_or); +} + +void ast_free_and_or(struct ast_and_or *and_or) +{ + if (!and_or) + return; + ast_free(&and_or->left); + ast_free(&and_or->right); + free(and_or); +} diff --git a/src/utils/ast/ast_and_or.h b/src/utils/ast/ast_and_or.h new file mode 100644 index 0000000..4813592 --- /dev/null +++ b/src/utils/ast/ast_and_or.h @@ -0,0 +1,26 @@ +#ifndef AST_AND_OR_H +#define AST_AND_OR_H + +#include "ast_base.h" + +enum ast_and_or_type +{ + AST_AND_OR_NULL, + AST_AND_OR_TYPE_AND, + AST_AND_OR_TYPE_OR +}; + +struct ast_and_or +{ + struct ast *left; + struct ast *right; + enum ast_and_or_type type; +}; + +bool ast_is_and_or(struct ast *node); +struct ast_and_or *ast_get_and_or(struct ast *node); +struct ast *ast_create_and_or(struct ast *left, struct ast *right, + enum ast_and_or_type type); +void ast_free_and_or(struct ast_and_or *and_or); + +#endif /* ! AST_AND_OR_H */ diff --git a/src/utils/ast/ast_assignment.c b/src/utils/ast/ast_assignment.c new file mode 100644 index 0000000..72cfdb6 --- /dev/null +++ b/src/utils/ast/ast_assignment.c @@ -0,0 +1,56 @@ +#define _POSIX_C_SOURCE 200809L + +#include "ast_assignment.h" + +#include +#include + +bool ast_is_assignment(struct ast *node) +{ + return node != NULL && node->type == AST_ASSIGNMENT; +} + +struct ast_assignment *ast_get_assignment(struct ast *node) +{ + if (node == NULL || node->type != AST_ASSIGNMENT) + return NULL; + return (struct ast_assignment *)node->data; +} + +/* @brief: splits the assignement 'name=value' into 2 parts, + * and fills the fields of ast_assignment with it. + */ +static void init_assignments(struct ast_assignment *ast_assignment, + char *assignment) +{ + if (assignment == NULL) + return; + char *split_pos = strchr(assignment, '='); + if (split_pos == NULL) + return; + + *split_pos = '\0'; + ast_assignment->name = strdup(assignment); + ast_assignment->value = strdup(split_pos + 1); +} + +struct ast *ast_create_assignment(char *assignment, bool global) +{ + struct ast_assignment *assignment_data = + calloc(1, sizeof(struct ast_assignment)); + if (!assignment_data) + return NULL; + + init_assignments(assignment_data, assignment); + assignment_data->global = global; + return ast_create(AST_ASSIGNMENT, assignment_data); +} + +void ast_free_assignment(struct ast_assignment *assignment_data) +{ + if (assignment_data == NULL) + return; + free(assignment_data->name); + free(assignment_data->value); + free(assignment_data); +} diff --git a/src/utils/ast/ast_assignment.h b/src/utils/ast/ast_assignment.h new file mode 100644 index 0000000..3ebac14 --- /dev/null +++ b/src/utils/ast/ast_assignment.h @@ -0,0 +1,18 @@ +#ifndef AST_ASSIGNMENT_H +#define AST_ASSIGNMENT_H + +#include "ast_base.h" + +struct ast_assignment +{ + char *name; + char *value; + bool global; +}; + +bool ast_is_assignment(struct ast *node); +struct ast_assignment *ast_get_assignment(struct ast *node); +struct ast *ast_create_assignment(char *assignment, bool global); +void ast_free_assignment(struct ast_assignment *assignment_data); + +#endif /* ! AST_ASSIGNMENT_H */ diff --git a/src/utils/ast/ast_base.h b/src/utils/ast/ast_base.h new file mode 100644 index 0000000..de7dcfa --- /dev/null +++ b/src/utils/ast/ast_base.h @@ -0,0 +1,52 @@ +#ifndef AST_BASE_H +#define AST_BASE_H + +#include +#include + +enum ast_type +{ + AST_END, + AST_LIST, + AST_IF, + AST_AND_OR, + AST_REDIR, + AST_VOID, + AST_CMD, + AST_WORD, + AST_PIPE, + AST_NEG, + AST_LOOP, + AST_ASSIGNMENT, + AST_FUNCTION, + AST_SUBSHELL +}; + +struct ast +{ + enum ast_type type; + + /** + * Data associated with this AST node. It can be one of the following: + * - NULL (AST_END) + * - struct ast_if* (AST_IF) + * - struct ast_command* (AST_CMD) + * - struct ast_and_or* (AST_AND_OR) + * - struct ast_redir* (AST_REDIR) + * - and a lot more now... + */ + void *data; +}; + +/* @brief: returns an ast* with corresponding data and type. + * + * @note: this function should only be called by ast_create_[TYPE] functions. + */ +struct ast *ast_create(enum ast_type type, void *data); + +/* @brief: frees the given ast. If ast is NULL, does nothing. + * + */ +void ast_free(struct ast **node); + +#endif /* ! AST_BASE_H */ diff --git a/src/utils/ast/ast_command.c b/src/utils/ast/ast_command.c new file mode 100644 index 0000000..81dd10e --- /dev/null +++ b/src/utils/ast/ast_command.c @@ -0,0 +1,43 @@ +#include "ast_command.h" + +#include +#include + +#include "../lists/lists.h" +#include "ast_list.h" + +struct ast *ast_create_command(struct list *command, struct list *redirections, + struct list *assignments) +{ + struct ast_command *command_data = malloc(sizeof(struct ast_command)); + if (!command_data) + return NULL; + + command_data->command = command; + command_data->redirections = redirections; + command_data->assignments = assignments; + + return ast_create(AST_CMD, command_data); +} + +struct ast_command *ast_get_command(struct ast *node) +{ + if (node == NULL || node->type != AST_CMD) + return NULL; + return (struct ast_command *)node->data; +} + +bool ast_is_command(struct ast *node) +{ + return node != NULL && node->type == AST_CMD; +} + +void ast_free_command(struct ast_command *command_data) +{ + if (command_data == NULL) + return; + list_deep_destroy(command_data->command); + ast_list_deep_destroy(command_data->redirections); + ast_list_deep_destroy(command_data->assignments); + free(command_data); +} diff --git a/src/utils/ast/ast_command.h b/src/utils/ast/ast_command.h new file mode 100644 index 0000000..7b24a2d --- /dev/null +++ b/src/utils/ast/ast_command.h @@ -0,0 +1,36 @@ +#ifndef AST_COMMAND_H +#define AST_COMMAND_H + +#include "../lists/lists.h" +#include "ast_base.h" + +struct ast_command +{ + struct list *command; // A list of words (char*) + struct list *redirections; // A list of ASTs, all ast_redir + struct list *assignments; // A list of ASTs, all ast_assignment +}; + +/** + * Checks if the given AST node is a command. + */ +bool ast_is_command(struct ast *node); + +/** + * Retrieves the command data from the given AST node. + * Assumes that the node is of type AST_CMD. + */ +struct ast_command *ast_get_command(struct ast *node); + +/** + * Creates a new AST node representing a command. + */ +struct ast *ast_create_command(struct list *command, struct list *redirections, + struct list *assignments); + +/* + * @brief: frees the given ast_command and sets the pointer to NULL. + */ +void ast_free_command(struct ast_command *command_data); + +#endif /* ! AST_COMMAND_H */ diff --git a/src/utils/ast/ast_end.c b/src/utils/ast/ast_end.c new file mode 100644 index 0000000..0d314bc --- /dev/null +++ b/src/utils/ast/ast_end.c @@ -0,0 +1,14 @@ +#include "ast_end.h" + +#include +#include + +bool ast_is_end(struct ast *node) +{ + return node != NULL && node->type == AST_END; +} + +struct ast *ast_create_end(void) +{ + return ast_create(AST_END, NULL); +} diff --git a/src/utils/ast/ast_end.h b/src/utils/ast/ast_end.h new file mode 100644 index 0000000..55b5322 --- /dev/null +++ b/src/utils/ast/ast_end.h @@ -0,0 +1,18 @@ +#ifndef AST_END_H +#define AST_END_H + +#include "../lists/lists.h" +#include "ast_base.h" + +/** + * Checks if the given AST node is of type AST_END. + */ +bool ast_is_end(struct ast *node); + +/** + * Creates a new AST node representing the end of input. + * WARNING: data will be a NULL pointer + */ +struct ast *ast_create_end(void); + +#endif /* ! AST_END_H */ diff --git a/src/utils/ast/ast_function.c b/src/utils/ast/ast_function.c new file mode 100644 index 0000000..62b5016 --- /dev/null +++ b/src/utils/ast/ast_function.c @@ -0,0 +1,42 @@ +#include "ast_function.h" + +#include +#include + +#include "ast_base.h" + +bool ast_is_function(struct ast *node) +{ + return node != NULL && node->type == AST_FUNCTION; +} + +struct ast_function *ast_get_function(struct ast *node) +{ + if (!ast_is_function(node)) + return NULL; + return (struct ast_function *)node->data; +} + +struct ast *ast_create_function(char *name, struct ast *value) +{ + struct ast_function *function_data = malloc(sizeof(struct ast_function)); + if (!function_data) + return NULL; + + function_data->name = name; + function_data->value = value; + + return ast_create(AST_FUNCTION, function_data); +} + +void ast_free_function(struct ast_function *function_data) +{ + if (function_data) + { + free(function_data->name); + // WARNING: this ast will be stored in the function hashmap. + // thus, it will be freed from the hashmap. + // ast_free(&function_data->value); + free(function_data); + } +} diff --git a/src/utils/ast/ast_function.h b/src/utils/ast/ast_function.h new file mode 100644 index 0000000..3bbc551 --- /dev/null +++ b/src/utils/ast/ast_function.h @@ -0,0 +1,33 @@ +#ifndef AST_FUNCTION_H +#define AST_FUNCTION_H + +#include + +struct ast_function +{ + char *name; + struct ast *value; +}; + +/** + * @brief: Checks if the given AST node is an ast_function + */ +bool ast_is_function(struct ast *node); + +/** + * @brief: Retrieves the function data from the given AST node. + * Assumes that the node is of type AST_function. + */ +struct ast_function *ast_get_function(struct ast *node); + +/** + * @brief: Creates a new AST node representing an AST_function + * @warning: name must be already allocated. + */ +struct ast *ast_create_function(char *name, struct ast *value); +/* + * @brief: frees the given ast_function and sets the pointer to NULL. + */ +void ast_free_function(struct ast_function *function_data); + +#endif /* AST_FUNCTION_H */ diff --git a/src/utils/ast/ast_if.c b/src/utils/ast/ast_if.c new file mode 100644 index 0000000..6b0ff5d --- /dev/null +++ b/src/utils/ast/ast_if.c @@ -0,0 +1,42 @@ +#include "ast_if.h" + +#include +#include + +struct ast *ast_create_if(struct ast *condition, struct ast *then_clause, + struct ast *else_clause) +{ + struct ast_if *if_data = malloc(sizeof(struct ast_if)); + if (!if_data) + return NULL; + + if_data->condition = condition; + if_data->then_clause = then_clause; + if_data->else_clause = else_clause; + + return ast_create(AST_IF, if_data); +} + +struct ast_if *ast_get_if(struct ast *node) +{ + if (node == NULL || node->type != AST_IF) + return NULL; + return node->data; +} + +bool ast_is_if(struct ast *node) +{ + return node != NULL && node->type == AST_IF; +} + +void ast_free_if(struct ast_if *if_data) +{ + if (if_data == NULL) + return; + + ast_free(&if_data->condition); + ast_free(&if_data->then_clause); + ast_free(&if_data->else_clause); + + free(if_data); +} diff --git a/src/utils/ast/ast_if.h b/src/utils/ast/ast_if.h new file mode 100644 index 0000000..f1842bd --- /dev/null +++ b/src/utils/ast/ast_if.h @@ -0,0 +1,34 @@ +#ifndef AST_IF_H +#define AST_IF_H + +#include "ast_base.h" + +struct ast_if +{ + struct ast *condition; + struct ast *then_clause; + struct ast *else_clause; +}; + +/** + * Checks if the given AST node is an if statement. + */ +bool ast_is_if(struct ast *node); + +/** + * Retrieves the if statement data from the given AST node. + * Assumes that the node is of type AST_IF. + */ +struct ast_if *ast_get_if(struct ast *node); + +/** + * Creates a new AST node representing an if statement. + */ +struct ast *ast_create_if(struct ast *condition, struct ast *then_clause, + struct ast *else_clause); +/* + * @brief: frees the given ast_if and sets the pointer to NULL. + */ +void ast_free_if(struct ast_if *if_data); + +#endif /* ! AST_IF_H */ diff --git a/src/utils/ast/ast_list.c b/src/utils/ast/ast_list.c new file mode 100644 index 0000000..cb4aaa6 --- /dev/null +++ b/src/utils/ast/ast_list.c @@ -0,0 +1,48 @@ +#include "ast_list.h" + +struct ast *ast_create_list(struct list *list) +{ + struct ast_list *ast_list = malloc(sizeof(struct ast_list)); + if (ast_list == NULL) + return NULL; + + ast_list->children = list; + + return ast_create(AST_LIST, ast_list); +} + +struct ast_list *ast_get_list(struct ast *node) +{ + if (node == NULL) + return NULL; + return node->data; +} + +bool ast_is_list(struct ast *node) +{ + return node != NULL && node->type == AST_LIST; +} + +void ast_free_list(struct ast_list *ast_list) +{ + if (ast_list == NULL) + return; + + ast_list_deep_destroy(ast_list->children); + free(ast_list); +} + +void ast_list_deep_destroy(struct list *l) +{ + struct list *elt = l; + struct list *next_elt; + while (elt != NULL) + { + next_elt = elt->next; + + struct ast *node = (struct ast *)elt->data; + ast_free(&node); + free(elt); + elt = next_elt; + } +} diff --git a/src/utils/ast/ast_list.h b/src/utils/ast/ast_list.h new file mode 100644 index 0000000..21b24fb --- /dev/null +++ b/src/utils/ast/ast_list.h @@ -0,0 +1,43 @@ +#ifndef AST_LIST_H +#define AST_LIST_H + +#include "../lists/lists.h" +#include "ast_base.h" + +struct ast_list +{ + struct list *children; // A list of ASTs (ast*) +}; + +/* +** Release the memory used by the AST list and its AST children +** Does nothing if `list` is `NULL`. +* +* @warning: this function should NEVER be used on a list containing +* anything else than ASTs. +*/ +void ast_list_deep_destroy(struct list *l); + +/** + * Creates a new AST node representing a list of ASTs + */ +struct ast *ast_create_list(struct list *ast_list); + +/** + * Retrieves the command data from the given AST node. + * Assumes that the node is of type AST_LIST. + */ +struct ast_list *ast_get_list(struct ast *node); + +/** + * Checks if the given AST node is a command. + */ +bool ast_is_list(struct ast *node); + +/* @brief: frees the given ast list. + * + * @warning: should only be called by ast_free() function. + */ +void ast_free_list(struct ast_list *ast_list); + +#endif /* ! AST_LIST_H */ diff --git a/src/utils/ast/ast_loop.c b/src/utils/ast/ast_loop.c new file mode 100644 index 0000000..fded922 --- /dev/null +++ b/src/utils/ast/ast_loop.c @@ -0,0 +1,37 @@ +#include "ast_loop.h" + +#include +#include + +struct ast *ast_create_loop(struct ast *condition, struct ast *body) +{ + struct ast_loop *node_data = malloc(sizeof(struct ast_loop)); + if (!node_data) + return NULL; + + node_data->condition = condition; + node_data->body = body; + + return ast_create(AST_LOOP, node_data); +} + +struct ast_loop *ast_get_loop(struct ast *node) +{ + if (node == NULL || node->type != AST_LOOP) + return NULL; + return (struct ast_loop *)node->data; +} + +bool ast_is_loop(struct ast *node) +{ + return node != NULL && node->type == AST_LOOP; +} + +void ast_free_loop(struct ast_loop *loop_data) +{ + if (loop_data == NULL) + return; + ast_free(&loop_data->condition); + ast_free(&loop_data->body); + free(loop_data); +} diff --git a/src/utils/ast/ast_loop.h b/src/utils/ast/ast_loop.h new file mode 100644 index 0000000..9718db1 --- /dev/null +++ b/src/utils/ast/ast_loop.h @@ -0,0 +1,34 @@ +#ifndef AST_LOOP_H +#define AST_LOOP_H + +#include "ast_base.h" + +struct ast_loop +{ + // Repeat body while condition is true + struct ast *condition; + struct ast *body; +}; + +/** + * Checks if the given AST node is a loop. + */ +bool ast_is_loop(struct ast *node); + +/** + * Retrieves the loop data from the given AST node. + * Assumes that the node is of type AST_LOOP. + */ +struct ast_loop *ast_get_loop(struct ast *node); + +/** + * Creates a new AST node representing a loop. + */ +struct ast *ast_create_loop(struct ast *condition, struct ast *body); + +/* + * @brief: frees the given ast_loop and sets the pointer to NULL. + */ +void ast_free_loop(struct ast_loop *loop_node); + +#endif /* ! AST_LOOP_H */ diff --git a/src/utils/ast/ast_neg.c b/src/utils/ast/ast_neg.c new file mode 100644 index 0000000..80dc4f3 --- /dev/null +++ b/src/utils/ast/ast_neg.c @@ -0,0 +1,34 @@ +#include "ast_neg.h" + +#include + +bool ast_is_neg(struct ast *node) +{ + return node != NULL && node->type == AST_NEG; +} + +struct ast_neg *ast_get_neg(struct ast *node) +{ + if (ast_is_neg(node)) + return node->data; + return NULL; +} + +struct ast *ast_create_neg(bool negation, struct ast *child) +{ + struct ast_neg *node = malloc(sizeof(struct ast_neg)); + if (!node) + return NULL; + + node->negation = negation; + node->child = child; + return ast_create(AST_NEG, node); +} + +void ast_free_neg(struct ast_neg *node) +{ + if (!node) + return; + ast_free(&node->child); + free(node); +} diff --git a/src/utils/ast/ast_neg.h b/src/utils/ast/ast_neg.h new file mode 100644 index 0000000..738c246 --- /dev/null +++ b/src/utils/ast/ast_neg.h @@ -0,0 +1,17 @@ +#ifndef AST_NEG_H +#define AST_NEG_H + +#include "ast_base.h" + +struct ast_neg +{ + bool negation; // True negates the child's output + struct ast *child; +}; + +bool ast_is_neg(struct ast *node); +struct ast_neg *ast_get_neg(struct ast *node); +struct ast *ast_create_neg(bool negation, struct ast *child); +void ast_free_neg(struct ast_neg *node); + +#endif /* ! AST_NEG_H */ diff --git a/src/utils/ast/ast_pipe.c b/src/utils/ast/ast_pipe.c new file mode 100644 index 0000000..92754cf --- /dev/null +++ b/src/utils/ast/ast_pipe.c @@ -0,0 +1,35 @@ +#include "ast_pipe.h" + +#include + +bool ast_is_pipe(struct ast *node) +{ + return node != NULL && node->type == AST_REDIR; +} + +struct ast_pipe *ast_get_pipe(struct ast *node) +{ + if (ast_is_pipe(node)) + return node->data; + return NULL; +} + +struct ast *ast_create_pipe(struct ast *left, struct ast *right) +{ + struct ast_pipe *node = malloc(sizeof(struct ast_pipe)); + if (!node) + return NULL; + node->left = left; + node->right = right; + + return ast_create(AST_PIPE, node); +} + +void ast_free_pipe(struct ast_pipe *node) +{ + if (!node) + return; + ast_free(&node->left); + ast_free(&node->right); + free(node); +} diff --git a/src/utils/ast/ast_pipe.h b/src/utils/ast/ast_pipe.h new file mode 100644 index 0000000..930cb2c --- /dev/null +++ b/src/utils/ast/ast_pipe.h @@ -0,0 +1,18 @@ +#ifndef AST_PIPE_H +#define AST_PIPE_H + +#include "ast_base.h" + +struct ast_pipe +{ + struct ast *left; + struct ast *right; + // Output of left will be redirected to right stdin +}; + +bool ast_is_pipe(struct ast *node); +struct ast_pipe *ast_get_pipe(struct ast *node); +struct ast *ast_create_pipe(struct ast *left, struct ast *right); +void ast_free_pipe(struct ast_pipe *node); + +#endif /* ! AST_PIPE_H */ diff --git a/src/utils/ast/ast_redir.c b/src/utils/ast/ast_redir.c new file mode 100644 index 0000000..d1dcedb --- /dev/null +++ b/src/utils/ast/ast_redir.c @@ -0,0 +1,38 @@ +#include "ast_redir.h" + +#include + +bool ast_is_redir(struct ast *node) +{ + return node != NULL && node->type == AST_REDIR; +} + +struct ast_redir *ast_get_redir(struct ast *node) +{ + if (ast_is_redir(node)) + return (struct ast_redir *)node->data; + return NULL; +} + +struct ast *ast_create_redir(char *filename, int io_number, + enum ast_redir_type type) +{ + struct ast_redir *redir = malloc(sizeof(struct ast_redir)); + if (!redir) + return NULL; + redir->filename = + filename; // Takes ownership? Usually yes in simple ASTs, or dup. Let's + // assume pointer copy for now, but user must manage memory. + redir->io_number = io_number; + redir->type = type; + + return ast_create(AST_REDIR, redir); +} + +void ast_free_redir(struct ast_redir *redir) +{ + if (!redir) + return; + free(redir->filename); + free(redir); +} diff --git a/src/utils/ast/ast_redir.h b/src/utils/ast/ast_redir.h new file mode 100644 index 0000000..9d9a9d3 --- /dev/null +++ b/src/utils/ast/ast_redir.h @@ -0,0 +1,33 @@ +#ifndef AST_REDIR_H +#define AST_REDIR_H + +#include "ast_base.h" + +enum ast_redir_type +{ + AST_REDIR_TYPE_NULL, + AST_REDIR_TYPE_LESS, // < + AST_REDIR_TYPE_GREAT, // > + AST_REDIR_TYPE_LESSGREAT, // <> + AST_REDIR_TYPE_DGREAT, // >> + AST_REDIR_TYPE_LESSAND, // <& + AST_REDIR_TYPE_GREATAND, // >& + AST_REDIR_TYPE_CLOBBER // >| +}; + +struct ast_redir +{ + char *filename; + int io_number; // The FD being redirected (default -1 if not specified, + // implies 0 or 1 based on type) + enum ast_redir_type type; + int saved_fd; // To store the original FD for restoration (-1 before save) +}; + +bool ast_is_redir(struct ast *node); +struct ast_redir *ast_get_redir(struct ast *node); +struct ast *ast_create_redir(char *filename, int io_number, + enum ast_redir_type type); +void ast_free_redir(struct ast_redir *redir); + +#endif /* ! AST_REDIR_H */ diff --git a/src/utils/ast/ast_subshell.c b/src/utils/ast/ast_subshell.c new file mode 100644 index 0000000..36efd6f --- /dev/null +++ b/src/utils/ast/ast_subshell.c @@ -0,0 +1,31 @@ +#include "ast_subshell.h" + +bool ast_is_subshell(struct ast *node) +{ + return node != NULL && node->type == AST_SUBSHELL; +} + +struct ast_subshell *ast_get_subshell(struct ast *node) +{ + if (ast_is_subshell(node)) + return (struct ast_subshell *)node->data; + return NULL; +} + +struct ast *ast_create_subshell(struct ast *child) +{ + struct ast_subshell *subshell = calloc(1, sizeof(struct ast_subshell)); + if (subshell == NULL) + return NULL; + subshell->child = child; + + return ast_create(AST_SUBSHELL, subshell); +} + +void ast_free_subshell(struct ast_subshell *subshell) +{ + if (!subshell) + return; + ast_free(&subshell->child); + free(subshell); +} diff --git a/src/utils/ast/ast_subshell.h b/src/utils/ast/ast_subshell.h new file mode 100644 index 0000000..a4648ef --- /dev/null +++ b/src/utils/ast/ast_subshell.h @@ -0,0 +1,19 @@ +#ifndef AST_SUBSHELL_H +#define AST_SUBSHELL_H + +#include "ast_base.h" + +struct ast_subshell +{ + struct ast *child; +}; + +bool ast_is_subshell(struct ast *node); + +struct ast_subshell *ast_get_subshell(struct ast *node); + +struct ast *ast_create_subshell(struct ast *child); + +void ast_free_subshell(struct ast_subshell *subshell); + +#endif /* ! AST_SUBSHELL_H */ \ No newline at end of file diff --git a/src/utils/ast/ast_void.c b/src/utils/ast/ast_void.c new file mode 100644 index 0000000..e7d2dee --- /dev/null +++ b/src/utils/ast/ast_void.c @@ -0,0 +1,14 @@ +#include "ast_void.h" + +#include +#include + +bool ast_is_void(struct ast *node) +{ + return node != NULL && node->type == AST_VOID; +} + +struct ast *ast_create_void(void) +{ + return ast_create(AST_VOID, NULL); +} diff --git a/src/utils/ast/ast_void.h b/src/utils/ast/ast_void.h new file mode 100644 index 0000000..05a5933 --- /dev/null +++ b/src/utils/ast/ast_void.h @@ -0,0 +1,18 @@ +#ifndef AST_VOID_H +#define AST_VOID_H + +#include "../lists/lists.h" +#include "ast_base.h" + +/** + * Checks if the given AST node is of type AST_VOID. + */ +bool ast_is_void(struct ast *node); + +/** + * Creates a new AST node representing NOTHING + * WARNING: data will be a NULL pointer + */ +struct ast *ast_create_void(void); + +#endif /* ! AST_VOID_H */ diff --git a/src/utils/ast/ast_word.c b/src/utils/ast/ast_word.c new file mode 100644 index 0000000..d83489c --- /dev/null +++ b/src/utils/ast/ast_word.c @@ -0,0 +1,49 @@ +#define _POSIX_C_SOURCE 200809L +#include "ast_word.h" + +#include +#include +#include + +struct ast *ast_create_word(char *word) +{ + struct ast_word *ast_node = malloc(sizeof(struct ast_word)); + if (ast_node == NULL) + return NULL; + + ast_node->type = AST_WORD; + ast_node->word = strdup(word); + struct ast *res = ast_create(AST_WORD, ast_node); + if (res == NULL) + { + free(ast_node->word); + free(ast_node); + return NULL; + } + + return res; +} + +struct ast_word *ast_get_word(struct ast *node) +{ + if (node == NULL || node->type != AST_WORD) + return NULL; + + return node->data; +} + +bool ast_is_word(struct ast *node) +{ + return node && node->type == AST_WORD; +} + +void ast_free_word(struct ast_word *ast_node) +{ + if (ast_node == NULL) + return; + + if (ast_node->word != NULL) + free(ast_node->word); + + free(ast_node); +} diff --git a/src/utils/ast/ast_word.h b/src/utils/ast/ast_word.h new file mode 100644 index 0000000..a049003 --- /dev/null +++ b/src/utils/ast/ast_word.h @@ -0,0 +1,33 @@ +#ifndef AST_WORD_H +#define AST_WORD_H + +#include "ast_base.h" + +struct ast_word +{ + enum ast_type type; + char *word; +}; + +/** + * Checks if the given AST node is a command. + */ +bool ast_is_word(struct ast *node); + +/** + * Retrieves the command data from the given AST node. + * Assumes that the node is of type AST_CMD. + */ +struct ast_word *ast_get_word(struct ast *node); + +/** + * Creates a new AST node representing a command. + */ +struct ast *ast_create_word(char *word); + +/* + * @brief: frees the given ast_command and sets the pointer to NULL. + */ +void ast_free_word(struct ast_word *ast_node); + +#endif /* ! AST_WORD_H */ diff --git a/src/utils/hash_map/hash_map.c b/src/utils/hash_map/hash_map.c new file mode 100644 index 0000000..0da4fc4 --- /dev/null +++ b/src/utils/hash_map/hash_map.c @@ -0,0 +1,216 @@ +#include "hash_map.h" + +#include +#include +#include +#include +#include +#include + +#include "../ast/ast.h" + +/* +** Hash the key using FNV-1a 32 bits hash algorithm. +*/ +static size_t hash(const char *key) +{ + if (key == NULL) + return 0; + + uint32_t hash = 2166136261; // FNV offset basis + uint32_t prime = 16777619; // FNV prime + + while (*key != 0) + { + hash ^= *key; + hash *= prime; + key++; + } + + return hash; +} + +static void destroy_pair_list(struct pair_list **p) +{ + free((char *)(*p)->key); + free((*p)->value); + free((*p)); + *p = NULL; +} + +static void destroy_pair_list_ast(struct pair_list **p) +{ + free((char *)(*p)->key); + ast_free((*p)->value); + free((*p)); + *p = NULL; +} + +struct hash_map *hash_map_init(size_t size) +{ + struct hash_map *p = malloc(sizeof(struct hash_map)); + if (p != NULL) + { + p->data = calloc(size, sizeof(struct pair_list *)); + if (p->data == NULL) + { + free(p); + return NULL; + } + p->size = size; + } + return p; +} + +bool hash_map_insert(struct hash_map *hash_map, const char *key, void *value, + bool *updated) +{ + if (hash_map == NULL || hash_map->size == 0 || key == NULL || value == NULL) + return false; + + size_t h = hash(key); + struct pair_list **l = hash_map->data; + size_t i = h % hash_map->size; + + if (l[i] != NULL) + { + // check if key is in linked list + struct pair_list *iter = l[i]; + while (iter) + { + if (strcmp(iter->key, key) == 0) + { + // update + free(iter->value); + iter->value = value; + if (updated) + *updated = true; + return true; + } + iter = iter->next; + } + + // if not found => collision + } + struct pair_list *p = malloc(sizeof(struct pair_list)); + if (p == NULL) + return false; + + if (updated) + *updated = false; + + p->key = key; + p->value = value; + p->next = l[i]; + l[i] = p; + l[i] = p; + return true; +} + +void hash_map_free(struct hash_map **hash_map) +{ + struct pair_list *l; + struct pair_list *prev; + + if (hash_map != NULL && *hash_map != NULL) + { + for (size_t i = 0; i < (*hash_map)->size; i++) + { + l = (*hash_map)->data[i]; + while (l != NULL) + { + prev = l; + l = l->next; + destroy_pair_list(&prev); + } + } + free((*hash_map)->data); + free(*hash_map); + *hash_map = NULL; + } +} + +void hash_map_free_ast(struct hash_map **hash_map) +{ + struct pair_list *l; + struct pair_list *prev; + + if (hash_map != NULL && *hash_map != NULL) + { + for (size_t i = 0; i < (*hash_map)->size; i++) + { + l = (*hash_map)->data[i]; + while (l != NULL) + { + prev = l; + l = l->next; + destroy_pair_list_ast(&prev); + } + } + free((*hash_map)->data); + free(*hash_map); + *hash_map = NULL; + } +} + +void hash_map_foreach(struct hash_map *hash_map, + void (*fn)(const char *, const void *)) +{ + if (hash_map == NULL) + return; + + struct pair_list *iter; + for (size_t i = 0; i < hash_map->size; i++) + { + iter = hash_map->data[i]; + while (iter != NULL) + { + fn(iter->key, iter->value); + iter = iter->next; + } + } +} + +const void *hash_map_get(const struct hash_map *hash_map, const char *key) +{ + if (hash_map == NULL || hash_map->size == 0) + return NULL; + + size_t h = hash(key); + size_t i = h % hash_map->size; + struct pair_list *l = hash_map->data[i]; + while (l != NULL) + { + if (strcmp(l->key, key) == 0) + return l->value; + l = l->next; + } + return NULL; +} + +bool hash_map_remove(struct hash_map *hash_map, const char *key) +{ + if (hash_map == NULL || hash_map->size == 0) + return false; + + size_t h = hash(key); + size_t i = h % hash_map->size; + struct pair_list *l = hash_map->data[i]; + struct pair_list *p = NULL; + + while (l != NULL) + { + if (strcmp(l->key, key) == 0) + { + if (p != NULL) + p->next = l->next; + else + hash_map->data[i] = l->next; + destroy_pair_list(&l); + return true; + } + p = l; + l = l->next; + } + return false; +} diff --git a/src/utils/hash_map/hash_map.h b/src/utils/hash_map/hash_map.h new file mode 100644 index 0000000..048a882 --- /dev/null +++ b/src/utils/hash_map/hash_map.h @@ -0,0 +1,47 @@ +#ifndef HASH_MAP_H +#define HASH_MAP_H + +#include +#include + +struct pair_list +{ + const char *key; + void *value; + struct pair_list *next; +}; + +struct hash_map +{ + struct pair_list **data; + size_t size; +}; + +struct hash_map *hash_map_init(size_t size); + +/** + * @brief Inserts a key-value pair into the hash map. Key and value are expected + * to be on the heap and will be managed by the hash map. It means they are + * consumed by this function. If the key already exists, its value is updated + * and the key is not consumed so it must be freed by the caller. + * + * @param hash_map The hash map. + * @param key The key to insert. + * @param value The value to insert. + * @param updated If not NULL, set to true if the key was already present and + * updated, false if the key was newly inserted. + * @return true on success, false on failure. + */ +bool hash_map_insert(struct hash_map *hash_map, const char *key, void *value, + bool *updated); + +void hash_map_free(struct hash_map **hash_map); + +void hash_map_foreach(struct hash_map *hash_map, + void (*fn)(const char *, const void *)); + +const void *hash_map_get(const struct hash_map *hash_map, const char *key); + +bool hash_map_remove(struct hash_map *hash_map, const char *key); + +#endif /* ! HASH_MAP_H */ diff --git a/src/utils/lists/lists.h b/src/utils/lists/lists.h new file mode 100644 index 0000000..9f8cebb --- /dev/null +++ b/src/utils/lists/lists.h @@ -0,0 +1,115 @@ +#ifndef LISTS_H +#define LISTS_H + +#include + +struct list +{ + void *data; + struct list *next; +}; + +/* +** @brief Insert a node containing `value` at the beginning of the list. +** @return `NULL` if an error occured. +*/ +struct list *list_prepend(struct list *list, void *value); + +/* +** Return the lenght of the list. +** Return `0` if the list is empty. +*/ +size_t list_length(struct list *list); + +/* +** Display the list contents on `stdout`. +** Nothing is displayed if the list is empty. +*/ +void list_print(struct list *list); + +/* +** Release the memory used by the list. +** Does nothing if `list` is `NULL`. +*/ +void list_destroy(struct list **list); + +/* +** Release the memory used by the list and its content +** Does nothing if `list` is `NULL`. +*/ +void list_deep_destroy(struct list *l); + +/* +** Append a node containing `value` at the end of the list. +** Return `NULL` if an error occured. +*/ +// START PROTO list_append +struct list *list_append(struct list *list, void *value); +// END PROTO list_append + +/* +** Insert a node containing `value` at the index `index` in the list. +** If the index is greater than the length of the list, the behaviour is the +** same as `list_append`. +** Return `NULL` if an error occured. +*/ +// START PROTO list_insert +struct list *list_insert(struct list *list, void *value, size_t index); +// END PROTO list_insert + +/* +** Remove the element at the index `index`. +** Return `NULL` if an error occured. +*/ +// START PROTO list_remove +struct list *list_remove(struct list *list, size_t index); +// END PROTO list_remove + +/* +** Return the position of the first node containing `value`. +** Return `-1` if nothing is found. +*/ +// START PROTO list_find +int list_find(struct list *list, void *value); +// END PROTO list_find + +/* +** Concatenate the list `list2` at the end of the list `list`. +** Return `list2` if `list` is `NULL`. +*/ +// START PROTO list_concat +// struct list *list_concat(struct list *list, struct list *list2); +// END PROTO list_concat + +/* +** Sort the elements of the list in ascending order. +** Return the new list. +*/ +// START PROTO list_sort +// struct list *list_sort(struct list *list); +// END PROTO list_sort + +/* +** Invert the order of the elements of the list. +** Return the new list. +*/ +// START PROTO list_reverse +// struct list *list_reverse(struct list *list); +// END PROTO list_reverse + +/* +** Split the list at index `index`. +** First part goes in `list` and contains the element at `index`. +** Second part is returned. +** Return `NULL` if `list` is `NULL` or `index` is invalid. +*/ +// START PROTO list_split +// struct list *list_split(struct list *list, size_t index); +// END PROTO list_split + +/** + * @brief: Folds the list from left to right using func and an accumulator. + */ +void list_fold(struct list *list, void *acc, void (*func)(void *, void *)); + +#endif /* ! LISTS_H */ diff --git a/src/utils/lists/lists1.c b/src/utils/lists/lists1.c new file mode 100644 index 0000000..521e131 --- /dev/null +++ b/src/utils/lists/lists1.c @@ -0,0 +1,88 @@ +#include +#include +#include + +#include "lists.h" + +struct list *list_prepend(struct list *list, void *value) +{ + struct list *new_elt = malloc(sizeof(struct list)); + if (new_elt == NULL) + { + return NULL; + } + new_elt->next = list; + new_elt->data = value; + + return new_elt; +} + +size_t list_length(struct list *list) +{ + size_t len = 0; + while (list != NULL) + { + len++; + list = list->next; + } + return len; +} + +void list_print(struct list *list) +{ + if (list == NULL) + { + return; + } + + while (list != NULL) + { + if (list->next != NULL) + { + printf("%p ", list->data); + } + else + { + printf("%p\n", list->data); + } + list = list->next; + } +} + +void list_destroy(struct list **list) +{ + struct list *elt = *list; + struct list *next_elt; + while (elt != NULL) + { + next_elt = elt->next; + free(elt); + elt = next_elt; + } + *list = NULL; +} + +struct list *list_append(struct list *list, void *value) +{ + if (list == NULL) + { + struct list *new_elt = malloc(sizeof(struct list)); + new_elt->data = value; + new_elt->next = NULL; + return new_elt; + } + + struct list *elt = list; + + while (elt->next != NULL) + { + elt = elt->next; + } + + struct list *new_elt = malloc(sizeof(struct list)); + new_elt->data = value; + new_elt->next = NULL; + elt->next = new_elt; + + return list; +} diff --git a/src/utils/lists/lists2.c b/src/utils/lists/lists2.c new file mode 100644 index 0000000..745098a --- /dev/null +++ b/src/utils/lists/lists2.c @@ -0,0 +1,107 @@ +#include + +#include "lists.h" + +/* + * + ******************* + * Advanced * + ******************* + * + */ + +struct list *list_insert(struct list *list, void *value, size_t index) +{ + if (list == NULL || index == 0) + { + struct list *new_elt = malloc(sizeof(struct list)); + new_elt->data = value; + new_elt->next = list; + return new_elt; + } + + struct list *elt = list; + + for (size_t i = 0; i < index - 1; i++) + { + if (elt->next == NULL) + { + break; + } + elt = elt->next; + } + + struct list *new_elt = malloc(sizeof(struct list)); + new_elt->data = value; + new_elt->next = elt->next; + elt->next = new_elt; + + return list; +} + +struct list *list_remove(struct list *list, size_t index) +{ + struct list *elt = list; + struct list *prev_elt; + + if (index == 0) + { + struct list *res = elt->next; + free(elt); + return res; + } + + for (size_t i = 0; i < index; i++) + { + if (elt == NULL) + { + return list; + } + prev_elt = elt; + elt = elt->next; + } + if (elt == NULL) + { + return list; + } + + prev_elt->next = elt->next; + free(elt); + + return list; +} + +int list_find(struct list *list, void *value) +{ + if (list == NULL) + { + return -1; + } + + int res = 0; + while (list->data != value) + { + list = list->next; + res++; + if (list == NULL) + { + return -1; + } + } + return res; +} + +struct list *list_concat(struct list *list, struct list *list2) +{ + if (list == NULL) + { + return list2; + } + struct list *elt = list; + while (elt->next != NULL) + { + elt = elt->next; + } + elt->next = list2; + return list; +} diff --git a/src/utils/lists/lists3.c b/src/utils/lists/lists3.c new file mode 100644 index 0000000..c18c7be --- /dev/null +++ b/src/utils/lists/lists3.c @@ -0,0 +1,127 @@ +#include + +#include "lists.h" +/* + * + ****************** + * Ultimate * + ****************** + * + */ + +static void swap_next(struct list *elt) +{ + void *c = elt->next->data; + elt->next->data = elt->data; + elt->data = c; +} +struct list *list_sort(struct list *list) +{ + // Bubble sort go ! + if (list == NULL) + { + return list; + } + struct list *elt = list; + int len = 0; + while (elt->next != NULL) + { + if (elt->data > elt->next->data) + { + swap_next(elt); + } + elt = elt->next; + len++; + } + + for (int i = 1; i < len; i++) + { + elt = list; + while (elt->next != NULL) + { + if (elt->data > elt->next->data) + { + swap_next(elt); + } + elt = elt->next; + } + } + + return list; +} + +// Old proto +// WARNING no malloc/free allowed (moulinette issue) +struct list *list_reverse(struct list *list) +{ + if (list == NULL) + { + return list; + } + + // Get len + struct list *elt = list; + int len = 0; + while (elt->next != NULL) + { + len++; + elt = elt->next; + } + elt = list; + + // Bring each elt to end + for (int i = 0; i < len; i++) + { + elt = list; + for (int j = 0; j < len - i; j++) + { + swap_next(elt); + elt = elt->next; + } + } + + return list; +} + +struct list *list_split(struct list *list, size_t index) +{ + struct list *elt = list; + for (size_t i = 0; i < index; i++) + { + if (elt == NULL) + { + return NULL; + } + elt = elt->next; + } + if (elt == NULL) + { + return NULL; + } + struct list *res = elt->next; + elt->next = NULL; + return res; +} + +void list_deep_destroy(struct list *l) +{ + struct list *elt = l; + struct list *next_elt; + while (elt != NULL) + { + next_elt = elt->next; + free(elt->data); + free(elt); + elt = next_elt; + } +} + +void list_fold(struct list *list, void *acc, void (*func)(void *, void *)) +{ + struct list *elt = list; + while (elt != NULL) + { + func(acc, elt->data); + elt = elt->next; + } +} diff --git a/src/utils/main_loop/main_loop.c b/src/utils/main_loop/main_loop.c new file mode 100644 index 0000000..362f640 --- /dev/null +++ b/src/utils/main_loop/main_loop.c @@ -0,0 +1,111 @@ +#define _POSIX_C_SOURCE 200809L + +#include "main_loop.h" + +// === Includes +#include +#include +#include +#include +#include +#include + +#include "../../execution/execution.h" +#include "../../io_backend/io_backend.h" +#include "../../lexer/lexer.h" +#include "../../parser/parser.h" +#include "../args/args.h" +#include "../vars/vars.h" + +// === Functions + +int err_input(struct hash_map **vars, struct lexer_context *ctx) +{ + hash_map_free(vars); + destroy_lexer_context(ctx); + return ERR_INPUT_PROCESSING; +} + +int main_loop(struct lexer_context *ctx, struct hash_map *vars) +{ + int return_code = SUCCESS; + + // Retrieve and build first AST + struct ast *command_ast = get_ast(ctx); + + // Main parse-execute loop + while (command_ast != NULL && command_ast->type != AST_END) + { + if (command_ast->type != AST_VOID) + { + // Execute AST + return_code = execution(command_ast, vars); + + // set $? variable + set_var_int(vars, "?", return_code); + } + + ast_free(&command_ast); + + // Retrieve and build next AST + command_ast = get_ast(ctx); + } + + if (command_ast == NULL) + return err_input(&vars, ctx); + + // === free + + ast_free(&command_ast); + hash_map_free(&vars); + destroy_lexer_context(ctx); + + return return_code; +} + +/* @brief: initializes a lexer context from a command string. + * @return: pointer to the lexer context, or NULL on failure. + */ +static struct lexer_context *lexer_init_from_string(char *command) +{ + // Create a lexer context from the command string + struct lexer_context *ctx = calloc(1, sizeof(struct lexer_context)); + if (ctx == NULL) + return NULL; + + ctx->end_previous_token = strdup(command); + if (ctx->end_previous_token == NULL) + { + free(ctx); + return NULL; + } + ctx->remaining_chars = strlen(command); + + return ctx; +} + +int start_subshell(struct hash_map **parent_vars, char *command) +{ + int fd = fork(); + if (fd < 0) + return ERR_GENERIC; + + else if (fd == 0) // Child process + { + struct lexer_context *ctx = lexer_init_from_string(command); + if (ctx == NULL) + return ERR_MALLOC; + int return_code = main_loop(ctx, *parent_vars); + exit(return_code); + } + else // Parent process + { + int status; + if (waitpid(fd, &status, 0) == -1) + return ERR_GENERIC; + if (WIFEXITED(status)) + return WEXITSTATUS(status); + else + return ERR_GENERIC; + } +} diff --git a/src/utils/main_loop/main_loop.h b/src/utils/main_loop/main_loop.h new file mode 100644 index 0000000..286c5b0 --- /dev/null +++ b/src/utils/main_loop/main_loop.h @@ -0,0 +1,31 @@ +#ifndef MAIN_LOOP_H +#define MAIN_LOOP_H + +#include "../../utils/vars/vars.h" +#include "../../lexer/lexer.h" + +// === Error codes +#define SUCCESS 0 +#define ERR_INPUT_PROCESSING 2 +#define ERR_MALLOC 3 +#define ERR_GENERIC 4 + +/* @brief: main loop called from main. + * @return: exit code. + */ +int main_loop(struct lexer_context *ctx, struct hash_map *vars); + +/* + * @brief: frees the hash map and lexer context. + * @return: ERR_INPUT_PROCESSING. + */ +int err_input(struct hash_map **vars, struct lexer_context *ctx); + +/* + * @brief: starts a subshell and builds the intern lexer context + * from the string. + * @return: exit code of the subshell. + */ +int start_subshell(struct hash_map **parent_vars, char *command); + +#endif /* MAIN_LOOP_H */ \ No newline at end of file diff --git a/src/utils/string_utils/string_utils.c b/src/utils/string_utils/string_utils.c new file mode 100644 index 0000000..15ad87e --- /dev/null +++ b/src/utils/string_utils/string_utils.c @@ -0,0 +1,53 @@ +#include "string_utils.h" + +#include +#include +#include +#include + +char *trim_blank_left(char *str) +{ + if (str == NULL) + return NULL; + + while (*str != '\0' && isblank(*str)) + str++; + + return str; +} + +char *insert_into(char *dest, const char *src, size_t pos, size_t len) +{ + size_t res_len = strlen(dest); + size_t prefix_len = pos; + size_t suffix_len = res_len - (pos + len); + size_t src_len = strlen(src); + size_t new_len = prefix_len + src_len + suffix_len; + + if (dest == NULL || src == NULL || pos + len > res_len) + return NULL; + + if (res_len < new_len) + { + char *p = realloc(dest, new_len + 1); + if (p == NULL) + return NULL; // allocation failure + dest = p; + } + + memmove(dest + pos + src_len, dest + pos + len, suffix_len); + memcpy(dest + pos, src, src_len); + dest[new_len] = 0; + + if (res_len > new_len) + return realloc(dest, new_len + 1); + return dest; +} + +void int_to_str(int value, char *buffer) +{ + if (buffer == NULL) + return; + + snprintf(buffer, 11, "%d", value); +} diff --git a/src/utils/string_utils/string_utils.h b/src/utils/string_utils/string_utils.h new file mode 100644 index 0000000..36f23ac --- /dev/null +++ b/src/utils/string_utils/string_utils.h @@ -0,0 +1,30 @@ +#ifndef STRING_UTILS_H +#define STRING_UTILS_H + +#include + +/* + * @brief trims leading blank characters (space and tab) from the input string. + * @param str input string to be trimmed. + * @return pointer to the first non-blank character in the string. If the + * string consists entirely of blank characters, returns a pointer to the null + * terminator at the end of the string. + */ +char *trim_blank_left(char *str); + +/** + * Inserts a substring into a destination string at a specified position, + * replacing a specified length of characters. + */ +char *insert_into(char *dest, const char *src, size_t pos, size_t len); + +/** + * Converts an integer to its string representation. + * @param value The integer value to convert. + * @param buffer A character array where the resulting string will be stored. + * The buffer must be at least 11 bytes long to accommodate the largest + * 32-bit integer and the null terminator. + */ +void int_to_str(int value, char *buffer); + +#endif /* STRING_UTILS_H */ diff --git a/src/utils/vars/vars.c b/src/utils/vars/vars.c new file mode 100644 index 0000000..1d94355 --- /dev/null +++ b/src/utils/vars/vars.c @@ -0,0 +1,86 @@ +#define _POSIX_C_SOURCE 200809L +#include "vars.h" + +#include +#include +#include +#include +#include +#include + +#include "../hash_map/hash_map.h" +#include "../string_utils/string_utils.h" + +#define VARS_INITIAL_SIZE 16 + +static void vars_default(struct hash_map *vars) +{ + set_var_copy(vars, "?", "0"); + pid_t pid = getpid(); + char pid_str[11]; + int_to_str(pid, pid_str); + set_var_copy(vars, "$", pid_str); +} + +struct hash_map *vars_init(void) +{ + struct hash_map *vars = hash_map_init(VARS_INITIAL_SIZE); + if (vars != NULL) + { + vars_default(vars); + char uid_str[11]; + int_to_str((int)getuid(), uid_str); + set_var_copy(vars, "UID", uid_str); + } + return vars; +} + +short short_random(void) +{ + static bool seeded = false; + if (!seeded) + { + srand((unsigned)time(NULL)); + seeded = true; + } + return (short)(rand() & 0x7FFF); // force 16 bits positive +} + +char *get_var(const struct hash_map *vars, const char *key) +{ + return (char *)hash_map_get(vars, key); +} + +char *get_var_or_env(const struct hash_map *vars, const char *key) +{ + char *value = (char *)hash_map_get(vars, key); + if (value == NULL) + value = getenv(key); + return value; +} + +bool set_var(struct hash_map *vars, const char *key, const char *value, + bool *updated) +{ + return hash_map_insert(vars, key, (void *)value, updated); +} + +bool set_var_copy(struct hash_map *vars, const char *key, const char *value) +{ + char *key_copy = strdup(key); + char *value_copy = strdup(value); + bool updated; + bool res = set_var(vars, key_copy, value_copy, &updated); + if (updated || !res) + free(key_copy); + if (!res) + free(value_copy); + return res; +} + +bool set_var_int(struct hash_map *vars, const char *key, int value) +{ + char value_str[11]; + int_to_str(value, value_str); + return set_var_copy(vars, key, value_str); +} diff --git a/src/utils/vars/vars.h b/src/utils/vars/vars.h new file mode 100644 index 0000000..19ffae1 --- /dev/null +++ b/src/utils/vars/vars.h @@ -0,0 +1,49 @@ +#ifndef VARS_H +#define VARS_H + +#include + +#include "../hash_map/hash_map.h" + +/** + * Initialize a new variables hash map. + */ +struct hash_map *vars_init(void); + +/** + * Generate a random short integer (16 bits positive [0-32767]). + */ +short short_random(void); + +/** + * Get the value of a variable, NULL if not found. + */ +char *get_var(const struct hash_map *vars, const char *key); + +/** + * Get the value of a variable, from the environment if not found in vars, + * NULL if not found in either. + */ +char *get_var_or_env(const struct hash_map *vars, const char *key); + +/** + * Set the value of a variable. Key and value ownership are transferred to + * the hash_map and need to be on the heap. Returns true on success, false on + * failure. + */ +bool set_var(struct hash_map *vars, const char *key, const char *value, + bool *updated); + +/** + * Same as set_var, but makes copies of key and value so you don't have to worry + * about their memory management. Returns true on success, false on failure. + */ +bool set_var_copy(struct hash_map *vars, const char *key, const char *value); + +/** + * Set the value of a variable to an integer. Behavior is similar to + * set_var_copy. + */ +bool set_var_int(struct hash_map *vars, const char *key, int value); + +#endif /* ! VARS_H */ diff --git a/tests/functional/and_ors.sh b/tests/functional/and_ors.sh new file mode 100755 index 0000000..c2f4cf0 --- /dev/null +++ b/tests/functional/and_ors.sh @@ -0,0 +1,17 @@ +true +echo 'true =' $? + +false +echo 'false =' $? + +false && true +echo 'false && true =' $? + +true && false +echo 'true && false =' $? + +true || false +echo 'true || false =' $? + +false || true +echo 'false || true =' $? diff --git a/tests/functional/func.sh b/tests/functional/func.sh new file mode 100755 index 0000000..dbd0590 --- /dev/null +++ b/tests/functional/func.sh @@ -0,0 +1,21 @@ +func() +{ + echo hello +} + +arg_func() +{ + echo first argument is "$1" +} + +func_in_func() +{ + func +} + +func_one_line() { echo "this is on one line"; } + +func +arg_func "HERE" +func_in_func +func_one_line diff --git a/tests/functional/loops.sh b/tests/functional/loops.sh new file mode 100755 index 0000000..c4d9896 --- /dev/null +++ b/tests/functional/loops.sh @@ -0,0 +1,20 @@ +echo "starting tests" + +while false; +do + echo "should NOT be printed" +done + +a='yes' +while [ "$a" -eq "yes" ]; +do + a="no" + echo "should be printed only once" +done; + +while true; +do + echo "yes" +done; + +echo "tests done" diff --git a/tests/functional/run-tests.sh b/tests/functional/run-tests.sh new file mode 100755 index 0000000..5592869 --- /dev/null +++ b/tests/functional/run-tests.sh @@ -0,0 +1,461 @@ +#!/bin/sh + + +################### +# Variables # +################### + +executable="$BIN_PATH" +ref_executable="dash" + +tmp_script="/tmp/test_script.sh" +output="/tmp/42sh_tests.output" +ref_output="/tmp/42sh_tests_ref.output" + +total_tests=0 +errors_count=0 +timeouts_count=0 + + +################## +# Colors # +################## + +# Reset +Color_Off='\033[0m' # Text Reset + +# Regular Colors +Black='\033[0;30m' # Black +Red='\033[0;31m' # Red +Green='\033[0;32m' # Green +Yellow='\033[0;33m' # Yellow +Blue='\033[0;34m' # Blue +Purple='\033[0;35m' # Purple +Cyan='\033[0;36m' # Cyan +White='\033[0;37m' # White + +# Bold +BBlack='\033[1;30m' # Black +BRed='\033[1;31m' # Red +BGreen='\033[1;32m' # Green +BYellow='\033[1;33m' # Yellow +BBlue='\033[1;34m' # Blue +BPurple='\033[1;35m' # Purple +BCyan='\033[1;36m' # Cyan +BWhite='\033[1;37m' # White + +# Underline +UBlack='\033[4;30m' # Black +URed='\033[4;31m' # Red +UGreen='\033[4;32m' # Green +UYellow='\033[4;33m' # Yellow +UBlue='\033[4;34m' # Blue +UPurple='\033[4;35m' # Purple +UCyan='\033[4;36m' # Cyan +UWhite='\033[4;37m' # White + +# Background +On_Black='\033[40m' # Black +On_Red='\033[41m' # Red +On_Green='\033[42m' # Green +On_Yellow='\033[43m' # Yellow +On_Blue='\033[44m' # Blue +On_Purple='\033[45m' # Purple +On_Cyan='\033[46m' # Cyan +On_White='\033[47m' # White + +# High Intensity +IBlack='\033[0;90m' # Black +IRed='\033[0;91m' # Red +IGreen='\033[0;92m' # Green +IYellow='\033[0;93m' # Yellow +IBlue='\033[0;94m' # Blue +IPurple='\033[0;95m' # Purple +ICyan='\033[0;96m' # Cyan +IWhite='\033[0;97m' # White + +# Bold High Intensity +BIBlack='\033[1;90m' # Black +BIRed='\033[1;91m' # Red +BIGreen='\033[1;92m' # Green +BIYellow='\033[1;93m' # Yellow +BIBlue='\033[1;94m' # Blue +BIPurple='\033[1;95m' # Purple +BICyan='\033[1;96m' # Cyan +BIWhite='\033[1;97m' # White + +# High Intensity backgrounds +On_IBlack='\033[0;100m' # Black +On_IRed='\033[0;101m' # Red +On_IGreen='\033[0;102m' # Green +On_IYellow='\033[0;103m' # Yellow +On_IBlue='\033[0;104m' # Blue +On_IPurple='\033[0;105m' # Purple +On_ICyan='\033[0;106m' # Cyan +On_IWhite='\033[0;107m' # White + + + +################## +# Wrappers # +################## + + +# @arg test command +# @arg actual code +# @arg ref code +check_result() { + + command="$1" + actual_code="$2" + ref_code="$3" + + test_failed=0 + + # Check return code + if [[ "$actual_code" -eq 124 ]]; then + echo -e $BRed "TIMEOUT" $Color_Off + echo -e ' ' "on '$command'" + echo -e ' ' "Expected code $ref_code but got $actual_code" + ((timeouts_count++)) + test_failed=1 + elif [[ "$actual_code" -ne "$ref_code" ]]; then + echo -e $BRed "FAILED" $Color_Off + echo -e ' ' "on '$command'" + echo -e ' ' "Expected code $ref_code but got $actual_code" + test_failed=1 + # Check output + elif [[ $(diff $output $ref_output > /dev/null) -ne 0 ]]; then + echo -e $BRed "FAILED" $Color_Off + echo -e ' ' "on '$command'" + echo -e ' ' "Output is not the one expected" + test_failed=1 + else + echo -e $BGreen OK $Color_Off + fi + + if [[ "$test_failed" -eq 1 ]]; then + ((errors_count++)) + fi + +} + + +# @arg test name +# @arg input string +test_str() { + + # Check input + if [[ -z "$1" || -z "$2" ]]; then + echo -e $BRed "\n\n" "Issue in the testsuite: test_str: One or more argument is empty" $Color_Off + exit 2 + fi + + echo -e $BBlue "================== $1 ==================" $Color_Off + + echo -e "$2" > $tmp_script + + # Arg + echo -e -n $Blue "= [ARG] " $Color_Off + timeout 0.2 $executable -c "$2" &> $output + actual_code=$? + $ref_executable -c "$2" &> $ref_output + ref_code=$? + ((total_tests++)) + check_result "$2" "$actual_code" "$ref_code" + + # Script + echo -e -n $Blue "= [SCRIPT]" $Color_Off + timeout 0.2 $executable "$tmp_script" &> $output + actual_code=$? + $ref_executable "$tmp_script" &> $ref_output + ref_code=$? + ((total_tests++)) + check_result "$2" "$actual_code" "$ref_code" + + # Stdin + echo -e -n $Blue "= [STDIN] " $Color_Off + timeout 0.2 $executable < "$tmp_script" &> $output + actual_code=$? + $ref_executable < "$tmp_script" &> $ref_output + ref_code=$? + ((total_tests++)) + check_result "$2" "$actual_code" "$ref_code" + + echo -e "\n" + # echo -e $BBlue "===========================================" $Color_Off "\n" +} + +# @arg test name +# @arg input script +test_script() { + + # Check input + if [[ -z "$1" || -z "$2" ]]; then + echo -e $BRed "\n\n" "Issue in the testsuite: test_script: One or more argument is empty" $Color_Off + exit 2 + fi + if [[ ! -f "$2" ]]; then + echo -e $BRed "\n\n" "Issue in the testsuite: test_script: Second argument is not a file" $Color_Off + exit 2 + fi + + echo -e $BBlue "================== $1 ==================" $Color_Off + + # Script + echo -e -n "= [SCRIPT] " + timeout 0.2 $executable "$tmp_script" &> $output + actual_code=$? + $ref_executable "$tmp_script" &> $ref_output + ref_code=$? + ((total_tests++)) + check_result "$2" "$actual_code" "$ref_code" + + # Stdin + echo -e -n "= [STDIN] " + timeout 0.2 $executable < "$tmp_script" &> $output + actual_code=$? + $ref_executable < "$2" &> $ref_output + ref_code=$? + ((total_tests++)) + check_result "$tmp_script" "$actual_code" "$ref_code" + + echo -e "\n" + # echo -e $BBlue "===========================================" $Color_Off "\n" +} + +summarize() { + + # Compute statistics + (( passed_tests = $total_tests - $errors_count )) + (( tests_percentage = 100 * $passed_tests / $total_tests )) + + # Adapt color depending on results + + # Percentage + if [[ tests_percentage -gt 80 ]]; then + coverage_color=$BGreen + elif [[ tests_percentage -gt 50 ]]; then + coverage_color=$BYellow + elif [[ tests_percentage -gt 30 ]]; then + coverage_color=$BOrange + else + coverage_color=$BRed + fi + # Timeouts + if [[ $timeouts_count -eq 0 ]]; then + timeouts_color=$BGreen + else + timeouts_color=$BRed + fi + + # Print + echo -e $BWhite "\n\n""===========" $UWhite"Summary"$Color_Off "\n" + echo -e " Passed $coverage_color$passed_tests/$total_tests$Color_Off tests ($coverage_color$tests_percentage%$Color_Off)" + echo -e " Got $timeouts_color$timeouts_count timeout(s)$Color_Off" + if [ "$OUTPUT_FILE" != "" ]; then + echo $tests_percentage > "$OUTPUT_FILE"; + fi + echo + +} + +# *********************************************************** +################# +# Tests # +################# + +echo -e "\n\n""===$BGreen TestsuitatorX Ultra Pro Max+ 365 Premium Gris Sidéral" "\n\n"$Color_Off + + + +echo -e "\n$BBlue=== Builtins ===$Color_Off" +# echo +test_str "Hello" "echo Hello" +test_str "Hello there" "echo Hello there;" +test_str "Hello there;" "echo Hello there;" +test_str "Hello; there;" "echo Hello; echo there;" +test_str "'Hello'" "echo 'Hello there'" +test_str "Hello;Hello" "echo Hello; echo Wesh attends quoi; echo pouquoi je suis une ligne en dessous" +test_str "Echo -n" "echo -n Hello" +test_str "Echo -e" "echo -e 'Hello\nThere'" +test_str "Empty echo" "echo" +test_str "Spaced echo" " echo spaced " + +test_str "Exit 0" "exit 0" +test_str "Exit 1" "exit 1" + +test_str "True" "true" +test_str "False" "false" + +test_str "cd basic" "cd /tmp; pwd" +test_str "cd maison" "cd; pwd" + +test_str "Alias basic" "alias foo=echo; foo bar" + +test_str "cat $0" "alias foo=echo; foo bar" + + +echo -e "\n$BBlue=== Programs ===$Color_Off" +test_str "LS" "ls" +test_str "Wrong ls" "sl" +test_str "IPA ma gueule" "ip a" +test_str "Wrong ls" "sl --bachibouzouk" +test_str "ls a b c" "ls a b c" +test_str "ls -a --best" "ls -a --best" +test_str "ls -a --help" "ls -a --help" + + +echo -e "\n$BBlue=== Quotes ===$Color_Off" +test_str "Single quotes" "echo 'Single Quote'" +test_str "Double quotes" "echo \"Double Quote\"" +test_str "Mixed quotes 1" "echo \"Mixed 'Quotes'\"" +test_str "Mixed quotes 2" "echo 'Mixed \"Quotes\"'" +test_str "Escaped double quote" "echo \"Escaped \\\"Quote\\\"\"" +test_str "Variable in double quotes" "VAR=val; echo \"Variable \$VAR\"" +test_str "Variable in single quotes" "VAR=val; echo 'Variable \$VAR'" +test_str "Backslash in double quotes" "echo \"Backslash \\\\\"" +test_str "Newline in string" "echo \"New\nline\"" +test_str "Empty quotes" "echo '' \"\"" +test_str "Concatenated quotes" "echo 'a'\"b\"'c'" + + +echo -e "\n$BBlue=== Comments ===$Color_Off" +test_str "Hello commentaire" "echo Hello # Commentaire" +test_str "Comment only" "# Comment only" +test_str "Comment after space" "echo foo #bar" +test_str "Hash inside word" "echo foo#bar" +test_str "Comment with special chars" "# echo 'hidden' $PATH" + + +echo -e "\n$BBlue=== Pipelines ===$Color_Off" +test_str "Simple pipe" "echo Hello | cat" +test_str "Double pipe" "echo Hello | rev | rev" # Pas mal non ? c'est frenssé +test_str "Pipe with grep" "echo 'a\nb\nc' | grep b" +test_str "Pipe exit code (là c'est dur)" "true | false | true" +test_str "Pipe sequence" "echo a | echo b" +test_str "Pipe with chiottes" "ls | wc -l" + + +echo -e "\n$BBlue=== Redirections ===$Color_Off" +test_str "Redirect output" "echo hello > /tmp/test_redir; cat /tmp/test_redir; rm /tmp/test_redir" +test_str "Append output" "echo Hello > /tmp/test_redir; echo World >> /tmp/test_redir; cat /tmp/test_redir; rm /tmp/test_redir" +test_str "Redirect input" "echo Hello > /tmp/test_in; cat < /tmp/test_in; rm /tmp/test_in" +test_str "Redirect stderr" "echo Error >&2" +test_str "Redirect to null" "echo Hello > /dev/null" +test_str "Redirect both" "ls > /dev/null 2> /dev/null" +test_str "Redirect fd" "echo foo 2>&1" +test_str "Clobbering" "echo foo > file; echo bar > file; cat file; rm file" +test_str "Pipe and redirect" "echo foo | cat > file; cat file; rm file" +test_str "Heredoc basic" "cat << EOF\nhello\nEOF" + + +echo -e "\n$BBlue=== And/Or ===$Color_Off" +test_str "AND true" "true && echo Oui" +test_str "AND false" "false && echo Non" +test_str "OR true" "true || echo Non" +test_str "OR false" "false || echo Oui" +test_str "AND OR mixed 1" "true && false || echo Oui" +test_str "AND OR mixed 2" "false || true && echo Oui" +test_str "Sequence AND" "echo a && echo b" +test_str "Sequence OR" "echo a || echo b" +test_str "Negation true" "! true" +test_str "Negation false" "! false" + + +echo -e "\n$BBlue=== If ===$Color_Off" +test_str "If true" "if true; then echo Yes; fi" +test_str "If false else" "if false; then echo No; else echo Yes; fi" +test_str "If elif" "if false; then echo No; elif true; then echo Yes; fi" +test_str "Nested if" "if true; then if true; then echo Nested; fi; fi" +test_str "If multiple commands" "if true; then echo a; echo b; fi" +test_str "If complex condition" "if true && true; then echo Yes; fi" +test_str "If with negation" "if ! false; then echo Yes; fi" +test_str "If faut aller niquer sa mere" "if false; ! false; then echo Embrasse moi; fi" + + +echo -e "\n$BBlue=== Loops ===$Color_Off" +test_str "While false" "while false; do false; done" +test_str "While(false) true" "while false; do true; done" +test_str "Until(true) false" "until true; do false; done" +test_str "Until true" "until true; do true; done" +# test_str "While var" "a=2; while [ \$a -eq 2 ]; do \$a=3; done" +test_str "While arithmetic" "i=0; while [ \$i -lt 3 ]; do echo \$i; i=\$((i+1)); done" +test_str "Until arithmetic" "i=0; until [ \$i -ge 3 ]; do echo \$i; i=\$((i+1)); done" +test_str "While break" "while true; do echo break; break; done" +test_str "While continue" "i=0; while [ \$i -lt 3 ]; do i=\$((i+1)); if [ \$i -eq 2 ]; then continue; fi; echo \$i; done" +test_str "For loop basic" "for i in a b c; do echo \$i; done" +test_str "For loop glob" "for i in *; do echo \$i; done" +test_str "For loop break" "for i in 1 2 3; do break; done" +test_str "Azy continue la con de ta mère" "for i in 1 2 3; do continue; done" +test_str "For loop empty" "for i in; do echo \$i; done" +test_str "For loop variable" "VAR='a b'; for i in \$VAR; do echo \$i; done" + + +echo -e "\n$BBlue=== Case ===$Color_Off" +test_str "Case simple" "case a in a) echo Yes;; esac" +test_str "Case basique" "case z in a) echo No;; *) echo Default;; esac" +test_str "Case multiple patterns" "case a in a|b) echo Yes;; esac" +test_str "Case no match (c pcq t'es moche)" "case z in a) echo No;; esac" +test_str "Case with variable" "v=foo; case \$v in foo) echo Yes;; esac" +test_str "Case nested" "case a in a) case b in b) echo Nested;; esac;; esac" + + +echo -e "\n$BBlue=== Variables ===$Color_Off" +test_str "Set and get" "var=value; echo \$var" +test_str "Braces" "var=value; echo \${var}" +test_str "Multi-word value" "var='a b'; echo \$var" +test_str "Unset" "var=value; unset var; echo \$var" +test_str "Export" "export VAR=val; env | grep VAR" +test_str "Assignment return" "a=1" +test_str "Multiple assignment (ouais askip c possible)" "a=1 b=2; echo \$a \$b" +test_str "Default value" "unset v; echo \${v:-default}" +test_str "Assign default" "unset v; echo \${v:=default}; echo \$v" +test_str "Alternative value" "v=val; echo \${v:+alt}" +test_str "Use default if unset" "echo \${unset_var-default}" + +test_str "\$@" "echo \$@" +test_str "\$*" "echo \$*" +test_str "\$?" "echo \$?" +test_str "\$$" "echo \$$" +test_str "\$1" "echo \$1" +test_str "\$2" "echo \$2" +test_str "\${10}" "echo \${10}" +test_str "\$#" "echo \$#" +test_str "\$RANDOM" "echo \$RANDOM" +test_str "\$UID" "echo \$UID" +test_str "\$OLDPWD" "echo \$OLDPWD" +test_str "\$PWD" "echo \$PWD" +test_str "\$IFS" "echo \$IFS" + +test_str "Default exit status" "echo \$?" +test_str "Exit status" "true; echo \$?" +test_str "PID" "echo \$\$" +test_str "Arg count" "echo \$#" + + +echo -e "\n$BBlue=== Arithmetic expansions de fou furieux ===$Color_Off" +test_str "Arithmetic add" "echo \$((1 + 1))" +test_str "Arithmetic mul" "echo \$((2 * 3))" +test_str "Arithmetic div" "echo \$((10 / 2))" +test_str "Arithmetic nested" "echo \$(( (1+2) * 3 ))" +test_str "Arithmetic mod" "echo \$(( 5 % 2 ))" +test_str "Arithmetic var" "var=1; echo \$(( var + 1 ))" +test_str "Command subst" "echo \$(echo command)" +test_str "Backticks" "echo \`echo backticks\`" +test_str "Tilde" "echo ~" +test_str "Length" "v=abc; echo \${#v}" + + +echo -e "\n$BBlue=== Subshells du démon ===$Color_Off" +test_str "Subshell basic" "(echo a; echo b)" +test_str "Subshell isolation" "var=1; (var=2; echo \$var); echo \$var" +test_str "Subshell exit" "(exit 1); echo \$?" +test_str "Grouping basic" "{ echo a; echo b; }" +test_str "Grouping side effect" "var=1; { var=2; }; echo \$var" +test_str "Nested subshells" "( ( echo nested ) )" +test_str "Subshell redirect" "(echo a) > /tmp/sub; cat /tmp/sub; rm /tmp/sub" +test_str "Group redirect" "{ echo a; } > /tmp/grp; cat /tmp/grp; rm /tmp/grp" + + +summarize diff --git a/tests/unit/expansion/expand.c b/tests/unit/expansion/expand.c new file mode 100644 index 0000000..777083e --- /dev/null +++ b/tests/unit/expansion/expand.c @@ -0,0 +1,272 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include + +#include "../../../src/expansion/expansion.h" +#include "../../../src/utils/ast/ast.h" +#include "../../../src/utils/hash_map/hash_map.h" +#include "../../../src/utils/vars/vars.h" + +TestSuite(expand); + +Test(expand, no_expansion) +{ + char str[] = "echo something"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + bool ret = expand(ast_command, NULL); + cr_expect(ret, "expansion failed with %s", str); + cr_expect_str_eq((char *)ast_command->command->data, "echo something", + "String without variables should remain unchanged"); + ast_free(&ast); +} + +Test(expand, single_quotes_no_expansion) +{ + char str[] = "echo '$VAR'"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR", "expanded"); + + bool ret = expand(ast_command, vars); + cr_expect(ret, "expansion failed with %s", str); + cr_expect_str_eq((char *)ast_command->command->data, "echo $VAR", + "Variable should not expand inside single quotes"); + ast_free(&ast); + hash_map_free(&vars); +} + +Test(expand, single_dollar) +{ + char str[] = "echo $ sign"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR", "expanded"); + + bool ret = expand(ast_command, vars); + cr_expect(ret, "expansion failed with %s", str); + cr_expect_str_eq((char *)ast_command->command->data, "echo $ sign", + "Variable should not expand inside single quotes"); + ast_free(&ast); + hash_map_free(&vars); +} + +Test(expand, empty_braces_no_expansion) +{ + char str[] = "echo ${}"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR", "expanded"); + + bool ret = expand(ast_command, vars); + cr_expect(ret == false, "expansion should fail with %s", str); + ast_free(&ast); + hash_map_free(&vars); +} + +Test(expand, basic_expansion) +{ + char str[] = "echo $VAR"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR", "expanded"); + + bool ret = expand(ast_command, vars); + cr_expect(ret, "expansion failed with %s", str); + cr_expect_str_eq((char *)ast_command->command->data, "echo expanded", + "Variable should expand correctly"); + ast_free(&ast); + hash_map_free(&vars); +} + +Test(expand, multiple_expansion) +{ + char str[] = "echo $VAR1 $VAR2 ${VAR3}"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR1", "expanded"); + set_var_copy(vars, "VAR2", "values"); + set_var_copy(vars, "VAR3", "here"); + + bool ret = expand(ast_command, vars); + cr_expect(ret, "expansion failed with %s", str); + cr_expect_str_eq((char *)ast_command->command->data, + "echo expanded values here", + "Multiple variables should expand correctly"); + ast_free(&ast); + hash_map_free(&vars); +} + +Test(expand, env_variable) +{ + char str[] = "echo $MY_ENV_VAR"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + setenv("MY_ENV_VAR", "environment", 0); + + bool ret = expand(ast_command, NULL); + cr_expect(ret, "expansion failed with %s", str); + cr_expect_str_eq((char *)ast_command->command->data, "echo environment", + "Environment variable should expand correctly"); + ast_free(&ast); +} + +Test(expand, undefined_variable) +{ + char str[] = "echo $UNDEFINED"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + + bool ret = expand(ast_command, vars); + cr_expect(ret, "expansion failed with %s", str); + cr_expect_str_eq((char *)ast_command->command->data, "echo ", + "Undefined variable should expand to empty string"); + ast_free(&ast); + hash_map_free(&vars); +} + +Test(expand, nested_expansion) +{ + char str[] = "echo $B"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "A", "expanded"); + set_var_copy(vars, "B", "$A"); + + bool ret = expand(ast_command, vars); + cr_expect(ret, "expansion failed with %s", str); + cr_expect_str_eq((char *)ast_command->command->data, "echo expanded", + "Nested variable should expand correctly"); + ast_free(&ast); + hash_map_free(&vars); +} + +// Test(expand, mixed_quotes_expansion) +// { +// char str[] = "echo \"$VAR1 and '$VAR2'\""; +// char *str_heap = strdup(str); +// struct list *list = list_append(NULL, str_heap); +// struct ast *ast = ast_create_command(list, NULL, NULL); +// struct ast_command *ast_command = ast_get_command(ast); + +// struct hash_map *vars = vars_init(); +// set_var_copy(vars, "VAR1", "expanded"); +// set_var_copy(vars, "VAR2", "not_expanded"); + +// bool ret = expand(ast_command, vars); +// cr_expect(ret, "expansion failed with %s", str); +// cr_expect_str_eq((char *)ast_command->command->data, +// "echo \"expanded and $VAR2\"", +// "Variable in double quotes should expand, while variable " +// "in single quotes should not"); +// ast_free(&ast); +// hash_map_free(&vars); +// } + +Test(expand, adjacent_variables) +{ + char str[] = "echo $VAR1$VAR2"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR1", "hello"); + set_var_copy(vars, "VAR2", "world"); + + bool ret = expand(ast_command, vars); + cr_expect(ret, "expansion failed with %s", str); + cr_expect_str_eq((char *)ast_command->command->data, "echo helloworld", + "Adjacent variables should expand correctly"); + ast_free(&ast); + hash_map_free(&vars); +} + +Test(expand, random) +{ + char str[] = "$RANDOM"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + bool ret = expand(ast_command, NULL); + cr_expect(ret, "expansion failed with %s", str); + int rnd = atoi((char *)ast_command->command->data); + cr_assert(rnd >= 0 && rnd <= 32767, + "RANDOM variable should expand to a value between 0 and 32767"); + ast_free(&ast); +} + +Test(expand, pid) +{ + char str[] = "$$"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + bool ret = expand(ast_command, vars); + cr_expect(ret, "expansion failed with %s", str); + int pid = atoi((char *)ast_command->command->data); + cr_assert(pid == getpid(), + "$$ variable should expand to the pid of the process"); + ast_free(&ast); + hash_map_free(&vars); +} + +Test(expand, default_last_exit_code) +{ + char str[] = "$?"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list, NULL, NULL); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + bool ret = expand(ast_command, vars); + cr_expect(ret, "expansion failed with %s", str); + int code = atoi((char *)ast_command->command->data); + cr_assert(code == 0, + "$? variable should expand to the last exit code (default 0)"); + ast_free(&ast); + hash_map_free(&vars); +} diff --git a/tests/unit/expansion/parse_subshell.c b/tests/unit/expansion/parse_subshell.c new file mode 100644 index 0000000..34ada43 --- /dev/null +++ b/tests/unit/expansion/parse_subshell.c @@ -0,0 +1,72 @@ +#include +#include + +#include "../../../src/expansion/expansion.h" + +TestSuite(parse_subshell_str); + +Test(parse_subshell_str, basic_subshell) +{ + char *input = "(ls -l)"; + char *extracted_var = NULL; + size_t r = parse_subshell_str(input, &extracted_var); + + cr_expect(r == 7); + cr_expect_str_eq(extracted_var, "ls -l"); + free(extracted_var); +} + +Test(parse_subshell_str, multi_basic_subshell) +{ + char *input = "(echo hello) and (echo world)"; + char *extracted_var = NULL; + size_t r = parse_subshell_str(input, &extracted_var); + + cr_expect(r == 12); + cr_expect_str_eq(extracted_var, "echo hello"); + free(extracted_var); + + input += r + 5; // skip " and " + r = parse_subshell_str(input, &extracted_var); + + cr_expect(r == 12); + cr_expect_str_eq(extracted_var, "echo world"); + free(extracted_var); +} + +Test(parse_subshell_str, incomplete_braces) +{ + char *input = "(echo hello"; + char *extracted_var = NULL; + size_t r = parse_subshell_str(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_subshell_str, empty_braces) +{ + char *input = "()"; + char *extracted_var = NULL; + size_t r = parse_subshell_str(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_subshell_str, nested_subshell) +{ + char *input = "(echo (nested))"; + char *extracted_var = NULL; + size_t r = parse_subshell_str(input, &extracted_var); + + cr_expect(r == 15); + cr_expect_str_eq(extracted_var, "echo (nested)"); + free(extracted_var); + + char *nested = input + 6; // point to the nested subshell + r = parse_subshell_str(nested, &extracted_var); + cr_expect(r == 8); + cr_expect_str_eq(extracted_var, "nested"); + free(extracted_var); +} diff --git a/tests/unit/expansion/parse_var.c b/tests/unit/expansion/parse_var.c new file mode 100644 index 0000000..4dc9e08 --- /dev/null +++ b/tests/unit/expansion/parse_var.c @@ -0,0 +1,143 @@ +#include +#include + +#include "../../../src/expansion/expansion.h" + +TestSuite(parse_var_name); + +Test(parse_var_name, basic_variable) +{ + char *input = "$MY_VAR"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 7); + cr_expect_str_eq(extracted_var, "MY_VAR"); + free(extracted_var); +} + +Test(parse_var_name, multi_basic_variable) +{ + char *input = "$MY$VAR"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 3); + cr_expect_str_eq(extracted_var, "MY"); + free(extracted_var); + + input += r; + r = parse_var_name(input, &extracted_var); + + cr_expect(r == 4); + cr_expect_str_eq(extracted_var, "VAR"); + free(extracted_var); +} + +Test(parse_var_name, variable_with_braces) +{ + char *input = "${MY_VAR}"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 9); + cr_expect_str_eq(extracted_var, "MY_VAR"); + free(extracted_var); +} + +Test(parse_var_name, special_variable) +{ + char *input = "$1"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 2); + cr_expect_str_eq(extracted_var, "1"); + free(extracted_var); +} + +Test(parse_var_name, special_variable_with_braces) +{ + char *input = "${1}"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 4); + cr_expect_str_eq(extracted_var, "1"); + free(extracted_var); +} + +Test(parse_var_name, incomplete_braces) +{ + char *input = "${MY_VAR"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_var_name, empty_braces) +{ + char *input = "${}"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_var_name, dollar_sign_only) +{ + char *input = "$"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_var_name, variable_followed_by_dollar) +{ + char *input = "$MY$VAR$"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 3); + cr_expect_str_eq(extracted_var, "MY"); + free(extracted_var); + + input += r; + r = parse_var_name(input, &extracted_var); + + cr_expect(r == 4); + cr_expect_str_eq(extracted_var, "VAR"); + free(extracted_var); + + input += r; + r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_var_name, special_variable_followed_by_text) +{ + char *input = "$1VAR"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 2); + cr_expect_str_eq(extracted_var, "1"); + free(extracted_var); +} + +Test(parse_var_name, bad_variable_with_braces) +{ + char *input = "${1VAR}"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} diff --git a/tests/unit/io_backend/io_backend.c b/tests/unit/io_backend/io_backend.c new file mode 100644 index 0000000..4449a57 --- /dev/null +++ b/tests/unit/io_backend/io_backend.c @@ -0,0 +1,96 @@ +#include +#include +#include +#include + +TestSuite(IO_Backend); + +// IOB Init + +Test(IO_Backend, init_null) +{ + struct iob_context ctx = { .mode = IOB_MODE_NULL, .args = NULL }; + int actual = iob_init(&ctx); + int expected = IOB_ERROR_BAD_ARG; + cr_expect(actual == expected, "Expected: %d. Got: %d", expected, actual); +} + +Test(IO_Backend, init_stdin) +{ + struct iob_context ctx = { .mode = IOB_MODE_STDIN, .args = NULL }; + int actual = iob_init(&ctx); + int expected = 0; + cr_expect(actual == expected, "Expected: %d. Got: %d", expected, actual); + iob_close(); +} + +// WARNING: this one could fail because of iob_close in the previous test +// Same applies for other tests +Test(IO_Backend, init_script) +{ + char *script_name = "script.tmp"; + struct iob_context ctx = { .mode = IOB_MODE_SCRIPT, .args = script_name }; + // Create file + FILE *f = fopen(script_name, "w"); + fclose(f); + + int actual = iob_init(&ctx); + int expected = 0; + cr_expect(actual == expected, "Expected: %d. Got: %d", expected, actual); + iob_close(); + remove(script_name); +} + +Test(IO_Backend, init_script_not_a_file) +{ + char *script_name = "not_a_file.tmp"; + struct iob_context ctx = { .mode = IOB_MODE_SCRIPT, .args = script_name }; + int actual = iob_init(&ctx); + int expected = IOB_ERROR_CANNOT_OPEN_FILE; + cr_expect(actual == expected, "Expected: %d. Got: %d", expected, actual); +} + +Test(IO_Backend, init_script_null) +{ + struct iob_context ctx = { .mode = IOB_MODE_SCRIPT, .args = NULL }; + int actual = iob_init(&ctx); + int expected = IOB_ERROR_CANNOT_OPEN_FILE; + cr_expect(actual == expected, "Expected: %d. Got: %d", expected, actual); +} + +Test(IO_Backend, init_cmd) +{ + char *cmd = "iamacommand --yesido"; + struct iob_context ctx = { .mode = IOB_MODE_CMD, .args = cmd }; + int actual = iob_init(&ctx); + int expected = 0; + cr_expect(actual == expected, "Expected: %d. Got: %d", expected, actual); + iob_close(); +} + +Test(IO_Backend, init_cmd_null) +{ + struct iob_context ctx = { .mode = IOB_MODE_CMD, .args = NULL }; + int actual = iob_init(&ctx); + int expected = IOB_ERROR_BAD_ARG; + cr_expect(actual == expected, "Expected: %d. Got: %d", expected, actual); +} + +Test(IO_Backend, init_already_init) +{ + char *cmd = "iamacommand --yesido"; + struct iob_context ctx = { .mode = IOB_MODE_CMD, .args = cmd }; + iob_init(&ctx); + int actual = iob_init(&ctx); + int expected = IOB_ERROR_MODULE_ALREADY_INITIALIZED; + cr_expect(actual == expected, "Expected: %d. Got: %d", expected, actual); + iob_close(); +} + +Test(IO_Backend, close_not_init) +{ + iob_close(); // Shouldn't do anything +} + +// IOB Stream +// TODO diff --git a/tests/unit/lexer/lexer_tests.c b/tests/unit/lexer/lexer_tests.c new file mode 100644 index 0000000..ecf662f --- /dev/null +++ b/tests/unit/lexer/lexer_tests.c @@ -0,0 +1,62 @@ +#include +#include +#include +#include +#include +#include + +#include "io_backend/io_backend.h" +#include "lexer/lexer.h" + +TestSuite(peek_token); + +Test(peek_token, basic_empty) +{ + // simulates input + char command[] = ""; + struct iob_context context = { IOB_MODE_CMD, command }; + iob_init(&context); + + struct lexer_context ctx = { .end_previous_token = NULL, + .remaining_chars = 0, + .only_digits = false, + .previous_token = NULL, + .current_token = NULL }; + + // test + struct token *tok = peek_token(&ctx); + + // expected + enum token_type type_expected = TOKEN_EOF; + char *string_expected = NULL; + + cr_assert(tok->data == string_expected); + cr_assert(type_expected == tok->type); + + free_token(&tok); +} + +Test(peek_token, basic_word) +{ + // simulates input + char command[] = "hello"; + struct iob_context context = { IOB_MODE_CMD, command }; + iob_init(&context); + + struct lexer_context ctx = { .end_previous_token = NULL, + .remaining_chars = 0, + .only_digits = false, + .previous_token = NULL, + .current_token = NULL }; + + // test + struct token *tok = peek_token(&ctx); + + // expected + enum token_type type_expected = TOKEN_WORD; + char string_expected[] = "hello"; + + cr_assert(eq(str, string_expected, tok->data)); + cr_assert(type_expected == tok->type); + free_token(&tok); +} \ No newline at end of file diff --git a/tests/unit/utils/args.c b/tests/unit/utils/args.c new file mode 100644 index 0000000..9f601bf --- /dev/null +++ b/tests/unit/utils/args.c @@ -0,0 +1,178 @@ +#include "../../../src/utils/args/args.h" + +#include +#include +#include + +#include "../../../src/utils/hash_map/hash_map.h" +#include "../../../src/utils/vars/vars.h" + +TestSuite(utils_args); + +Test(utils_args, basic_command) +{ + int argc = 3; + struct args_options options; + char *input[] = { "program", "-c", "echo Hello, World!" }; + + struct hash_map *vars = vars_init(); + int r = args_handler(argc, input, &options, vars); + + cr_expect(r == 0); + // cr_expect(options.pretty_print == false); + cr_expect(options.verbose == false); + cr_expect(options.type == INPUT_CMD); + cr_expect(eq(options.input_source, "echo Hello, World!")); + hash_map_free(&vars); +} + +Test(utils_args, basic_command_with_flags) +{ + int argc = 5; + struct args_options options; + /* char *input[] = { "program", "--pretty-print", "-c", "echo Hello, + World!", + "--verbose" };*/ + char *input[] = { "program", "-c", "echo Hello, World!", "--verbose" }; + + struct hash_map *vars = vars_init(); + int r = args_handler(argc, input, &options, vars); + + cr_expect(r == 0); + // cr_expect(options.pretty_print == true); + cr_expect(options.verbose == true); + cr_expect(options.type == INPUT_CMD); + cr_expect(eq(options.input_source, "echo Hello, World!")); + hash_map_free(&vars); +} + +Test(utils_args, basic_file_input) +{ + int argc = 2; + struct args_options options; + char *input[] = { "program", "input.txt" }; + + struct hash_map *vars = vars_init(); + int r = args_handler(argc, input, &options, vars); + + cr_expect(r == 0); + // cr_expect(options.pretty_print == false); + cr_expect(options.verbose == false); + cr_expect(options.type == INPUT_FILE); + cr_expect(eq(options.input_source, "input.txt")); + hash_map_free(&vars); +} + +Test(utils_args, basic_file_input_with_flags) +{ + int argc = 4; + struct args_options options; + // char *input[] = { "program", "--verbose", "input.txt", "--pretty-print" + // }; + char *input[] = { "program", "--verbose", "input.txt" }; + + struct hash_map *vars = vars_init(); + int r = args_handler(argc, input, &options, vars); + + cr_expect(r == 0); + // cr_expect(options.pretty_print == true); + cr_expect(options.verbose == true); + cr_expect(options.type == INPUT_FILE); + cr_expect(eq(options.input_source, "input.txt")); + hash_map_free(&vars); +} + +Test(utils_args, basic_stdin_input) +{ + int argc = 1; + struct args_options options; + char *input[] = { "program" }; + + struct hash_map *vars = vars_init(); + int r = args_handler(argc, input, &options, vars); + + cr_expect(r == 0); + // cr_expect(options.pretty_print == false); + cr_expect(options.verbose == false); + cr_expect(options.type == INPUT_STDIN); + cr_expect(options.input_source == NULL); + hash_map_free(&vars); +} + +Test(utils_args, pretty_print_and_verbose_flags) +{ + int argc = 3; + struct args_options options; + // char *input[] = { "program", "--pretty-print", "--verbose" }; + char *input[] = { "program", "--verbose" }; + + struct hash_map *vars = vars_init(); + int r = args_handler(argc, input, &options, vars); + + cr_expect(r == 0); + // cr_expect(options.pretty_print == true); + cr_expect(options.verbose == true); + cr_expect(options.type == INPUT_STDIN); + cr_expect(options.input_source == NULL); + hash_map_free(&vars); +} + +Test(utils_args, missing_command_after_c, .init = cr_redirect_stderr) +{ + int argc = 2; + struct args_options options; + char *input[] = { "program", "-c" }; + + struct hash_map *vars = vars_init(); + int r = args_handler(argc, input, &options, vars); + + cr_expect(r != 0); + cr_assert_stderr_eq_str("No command provided after -c\n"); + hash_map_free(&vars); +} + +Test(utils_args, unknown_option, .init = cr_redirect_stderr) +{ + int argc = 2; + struct args_options options; + char *input[] = { "program", "--unknown" }; + + struct hash_map *vars = vars_init(); + int r = args_handler(argc, input, &options, vars); + + cr_expect(r != 0); + cr_assert_stderr_eq_str("Unknown option: --unknown\n"); + hash_map_free(&vars); +} + +Test(utils_args, multiple_command, .init = cr_redirect_stderr) +{ + int argc = 4; + struct args_options options; + char *input[] = { "program", "exec.sh", "-c", "echo World" }; + + struct hash_map *vars = vars_init(); + int r = args_handler(argc, input, &options, vars); + + cr_expect(r != 0); + cr_assert_stderr_eq_str("Multiple input sources specified: echo World\n"); + hash_map_free(&vars); +} + +Test(utils_args, command_with_additional_arguments) +{ + int argc = 5; + struct args_options options; + char *input[] = { "program", "-c", "echo Hello", "arg1", "arg2" }; + + struct hash_map *vars = vars_init(); + int r = args_handler(argc, input, &options, vars); + + cr_expect(r == 0); + cr_expect(options.type == INPUT_CMD); + cr_expect(eq(options.input_source, "echo Hello")); + cr_expect_str_eq(get_var(vars, "0"), "program"); + cr_expect_str_eq(get_var(vars, "1"), "arg1"); + cr_expect_str_eq(get_var(vars, "2"), "arg2"); + hash_map_free(&vars); +} diff --git a/tests/unit/utils/hash_map.c b/tests/unit/utils/hash_map.c new file mode 100644 index 0000000..3c3bd04 --- /dev/null +++ b/tests/unit/utils/hash_map.c @@ -0,0 +1,217 @@ +#define _POSIX_C_SOURCE 200809L +#include "../../../src/utils/hash_map/hash_map.h" + +#include +#include +#include +#include + +TestSuite(utils_hash_map); + +Test(utils_hash_map, init_free) +{ + struct hash_map *map = hash_map_init(10); + cr_expect_not_null(map); + cr_expect_eq(map->size, 10); + hash_map_free(&map); +} + +Test(utils_hash_map, insert_basic) +{ + struct hash_map *map = hash_map_init(10); + cr_expect_not_null(map); + cr_expect_eq(map->size, 10); + + bool updated = false; + bool res = hash_map_insert(map, strdup("key1"), strdup("value1"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, false); + + hash_map_free(&map); +} + +Test(utils_hash_map, insert_multiple) +{ + struct hash_map *map = hash_map_init(10); + cr_expect_not_null(map); + cr_expect_eq(map->size, 10); + + bool updated = false; + bool res = hash_map_insert(map, strdup("key1"), strdup("value1"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, false); + res = hash_map_insert(map, strdup("key2"), strdup("value2"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, false); + + hash_map_free(&map); +} + +Test(utils_hash_map, insert_update) +{ + struct hash_map *map = hash_map_init(10); + cr_expect_not_null(map); + cr_expect_eq(map->size, 10); + + bool updated = false; + bool res = hash_map_insert(map, strdup("key1"), strdup("value1"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, false); + res = hash_map_insert(map, "key1", strdup("value2"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, true); + + hash_map_free(&map); +} + +Test(utils_hash_map, insert_update_multiple) +{ + struct hash_map *map = hash_map_init(10); + cr_expect_not_null(map); + cr_expect_eq(map->size, 10); + + bool updated = false; + bool res = hash_map_insert(map, strdup("key1"), strdup("value1"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, false); + res = hash_map_insert(map, strdup("key2"), strdup("value2"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, false); + res = hash_map_insert(map, "key1", strdup("value2"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, true); + res = hash_map_insert(map, "key1", strdup("value3"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, true); + + hash_map_free(&map); +} + +Test(utils_hash_map, get_basic) +{ + struct hash_map *map = hash_map_init(10); + cr_expect_not_null(map); + cr_expect_eq(map->size, 10); + + bool updated = false; + bool res = hash_map_insert(map, strdup("key1"), strdup("value1"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, false); + + char *value = (char *)hash_map_get(map, "key1"); + cr_expect_str_eq(value, "value1"); + + hash_map_free(&map); +} + +Test(utils_hash_map, get_after_update) +{ + struct hash_map *map = hash_map_init(10); + cr_expect_not_null(map); + cr_expect_eq(map->size, 10); + + bool updated = false; + bool res = hash_map_insert(map, strdup("key1"), strdup("value1"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, false); + + res = hash_map_insert(map, "key1", strdup("value2"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, true); + + char *value = (char *)hash_map_get(map, "key1"); + cr_expect_str_eq(value, "value2"); + + hash_map_free(&map); +} + +Test(utils_hash_map, get_unknown_key) +{ + struct hash_map *map = hash_map_init(10); + cr_expect_not_null(map); + cr_expect_eq(map->size, 10); + + char *value = (char *)hash_map_get(map, "unknown_key"); + cr_expect_null(value); + + hash_map_free(&map); +} + +Test(utils_hash_map, delete_key) +{ + struct hash_map *map = hash_map_init(10); + cr_expect_not_null(map); + cr_expect_eq(map->size, 10); + + bool updated = false; + bool res = hash_map_insert(map, strdup("key1"), strdup("value1"), &updated); + cr_expect_eq(res, true); + cr_expect_eq(updated, false); + + res = hash_map_remove(map, "key1"); + cr_expect_eq(res, true); + + char *value = (char *)hash_map_get(map, "key1"); + cr_expect_null(value); + + hash_map_free(&map); +} + +Test(utils_hash_map, delete_unknown_key) +{ + struct hash_map *map = hash_map_init(10); + cr_expect_not_null(map); + cr_expect_eq(map->size, 10); + + bool res = hash_map_remove(map, "unknown_key"); + cr_expect_eq(res, false); + + hash_map_free(&map); +} + +Test(utils_hash_map, free_nonnull_map) +{ + struct hash_map *map = hash_map_init(10); + cr_expect_not_null(map); + cr_expect_eq(map->size, 10); + + bool res = hash_map_insert(map, strdup("key1"), strdup("value1"), NULL); + cr_expect_eq(res, true); + res = hash_map_insert(map, strdup("key2"), strdup("value2"), NULL); + cr_expect_eq(res, true); + + hash_map_free(&map); +} + +Test(utils_hash_map, free_null_map) +{ + hash_map_free(NULL); +} + +static size_t count = 0; +void foreach_fn(const char *key, const void *value) +{ + printf("Key: %s, Value: %s\n", key, (const char *)value); + count++; +} + +Test(utils_hash_map, foreach, .init = cr_redirect_stdout) +{ + struct hash_map *map = hash_map_init(10); + cr_expect_not_null(map); + cr_expect_eq(map->size, 10); + + bool res = hash_map_insert(map, strdup("key1"), strdup("value1"), NULL); + cr_expect_eq(res, true); + res = hash_map_insert(map, strdup("key2"), strdup("value2"), NULL); + cr_expect_eq(res, true); + + count = 0; + hash_map_foreach(map, foreach_fn); + fflush(stdout); + cr_expect_eq(count, 2); + cr_expect_stdout_eq_str( + "Key: key2, Value: value2\nKey: key1, Value: value1\n"); + + hash_map_free(&map); +} diff --git a/tests/unit/utils/insert_into.c b/tests/unit/utils/insert_into.c new file mode 100644 index 0000000..0bcc833 --- /dev/null +++ b/tests/unit/utils/insert_into.c @@ -0,0 +1,85 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include + +#include "../../../src/utils/string_utils/string_utils.h" + +TestSuite(insert_into); + +Test(insert_into, basic) +{ + char *dest = strdup("The is nice."); + const char *src = "weather"; + size_t pos = 4; + + char *result = insert_into(dest, src, pos, 6); + + cr_expect(result != NULL); + cr_expect(eq(str, result, "The weather is nice.")); + + if (result) + free(result); +} + +Test(insert_into, begin) +{ + char *dest = strdup("Hello World!"); + const char *src = "Hi"; + size_t pos = 0; + + char *result = insert_into(dest, src, pos, 5); + + cr_expect(result != NULL); + cr_expect(eq(str, result, "Hi World!")); + + if (result) + free(result); +} + +Test(insert_into, end) +{ + char *dest = strdup("The number is 1024"); + const char *src = "2048"; + size_t pos = 14; + + char *result = insert_into(dest, src, pos, 4); + + cr_expect(result != NULL); + cr_expect(eq(str, result, "The number is 2048")); + + if (result) + free(result); +} + +Test(insert_into, big) +{ + char *dest = strdup("I could insert [VAR] here."); + const char *src = "a very very long string"; + size_t pos = 15; + + char *result = insert_into(dest, src, pos, 5); + + cr_expect(result != NULL); + cr_expect(eq(str, result, "I could insert a very very long string here.")); + + if (result) + free(result); +} + +Test(insert_into, small) +{ + char *dest = strdup("I could insert [VARNAME_IS_SO_LONG] string here."); + const char *src = "a short"; + size_t pos = 15; + + char *result = insert_into(dest, src, pos, 20); + + cr_expect(result != NULL); + cr_expect(eq(str, result, "I could insert a short string here.")); + + if (result) + free(result); +} diff --git a/tests/unit/utils/lists.c b/tests/unit/utils/lists.c new file mode 100644 index 0000000..c852803 --- /dev/null +++ b/tests/unit/utils/lists.c @@ -0,0 +1,305 @@ +#define _POSIX_C_SOURCE 200809L + +#include "../../../src/utils/lists/lists.h" + +#include +#include +#include +#include +#include +#include +#include + +TestSuite(lists); + +Test(lists, append_empty) +{ + struct list *lst = NULL; + lst = list_append(lst, (void *)1); + cr_expect(lst != NULL); + cr_expect(lst->data == (void *)1); + cr_expect(lst->next == NULL); + cr_expect(list_length(lst) == 1); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, prepend_empty) +{ + struct list *lst = NULL; + lst = list_prepend(lst, (void *)1); + cr_expect(lst != NULL); + cr_expect(lst->data == (void *)1); + cr_expect(lst->next == NULL); + cr_expect(list_length(lst) == 1); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, insert_empty) +{ + struct list *lst = NULL; + lst = list_insert(lst, (void *)1, 0); + cr_expect(lst != NULL); + cr_expect(lst->data == (void *)1); + cr_expect(lst->next == NULL); + cr_expect(list_length(lst) == 1); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, insert_out_of_bounds) +{ + struct list *lst = NULL; + lst = list_insert(lst, (void *)1, 5); + cr_expect(lst != NULL); + cr_expect(lst->data == (void *)1); + cr_expect(lst->next == NULL); + cr_expect(list_length(lst) == 1); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, remove_out_of_bounds) +{ + struct list *lst = NULL; + lst = list_append(lst, (void *)1); + lst = list_remove(lst, 5); + cr_expect(lst != NULL); + cr_expect(lst->data == (void *)1); + cr_expect(lst->next == NULL); + cr_expect(list_length(lst) == 1); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, append_multiple) +{ + struct list *lst = NULL; + lst = list_append(lst, (void *)1); + lst = list_append(lst, (void *)2); + lst = list_append(lst, (void *)3); + cr_expect(lst != NULL); + cr_expect(lst->data == (void *)1); + cr_expect(lst->next->data == (void *)2); + cr_expect(lst->next->next->data == (void *)3); + cr_expect(lst->next->next->next == NULL); + cr_expect(list_length(lst) == 3); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, prepend_multiple) +{ + struct list *lst = NULL; + lst = list_prepend(lst, (void *)1); + lst = list_prepend(lst, (void *)2); + lst = list_prepend(lst, (void *)3); + cr_expect(lst != NULL); + cr_expect(lst->data == (void *)3); + cr_expect(lst->next->data == (void *)2); + cr_expect(lst->next->next->data == (void *)1); + cr_expect(lst->next->next->next == NULL); + cr_expect(list_length(lst) == 3); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, insert_multiple) +{ + struct list *lst = NULL; + lst = list_insert(lst, (void *)1, 0); + lst = list_insert(lst, (void *)3, 1); + lst = list_insert(lst, (void *)2, 1); + cr_expect(lst != NULL); + cr_expect(lst->data == (void *)1); + cr_expect(lst->next->data == (void *)2); + cr_expect(lst->next->next->data == (void *)3); + cr_expect(lst->next->next->next == NULL); + cr_expect(list_length(lst) == 3); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, append) +{ + struct list *lst = NULL; + lst = list_prepend(lst, (void *)2); + lst = list_prepend(lst, (void *)1); + + lst = list_append(lst, (void *)3); + + cr_expect(lst != NULL); + cr_expect(lst->data == (void *)1); + cr_expect(lst->next->data == (void *)2); + cr_expect(lst->next->next->data == (void *)3); + cr_expect(lst->next->next->next == NULL); + cr_expect(list_length(lst) == 3); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, prepend) +{ + struct list *lst = NULL; + lst = list_append(lst, (void *)1); + lst = list_append(lst, (void *)2); + + lst = list_prepend(lst, (void *)0); + + cr_expect(lst != NULL); + cr_expect(lst->data == (void *)0); + cr_expect(lst->next->data == (void *)1); + cr_expect(lst->next->next->data == (void *)2); + cr_expect(lst->next->next->next == NULL); + cr_expect(list_length(lst) == 3); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, insert) +{ + struct list *lst = NULL; + lst = list_append(lst, (void *)1); + lst = list_append(lst, (void *)3); + + lst = list_insert(lst, (void *)2, 1); + lst = list_insert(lst, (void *)0, 0); + + cr_expect(lst != NULL); + cr_expect(lst->data == (void *)0); + cr_expect(lst->next->data == (void *)1); + cr_expect(lst->next->next->data == (void *)2); + cr_expect(lst->next->next->next->data == (void *)3); + cr_expect(lst->next->next->next->next == NULL); + cr_expect(list_length(lst) == 4); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, remove) +{ + struct list *lst = NULL; + lst = list_append(lst, (void *)1); + lst = list_append(lst, (void *)2); + lst = list_append(lst, (void *)3); + lst = list_append(lst, (void *)4); + + lst = list_remove(lst, 1); // remove 2 + lst = list_remove(lst, 2); // remove 4 + lst = list_remove(lst, 0); // remove 1 + + cr_expect(lst != NULL); + cr_expect(lst->data == (void *)3); + cr_expect(lst->next == NULL); + cr_expect(list_length(lst) == 1); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, destroy_null) +{ + struct list *lst = NULL; + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, deep_destroy_null) +{ + struct list *lst = NULL; + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, deep_destroy) +{ + struct list *lst = NULL; + lst = list_append(lst, strdup("string1")); + lst = list_append(lst, strdup("string2")); + lst = list_append(lst, strdup("string3")); + + list_deep_destroy(lst); +} + +Test(lists, length_empty) +{ + struct list *lst = NULL; + cr_expect(list_length(lst) == 0); +} + +Test(lists, print_empty, .init = cr_redirect_stdout) +{ + struct list *lst = NULL; + list_print(lst); + cr_expect_stdout_eq_str(""); +} + +Test(lists, print_non_empty, .init = cr_redirect_stdout) +{ + struct list *lst = NULL; + lst = list_append(lst, (void *)1); + lst = list_append(lst, (void *)2); + lst = list_append(lst, (void *)3); + + list_print(lst); + fflush(stdout); + cr_expect_stdout_eq_str("0x1 0x2 0x3\n"); + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +Test(lists, find_empty) +{ + struct list *lst = NULL; + cr_expect(list_find(lst, (void *)1) == -1); +} + +Test(lists, find_non_empty) +{ + struct list *lst = NULL; + lst = list_append(lst, (void *)1); + lst = list_append(lst, (void *)2); + lst = list_append(lst, (void *)3); + + cr_expect(list_find(lst, (void *)1) == 0); + cr_expect(list_find(lst, (void *)2) == 1); + cr_expect(list_find(lst, (void *)3) == 2); + cr_expect(list_find(lst, (void *)4) == -1); // not found + + list_destroy(&lst); + cr_expect(lst == NULL); +} + +static void fold_func(void *acc, void *data) +{ + *(int *)acc += *(int *)data; +} + +Test(lists, fold) +{ + struct list *lst = NULL; + int v1 = 10, v2 = 20, v3 = 30; + lst = list_append(lst, &v1); + lst = list_append(lst, &v2); + lst = list_append(lst, &v3); + + int sum = 0; + list_fold(lst, &sum, fold_func); + cr_expect(sum == 60); + + list_destroy(&lst); + cr_expect(lst == NULL); +} diff --git a/tests/unit/utils/utils_tests.c b/tests/unit/utils/utils_tests.c new file mode 100644 index 0000000..b7c326f --- /dev/null +++ b/tests/unit/utils/utils_tests.c @@ -0,0 +1,105 @@ +#include +#include +#include +#include +#include +#include + +#include "../../../src/utils/string_utils/string_utils.h" + +TestSuite(string_utils); + +Test(string_utils, skipblank_basic) +{ + char input[] = " Hello World"; + char expected_str[] = "Hello World"; + + char *trimmed = trim_blank_left(input); + ssize_t offset = trimmed - input; + ssize_t expected = 2; + cr_expect(eq(str, trimmed, expected_str)); + cr_expect(offset == expected); +} + +Test(string_utils, skipblank_noblank) +{ + char input[] = "Hello World"; + char expected_str[] = "Hello World"; + + char *trimmed = trim_blank_left(input); + ssize_t offset = trimmed - input; + ssize_t expected = 0; + cr_expect(eq(str, trimmed, expected_str)); + cr_expect(offset == expected); +} + +Test(string_utils, skipblank_tab) +{ + char input[] = "\tHello World"; + char expected_str[] = "Hello World"; + + char *trimmed = trim_blank_left(input); + ssize_t offset = trimmed - input; + ssize_t expected = 1; + cr_expect(eq(str, trimmed, expected_str)); + cr_expect(offset == expected); +} + +Test(string_utils, skipblank_space_tab) +{ + char input[] = " \tHello World"; + char expected_str[] = "Hello World"; + + char *trimmed = trim_blank_left(input); + ssize_t offset = trimmed - input; + ssize_t expected = 2; + cr_expect(eq(str, trimmed, expected_str)); + cr_expect(offset == expected); +} + +Test(string_utils, skipblank_2tab_1space) +{ + char input[] = "\t \tHello World"; + char expected_str[] = "Hello World"; + + char *trimmed = trim_blank_left(input); + ssize_t offset = trimmed - input; + ssize_t expected = 3; + cr_expect(eq(str, trimmed, expected_str)); + cr_expect(offset == expected); +} + +Test(string_utils, skipblank_a_lot) +{ + char input[] = "\t \t \tHello World"; + char expected_str[] = "Hello World"; + + char *trimmed = trim_blank_left(input); + ssize_t offset = trimmed - input; + ssize_t expected = 8; + cr_expect(eq(str, trimmed, expected_str)); + cr_expect(offset == expected); +} + +Test(string_utils, skipblank_newline) +{ + char input[] = "\nHello World"; + char expected_str[] = "\nHello World"; + + char *trimmed = trim_blank_left(input); + ssize_t offset = trimmed - input; + ssize_t expected = 0; + cr_expect(eq(str, trimmed, expected_str)); + cr_expect(offset == expected); +} + +Test(string_utils, skipblank_nul) +{ + char *input = NULL; + + char *trimmed = trim_blank_left(input); + ssize_t offset = trimmed - input; + ssize_t expected = 0; + cr_expect(input == NULL); + cr_expect(offset == expected); +} diff --git a/tests/unit/utils/vars.c b/tests/unit/utils/vars.c new file mode 100644 index 0000000..73784cf --- /dev/null +++ b/tests/unit/utils/vars.c @@ -0,0 +1,85 @@ +#define _POSIX_C_SOURCE 200809L +#include "../../../src/utils/vars/vars.h" + +#include +#include +#include +#include +#include + +#include "../../../src/utils/hash_map/hash_map.h" +#include "../../../src/utils/string_utils/string_utils.h" + +TestSuite(utils_vars); + +Test(utils_vars, init_free) +{ + struct hash_map *map = vars_init(); + cr_expect_not_null(map); + hash_map_free(&map); +} + +Test(utils_vars, get_defaults) +{ + struct hash_map *map = vars_init(); + cr_expect_not_null(map); + cr_assert_str_eq(get_var(map, "?"), "0"); + char int_str[11]; + int_to_str((int)getpid(), int_str); + cr_assert_str_eq(get_var(map, "$"), int_str); + int_to_str((int)getuid(), int_str); + cr_assert_str_eq(get_var(map, "UID"), int_str); + + hash_map_free(&map); +} + +Test(utils_vars, set_vars) +{ + struct hash_map *map = vars_init(); + cr_expect_not_null(map); + set_var_copy(map, "key1", "value1"); + cr_assert_str_eq(get_var(map, "key1"), "value1"); + set_var_copy(map, "key2", "value2"); + cr_assert_str_eq(get_var(map, "key2"), "value2"); + + hash_map_free(&map); +} + +Test(utils_vars, get_env_vars) +{ + struct hash_map *map = vars_init(); + cr_expect_not_null(map); + cr_assert_eq(get_var_or_env(map, "ENV_TEST"), NULL); + setenv("ENV_TEST", "value1", 0); + cr_assert_str_eq(get_var_or_env(map, "ENV_TEST"), "value1"); + setenv("ENV_TEST", "value2", 1); + cr_assert_str_eq(get_var_or_env(map, "ENV_TEST"), "value2"); + + hash_map_free(&map); +} + +Test(utils_vars, set_vars_update) +{ + struct hash_map *map = vars_init(); + cr_expect_not_null(map); + set_var_copy(map, "key", "value1"); + cr_assert_str_eq(get_var(map, "key"), "value1"); + set_var_copy(map, "key", "value2"); + cr_assert_str_eq(get_var(map, "key"), "value2"); + + hash_map_free(&map); +} + +Test(utils_vars, set_vars_int) +{ + struct hash_map *map = vars_init(); + cr_expect_not_null(map); + set_var_int(map, "key1", 100); + cr_assert_str_eq(get_var(map, "key1"), "100"); + set_var_int(map, "key1", 200); + cr_assert_str_eq(get_var(map, "key1"), "200"); + set_var_int(map, "key2", 10); + cr_assert_str_eq(get_var(map, "key2"), "10"); + + hash_map_free(&map); +} diff --git a/tests/wrap.sh b/tests/wrap.sh new file mode 100755 index 0000000..58519d3 --- /dev/null +++ b/tests/wrap.sh @@ -0,0 +1,12 @@ +#!/bin/sh + +if [ "$BIN_PATH" = "" ]; +then export BIN_PATH="$(pwd)/42sh" +fi + +if [ "$COVERAGE" = "yes" ]; #coverage +then (./testsuite || true) && ../tests/functional/run-tests.sh +else ../tests/functional/run-tests.sh +fi +echo bin path: "$BIN_PATH" +echo output file: "$OUTPUT_FILE"