diff --git a/.clang-tidy b/.clang-tidy index 0269e54..828ae5a 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,20 +1,20 @@ -Checks: > - bugprone-*, - clang-analyzer-*, - performance-*, - portability-*, - readability-*, - -readability-identifier-length, - -readability-magic-numbers, - -readability-isolate-declaration, - -readability-function-cognitive-complexity, - -bugprone-multi-level-implicit-pointer-conversion, - -bugprone-easily-swappable-parameters, - -bugprone-branch-clone, - -bugprone-implicit-widening-of-multiplication-result, - -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, - -clang-analyzer-core.NonNullParamChecker, - -clang-analyzer-security.ArrayBound, - -clang-analyzer-unix.StdCLibraryFunctions, - -clang-analyzer-core.NullDereference -WarningsAsErrors: '' +Checks: > + bugprone-*, + clang-analyzer-*, + performance-*, + portability-*, + readability-*, + -readability-identifier-length, + -readability-magic-numbers, + -readability-isolate-declaration, + -readability-function-cognitive-complexity, + -bugprone-multi-level-implicit-pointer-conversion, + -bugprone-easily-swappable-parameters, + -bugprone-branch-clone, + -bugprone-implicit-widening-of-multiplication-result, + -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, + -clang-analyzer-core.NonNullParamChecker, + -clang-analyzer-security.ArrayBound, + -clang-analyzer-unix.StdCLibraryFunctions, + -clang-analyzer-core.NullDereference +WarningsAsErrors: '' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 641f661..0efce80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,22 @@ jobs: - name: Build and test run: ./run_tests.sh + sanitize: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install cmake + run: | + sudo apt-get update + sudo apt-get install -y cmake + - name: Build and test with sanitizers + env: + ASAN_OPTIONS: detect_leaks=1 + run: | + cmake -B cmake-build-debug -S . -DTRIGGER_SANITIZE=ON + cmake --build cmake-build-debug + cd cmake-build-debug && ctest --output-on-failure + lint: runs-on: ubuntu-latest steps: diff --git a/CMakeLists.txt b/CMakeLists.txt index 21e6eb7..756d0ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,23 +25,46 @@ set(HEADERS include/glob_expand.h ) -add_executable(shell src/main.c ${LIB_SOURCES} ${UTIL_SOURCES} ${HEADERS}) +add_library(trigger_core STATIC ${LIB_SOURCES} ${UTIL_SOURCES}) + +option(TRIGGER_SANITIZE "Build with address and undefined behavior sanitizers" OFF) + +if(TRIGGER_SANITIZE) + add_compile_options(-fsanitize=address,undefined -fno-omit-frame-pointer -g) + add_link_options(-fsanitize=address,undefined) +endif() + +target_compile_options(trigger_core PRIVATE -Wall -Wextra -Werror) + +add_executable(shell src/main.c) +target_link_libraries(shell trigger_core) +target_compile_options(shell PRIVATE -Wall -Wextra -Werror) enable_testing() -add_executable(test_input tests/test_input.c src/input.c ${UTIL_SOURCES}) +add_executable(test_input tests/test_input.c) +target_link_libraries(test_input trigger_core) +target_compile_options(test_input PRIVATE -Wall -Wextra -Werror) add_test(NAME InputTests COMMAND test_input) -add_executable(test_builtins tests/test_builtins.c src/builtins.c src/input.c ${UTIL_SOURCES}) +add_executable(test_builtins tests/test_builtins.c) +target_link_libraries(test_builtins trigger_core) +target_compile_options(test_builtins PRIVATE -Wall -Wextra -Werror) add_test(NAME BuiltinsTests COMMAND test_builtins) -add_executable(test_execute tests/test_execute.c src/execute.c src/builtins.c src/pipeline.c src/glob.c src/input.c ${UTIL_SOURCES}) +add_executable(test_execute tests/test_execute.c) +target_link_libraries(test_execute trigger_core) +target_compile_options(test_execute PRIVATE -Wall -Wextra -Werror) add_test(NAME ExecuteTests COMMAND test_execute) -add_executable(test_pipeline tests/test_pipeline.c src/pipeline.c src/execute.c src/builtins.c src/glob.c src/input.c ${UTIL_SOURCES}) +add_executable(test_pipeline tests/test_pipeline.c) +target_link_libraries(test_pipeline trigger_core) +target_compile_options(test_pipeline PRIVATE -Wall -Wextra -Werror) add_test(NAME PipelineTests COMMAND test_pipeline) -add_executable(test_glob tests/test_glob.c src/glob.c src/input.c ${UTIL_SOURCES}) +add_executable(test_glob tests/test_glob.c) +target_link_libraries(test_glob trigger_core) +target_compile_options(test_glob PRIVATE -Wall -Wextra -Werror) add_test(NAME GlobTests COMMAND test_glob) add_custom_target(run_tests diff --git a/include/builtins.h b/include/builtins.h index 8242cab..120525b 100644 --- a/include/builtins.h +++ b/include/builtins.h @@ -1,16 +1,20 @@ -#ifndef BUILTINS_H -#define BUILTINS_H - -int trigger_cd(char **args); -int trigger_help(char **args); -int trigger_exit(char **args); -int trigger_pwd(char **args); -int trigger_echo(char **args); -int trigger_export(char **args); -int trigger_unset(char **args); -int trigger_num_builtins(void); - -extern char *builtin_str[]; -extern int (*builtin_func[])(char **); - -#endif +#ifndef BUILTINS_H +#define BUILTINS_H + +typedef struct { + const char *name; + int (*fn)(char **); +} Builtin; + +int trigger_cd(char **args); +int trigger_help(char **args); +int trigger_exit(char **args); +int trigger_pwd(char **args); +int trigger_echo(char **args); +int trigger_export(char **args); +int trigger_unset(char **args); + +int trigger_num_builtins(void); +const Builtin *find_builtin(const char *name); + +#endif diff --git a/include/execute.h b/include/execute.h index 14b16b2..6682817 100644 --- a/include/execute.h +++ b/include/execute.h @@ -1,10 +1,16 @@ -#ifndef EXECUTE_H -#define EXECUTE_H - -#define CHILD_PROCESS_EXITED 0 -#define EXEC_RETURNED_FAILURE (-1) - -int trigger_launch(char **args); -int trigger_execute(char ***args_ptr, int **glob_eligible_ptr); - -#endif +#ifndef EXECUTE_H +#define EXECUTE_H + +#include + +typedef struct TokenList TokenList; + +typedef struct { + int status; + bool should_exit; +} ExecuteResult; + +int trigger_launch(char **args); +ExecuteResult trigger_execute(TokenList *tl); + +#endif diff --git a/include/glob_expand.h b/include/glob_expand.h index b490e8a..cae8c17 100644 --- a/include/glob_expand.h +++ b/include/glob_expand.h @@ -1,6 +1,8 @@ -#ifndef GLOB_EXPAND_H -#define GLOB_EXPAND_H - -char **expand_globs(char **argv, int **glob_eligible_ptr); - -#endif +#ifndef GLOB_EXPAND_H +#define GLOB_EXPAND_H + +typedef struct TokenList TokenList; + +void expand_globs(TokenList *tl); + +#endif diff --git a/include/input.h b/include/input.h index 67dfa2a..d17ce9f 100644 --- a/include/input.h +++ b/include/input.h @@ -1,12 +1,8 @@ -#ifndef INPUT_H -#define INPUT_H - -#define TRIGGER_AUTOMATIC_BUFFER_SIZE 0 -#define GET_LINE_REACHED_END_OF_FILE_OR_ERROR (-1) -#define TRIGGER_TOK_DELIM " \t\r\n\a" - -char *trigger_read_line(void); -char **trigger_split_line(const char *line); -char **trigger_split_line_ex(const char *line, int **out_glob_eligible); - -#endif +#ifndef INPUT_H +#define INPUT_H + +char *trigger_read_line(void); +char **trigger_split_line(const char *line); +char **trigger_split_line_ex(const char *line, int **out_glob_eligible); + +#endif diff --git a/include/pipeline.h b/include/pipeline.h index 7d2105d..5391880 100644 --- a/include/pipeline.h +++ b/include/pipeline.h @@ -1,14 +1,25 @@ -#ifndef PIPELINE_H -#define PIPELINE_H - -typedef struct { - char **argv; - char *infile; - char *outfile; - int append; -} PipelineStage; - -PipelineStage *trigger_parse_pipeline(char **tokens, int *num_stages); -int trigger_execute_pipeline(PipelineStage *stages, int num_stages); - -#endif +#ifndef PIPELINE_H +#define PIPELINE_H + +#define FILE_MODE 0644 + +typedef enum { OP_NONE, OP_PIPE, OP_REDIR_IN, OP_REDIR_OUT, OP_REDIR_APPEND } Operator; + +typedef struct { + char **argv; + char *infile; + char *outfile; + int append; +} PipelineStage; + +typedef struct { + int last_status; +} PipelineResult; + +Operator classify_operator(const char *token); + +PipelineStage *trigger_parse_pipeline(char **tokens, const int *glob_eligible, int *num_stages); +PipelineResult trigger_execute_pipeline(PipelineStage *stages, int num_stages); +void trigger_free_pipeline(PipelineStage *stages, int num_stages); + +#endif diff --git a/src/builtins.c b/src/builtins.c index 26f5265..3fc650f 100644 --- a/src/builtins.c +++ b/src/builtins.c @@ -1,91 +1,116 @@ -#include "builtins.h" -#include "utils/utils.h" -#include -#include -#include -#include - -char *builtin_str[] = {"cd", "help", "exit", "pwd", "echo", "export", "unset"}; - -int (*builtin_func[])(char **) = {&trigger_cd, &trigger_help, &trigger_exit, &trigger_pwd, - &trigger_echo, &trigger_export, &trigger_unset}; - -int trigger_num_builtins(void) { return sizeof(builtin_str) / sizeof(char *); } - -int trigger_cd(char **args) { - if (args[1] == NULL) { - fprintf(stderr, "trigger: expected argument to \"cd\"\n"); - } else { - if (chdir(args[1]) != EXIT_SUCCESS) { - perror("trigger"); - } - } - - return true; -} - -int trigger_help(char **args) { - printf("Trigger: A simple shell written in C\n"); - printf("Type program names and arguments, and hit enter.\n"); - printf("The following are built in:\n"); - - for (int i = 0; i < trigger_num_builtins(); i++) { - printf(" %s\n", builtin_str[i]); - } - - printf("Use the man command for information on other programs.\n"); - return true; -} - -int trigger_exit(char **args) { return EXIT_SUCCESS; } - -int trigger_pwd(char **args) { - char cwd[4096]; - - if (getcwd(cwd, sizeof(cwd)) == NULL) { - perror("trigger"); - return true; - } - - printf("%s\n", cwd); - return true; -} - -int trigger_echo(char **args) { - for (int i = 1; args[i] != NULL; i++) { - if (i > 1) { - printf(" "); - } - printf("%s", args[i]); - } - printf("\n"); - return true; -} - -int trigger_export(char **args) { - for (int i = 1; args[i] != NULL; i++) { - char *equals = strchr(args[i], '='); - - if (equals == NULL) { - continue; - } - - char *name = strndup(args[i], equals - args[i]); - - if (name == NULL) { - perror("trigger"); - continue; - } - - setenv(name, equals + 1, 1); - free(name); - } - return true; -} - -int trigger_unset(char **args) { - for (int i = 1; args[i] != NULL; i++) { - unsetenv(args[i]); - } - return true; -} +#include "builtins.h" +#include "utils/utils.h" +#include +#include +#include +#include + +static const Builtin builtins[] = { + {"cd", trigger_cd}, {"help", trigger_help}, {"exit", trigger_exit}, + {"pwd", trigger_pwd}, {"echo", trigger_echo}, {"export", trigger_export}, + {"unset", trigger_unset}, +}; + +static const int builtin_count = sizeof(builtins) / sizeof(builtins[0]); + +int trigger_num_builtins(void) { return builtin_count; } + +const Builtin *find_builtin(const char *name) { + for (int i = 0; i < builtin_count; i++) { + if (strcmp(builtins[i].name, name) == 0) { + return &builtins[i]; + } + } + return NULL; +} + +int trigger_cd(char **args) { + if (args[1] == NULL) { + fprintf(stderr, "trigger: expected argument to \"cd\"\n"); + return 1; + } + if (chdir(args[1]) != 0) { + perror("trigger"); + return 1; + } + + return 0; +} + +int trigger_help(char **args __attribute__((unused))) { + printf("Trigger: A simple shell written in C\n"); + printf("Type program names and arguments, and hit enter.\n"); + printf("The following are built in:\n"); + + for (int i = 0; i < builtin_count; i++) { + printf(" %s\n", builtins[i].name); + } + + printf("Use the man command for information on other programs.\n"); + return 0; +} + +int trigger_exit(char **args) { + if (args[1] != NULL) { + char *endptr; + long n = strtol(args[1], &endptr, 10); + if (*endptr == '\0' && n >= 0 && n <= 255) { + return (int) n; + } + fprintf(stderr, "trigger: exit: %s: numeric argument required\n", args[1]); + return 2; + } + return EXIT_SUCCESS; +} + +int trigger_pwd(char **args __attribute__((unused))) { + char *cwd = getcwd(NULL, 0); + + if (cwd == NULL) { + perror("trigger"); + return 1; + } + + printf("%s\n", cwd); + free(cwd); + return 0; +} + +int trigger_echo(char **args) { + for (int i = 1; args[i] != NULL; i++) { + if (i > 1) { + printf(" "); + } + printf("%s", args[i]); + } + printf("\n"); + return 0; +} + +int trigger_export(char **args) { + for (int i = 1; args[i] != NULL; i++) { + char *equals = strchr(args[i], '='); + + if (equals == NULL) { + continue; + } + + char *name = strndup(args[i], equals - args[i]); + + if (name == NULL) { + perror("trigger"); + continue; + } + + setenv(name, equals + 1, 1); + free(name); + } + return 0; +} + +int trigger_unset(char **args) { + for (int i = 1; args[i] != NULL; i++) { + unsetenv(args[i]); + } + return 0; +} diff --git a/src/execute.c b/src/execute.c index 8bcc880..446f905 100644 --- a/src/execute.c +++ b/src/execute.c @@ -1,75 +1,101 @@ -#include "execute.h" -#include "builtins.h" -#include "glob_expand.h" -#include "pipeline.h" -#include "utils/utils.h" -#include -#include -#include -#include -#include - -int trigger_launch(char **args) { - int status; - - const pid_t pid = fork(); - - if (pid == CHILD_PROCESS_EXITED) { - if (execvp(args[0], args) == EXEC_RETURNED_FAILURE) { - perror("trigger"); - } - exit(EXIT_FAILURE); - } - - if (pid < 0) { - perror("trigger"); - } else { - do { - waitpid(pid, &status, WUNTRACED); - } while (!WIFEXITED(status) && !WIFSIGNALED(status)); - } - - return true; -} - -int trigger_execute(char ***args_ptr, int **glob_eligible_ptr) { - char **args = *args_ptr; - - if (args == NULL || args[0] == NULL) { - return true; - } - - args = expand_globs(args, glob_eligible_ptr); - *args_ptr = args; - int *glob_eligible = (glob_eligible_ptr != NULL) ? *glob_eligible_ptr : NULL; - - int has_operator = 0; - - for (int i = 0; args[i] != NULL; i++) { - if (glob_eligible == NULL || glob_eligible[i]) { - if (strcmp(args[i], "|") == 0 || strcmp(args[i], "<") == 0 || - strcmp(args[i], ">") == 0 || strcmp(args[i], ">>") == 0) { - has_operator = 1; - break; - } - } - } - - if (has_operator) { - int num_stages = 0; - PipelineStage *stages = trigger_parse_pipeline(args, &num_stages); - int result = trigger_execute_pipeline(stages, num_stages); - - free(args); - *args_ptr = NULL; - return result; - } - - for (int i = 0; i < trigger_num_builtins(); i++) { - if (strcmp(args[0], builtin_str[i]) == 0) { - return (*builtin_func[i])(args); - } - } - - return trigger_launch(args); -} +#include "execute.h" +#include "builtins.h" +#include "glob_expand.h" +#include "pipeline.h" +#include "utils/utils.h" +#include +#include +#include +#include +#include +#include +#include + +int trigger_launch(char **args) { + int status = 0; + + const pid_t pid = fork(); + + if (pid == 0) { + signal(SIGINT, SIG_DFL); + signal(SIGQUIT, SIG_DFL); + if (execvp(args[0], args) == -1) { + perror("trigger"); + } + _exit(127); + } + + if (pid < 0) { + perror("trigger"); + return 1; + } + + int w; + do { + w = waitpid(pid, &status, 0); + } while (w == -1 && errno == EINTR); + + if (w == -1) { + perror("trigger"); + return 1; + } + + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } + + if (WIFSIGNALED(status)) { + return 128 + WTERMSIG(status); + } + + return 1; +} + +ExecuteResult trigger_execute(TokenList *tl) { + ExecuteResult result = {0, false}; + + if (tl == NULL || tl->argv == NULL || tl->argv[0] == NULL) { + result.status = 0; + return result; + } + + expand_globs(tl); + + int has_operator = 0; + + for (size_t i = 0; tl->argv[i] != NULL; i++) { + if (tl->glob_eligible == NULL || tl->glob_eligible[i]) { + if (classify_operator(tl->argv[i]) != OP_NONE) { + has_operator = 1; + break; + } + } + } + + if (has_operator) { + int num_stages = 0; + PipelineStage *stages = trigger_parse_pipeline(tl->argv, tl->glob_eligible, &num_stages); + + if (stages == NULL) { + result.status = 1; + return result; + } + + PipelineResult pr = trigger_execute_pipeline(stages, num_stages); + result.status = pr.last_status; + return result; + } + + const Builtin *b = find_builtin(tl->argv[0]); + + if (b != NULL) { + if (b->fn == trigger_exit) { + result.should_exit = true; + } + result.status = b->fn(tl->argv); + return result; + } + + result.status = trigger_launch(tl->argv); + return result; +} diff --git a/src/glob.c b/src/glob.c index 1f7688b..4726671 100644 --- a/src/glob.c +++ b/src/glob.c @@ -1,110 +1,92 @@ -#include "glob_expand.h" -#include -#include -#include -#include - -static int has_metachars(const char *s) { - return strchr(s, '*') != NULL || strchr(s, '?') != NULL || strchr(s, '[') != NULL; -} - -static void grow_if_needed(char ***argv, int **eligible, int *capacity, int count) { - if (count < *capacity) { - return; - } - - int new_cap = *capacity * 2; - - char **tmp_a = realloc(*argv, (new_cap + 1) * sizeof(char *)); - int *tmp_e = realloc(*eligible, (new_cap + 1) * sizeof(int)); - - if (tmp_a == NULL || tmp_e == NULL) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - *argv = tmp_a; - *eligible = tmp_e; - *capacity = new_cap; -} - -char **expand_globs(char **argv, int **glob_eligible_ptr) { - if (glob_eligible_ptr == NULL) { - return argv; - } - - int *glob_eligible = *glob_eligible_ptr; - - if (glob_eligible == NULL) { - return argv; - } - - int has_any = 0; - - for (int i = 0; argv[i] != NULL; i++) { - if (glob_eligible[i] && has_metachars(argv[i])) { - has_any = 1; - break; - } - } - - if (!has_any) { - return argv; - } - - int capacity = 8; - int count = 0; - char **new_argv = malloc((capacity + 1) * sizeof(char *)); - - if (new_argv == NULL) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - int *new_eligible = malloc((capacity + 1) * sizeof(int)); - - if (new_eligible == NULL) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - for (int i = 0; argv[i] != NULL; i++) { - if (glob_eligible[i] && has_metachars(argv[i])) { - glob_t g; - int r = glob(argv[i], GLOB_NOCHECK, NULL, &g); - - if (r == GLOB_NOSPACE) { - fprintf(stderr, "trigger: glob out of memory\n"); - globfree(&g); - grow_if_needed(&new_argv, &new_eligible, &capacity, count); - new_argv[count] = argv[i]; - new_eligible[count] = 0; - count++; - continue; - } - - for (size_t j = 0; j < g.gl_pathc; j++) { - grow_if_needed(&new_argv, &new_eligible, &capacity, count); - new_argv[count] = strdup(g.gl_pathv[j]); - new_eligible[count] = 0; - count++; - } - - free(argv[i]); - globfree(&g); - } else { - grow_if_needed(&new_argv, &new_eligible, &capacity, count); - new_argv[count] = argv[i]; - new_eligible[count] = glob_eligible[i]; - count++; - } - } - - new_argv[count] = NULL; - - free(argv); - free(glob_eligible); - - *glob_eligible_ptr = new_eligible; - return new_argv; -} +#include "glob_expand.h" +#include "utils/utils.h" +#include +#include +#include +#include +#include + +static int has_metachars(const char *s) { + return strchr(s, '*') != NULL || strchr(s, '?') != NULL || strchr(s, '[') != NULL; +} + +static void grow_if_needed(char ***argv, int **eligible, size_t *capacity, size_t count) { + if (count < *capacity) { + return; + } + + if (*capacity > SIZE_MAX / 2) { + fprintf(stderr, "allocation error\n"); + exit(EXIT_FAILURE); + } + + size_t new_cap = *capacity * 2; + + *argv = xrealloc(*argv, (new_cap + 1) * sizeof(char *)); + *eligible = xrealloc(*eligible, (new_cap + 1) * sizeof(int)); + *capacity = new_cap; +} + +void expand_globs(TokenList *tl) { + if (tl == NULL || tl->argv == NULL || tl->glob_eligible == NULL) { + return; + } + + int has_any = 0; + + for (size_t i = 0; tl->argv[i] != NULL; i++) { + if (tl->glob_eligible[i] && has_metachars(tl->argv[i])) { + has_any = 1; + break; + } + } + + if (!has_any) { + return; + } + + size_t capacity = 8; + size_t count = 0; + char **new_argv = xmalloc((capacity + 1) * sizeof(char *)); + int *new_eligible = xmalloc((capacity + 1) * sizeof(int)); + + for (size_t i = 0; tl->argv[i] != NULL; i++) { + if (tl->glob_eligible[i] && has_metachars(tl->argv[i])) { + glob_t g; + int r = glob(tl->argv[i], GLOB_NOCHECK, NULL, &g); + + if (r != 0) { + globfree(&g); + grow_if_needed(&new_argv, &new_eligible, &capacity, count); + new_argv[count] = tl->argv[i]; + new_eligible[count] = 0; + count++; + continue; + } + + for (size_t j = 0; j < g.gl_pathc; j++) { + grow_if_needed(&new_argv, &new_eligible, &capacity, count); + new_argv[count] = xstrdup(g.gl_pathv[j]); + new_eligible[count] = 0; + count++; + } + + free(tl->argv[i]); + globfree(&g); + } else { + grow_if_needed(&new_argv, &new_eligible, &capacity, count); + new_argv[count] = tl->argv[i]; + new_eligible[count] = tl->glob_eligible[i]; + count++; + } + } + + new_argv[count] = NULL; + + free(tl->argv); + free(tl->glob_eligible); + + tl->argv = new_argv; + tl->glob_eligible = new_eligible; + tl->count = count; +} diff --git a/src/input.c b/src/input.c index 615be79..0004318 100644 --- a/src/input.c +++ b/src/input.c @@ -1,44 +1,61 @@ -#include "input.h" -#include "utils/utils.h" -#include -#include -#include - -char *trigger_read_line(void) { - char *line = NULL; - size_t buffer_size = TRIGGER_AUTOMATIC_BUFFER_SIZE; - - if (getline(&line, &buffer_size, stdin) == GET_LINE_REACHED_END_OF_FILE_OR_ERROR) { - if (feof(stdin)) { - exit(EXIT_SUCCESS); - } - - perror("readline"); - exit(EXIT_FAILURE); - } - - return line; -} - -char **trigger_split_line(const char *line) { - char **tokens = parse_line_with_quotes(line, NULL); - - if (tokens == NULL) { - return NULL; - } - - return tokens; -} - -char **trigger_split_line_ex(const char *line, int **out_glob_eligible) { - char **tokens = parse_line_with_quotes(line, out_glob_eligible); - - if (tokens == NULL) { - if (out_glob_eligible != NULL) { - *out_glob_eligible = NULL; - } - return NULL; - } - - return tokens; -} +#include "input.h" +#include "utils/utils.h" +#include +#include +#include +#include + +char *trigger_read_line(void) { + char *line = NULL; + size_t buffer_size = 0; + ssize_t result; + + do { + result = getline(&line, &buffer_size, stdin); + } while (result == -1 && errno == EINTR); + + if (result == -1) { + if (feof(stdin)) { + exit(EXIT_SUCCESS); + } + + perror("readline"); + exit(EXIT_FAILURE); + } + + return line; +} + +char **trigger_split_line(const char *line) { + TokenList *tl = parse_line_with_quotes(line); + + if (tl == NULL) { + return NULL; + } + + char **argv = tl->argv; + free(tl->glob_eligible); + free(tl); + return argv; +} + +char **trigger_split_line_ex(const char *line, int **out_glob_eligible) { + TokenList *tl = parse_line_with_quotes(line); + + if (tl == NULL) { + if (out_glob_eligible != NULL) { + *out_glob_eligible = NULL; + } + return NULL; + } + + if (out_glob_eligible != NULL) { + *out_glob_eligible = tl->glob_eligible; + } else { + free(tl->glob_eligible); + } + + char **argv = tl->argv; + free(tl); + return argv; +} diff --git a/src/main.c b/src/main.c index a62da37..3e05a73 100644 --- a/src/main.c +++ b/src/main.c @@ -1,36 +1,44 @@ -#include "execute.h" -#include "input.h" -#include "utils/utils.h" -#include -#include - -void trigger_loop(void) { - int status; - - do { - printf("> "); - fflush(stdout); - char *line = trigger_read_line(); - int *glob_eligible = NULL; - char **args = trigger_split_line_ex(line, &glob_eligible); - - if (args == NULL) { - free(line); - free(glob_eligible); - status = true; - continue; - } - - status = trigger_execute(&args, &glob_eligible); - - free(line); - free(glob_eligible); - free_array_of_strings(args); - } while (status); -} - -int main(void) { - trigger_loop(); - - return EXIT_SUCCESS; -} +#include "execute.h" +#include "input.h" +#include "utils/utils.h" +#include +#include +#include +#include + +void trigger_loop(void) { + int last_status = 0; + + for (;;) { + printf("> "); + fflush(stdout); + char *line = trigger_read_line(); + TokenList *tl = parse_line_with_quotes(line); + + if (tl == NULL) { + free(line); + continue; + } + + ExecuteResult r = trigger_execute(tl); + last_status = r.status; + + free(line); + token_list_free(tl); + + if (r.should_exit) { + break; + } + } + + exit(last_status); +} + +int main(void) { + signal(SIGINT, SIG_IGN); + signal(SIGQUIT, SIG_IGN); + + trigger_loop(); + + return EXIT_SUCCESS; +} diff --git a/src/pipeline.c b/src/pipeline.c index 4e83bc0..fddd162 100644 --- a/src/pipeline.c +++ b/src/pipeline.c @@ -1,354 +1,406 @@ -#include "pipeline.h" -#include "builtins.h" -#include "execute.h" -#include "utils/utils.h" -#include -#include -#include -#include -#include -#include - -PipelineStage *trigger_parse_pipeline(char **tokens, int *num_stages) { - int stage_count = 1; - int total_tokens = 0; - - for (int i = 0; tokens[i] != NULL; i++) { - total_tokens++; - if (strcmp(tokens[i], "|") == 0) { - stage_count++; - } - } - - PipelineStage *stages = malloc(stage_count * sizeof(PipelineStage)); - - if (stages == NULL) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - int si = 0; - int stage_start = 0; - - while (si < stage_count) { - stages[si].argv = NULL; - stages[si].infile = NULL; - stages[si].outfile = NULL; - stages[si].append = 0; - - int argv_count = 0; - char *infile_val = NULL; - char *outfile_val = NULL; - int outfile_append = 0; - - int j = stage_start; - - while (tokens[j] != NULL && strcmp(tokens[j], "|") != 0) { - if (strcmp(tokens[j], "<") == 0) { - if (tokens[j + 1] != NULL && strcmp(tokens[j + 1], "|") != 0 && - strcmp(tokens[j + 1], "<") != 0 && strcmp(tokens[j + 1], ">") != 0 && - strcmp(tokens[j + 1], ">>") != 0) { - infile_val = tokens[j + 1]; - j++; - } else { - fprintf(stderr, "trigger: syntax error: expected filename after '<'\n"); - } - } else if (strcmp(tokens[j], ">") == 0) { - if (tokens[j + 1] != NULL && strcmp(tokens[j + 1], "|") != 0 && - strcmp(tokens[j + 1], "<") != 0 && strcmp(tokens[j + 1], ">") != 0 && - strcmp(tokens[j + 1], ">>") != 0) { - outfile_val = tokens[j + 1]; - outfile_append = 0; - j++; - } else { - fprintf(stderr, "trigger: syntax error: expected filename after '>'\n"); - } - } else if (strcmp(tokens[j], ">>") == 0) { - if (tokens[j + 1] != NULL && strcmp(tokens[j + 1], "|") != 0 && - strcmp(tokens[j + 1], "<") != 0 && strcmp(tokens[j + 1], ">") != 0 && - strcmp(tokens[j + 1], ">>") != 0) { - outfile_val = tokens[j + 1]; - outfile_append = 1; - j++; - } else { - fprintf(stderr, "trigger: syntax error: expected filename after '>>'\n"); - } - } else { - argv_count++; - } - j++; - } - - stages[si].argv = malloc((argv_count + 1) * sizeof(char *)); - - if (stages[si].argv == NULL) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - int ai = 0; - - j = stage_start; - - while (tokens[j] != NULL && strcmp(tokens[j], "|") != 0) { - if (strcmp(tokens[j], "<") == 0) { - if (tokens[j + 1] != NULL && strcmp(tokens[j + 1], "|") != 0 && - strcmp(tokens[j + 1], "<") != 0 && strcmp(tokens[j + 1], ">") != 0 && - strcmp(tokens[j + 1], ">>") != 0) { - j++; - } - } else if (strcmp(tokens[j], ">") == 0) { - if (tokens[j + 1] != NULL && strcmp(tokens[j + 1], "|") != 0 && - strcmp(tokens[j + 1], "<") != 0 && strcmp(tokens[j + 1], ">") != 0 && - strcmp(tokens[j + 1], ">>") != 0) { - j++; - } - } else if (strcmp(tokens[j], ">>") == 0) { - if (tokens[j + 1] != NULL && strcmp(tokens[j + 1], "|") != 0 && - strcmp(tokens[j + 1], "<") != 0 && strcmp(tokens[j + 1], ">") != 0 && - strcmp(tokens[j + 1], ">>") != 0) { - j++; - } - } else { - stages[si].argv[ai++] = tokens[j]; - tokens[j] = NULL; - } - j++; - } - stages[si].argv[ai] = NULL; - - stages[si].infile = infile_val; - stages[si].outfile = outfile_val; - stages[si].append = outfile_append; - - if (infile_val != NULL) { - for (int k = 0; k < total_tokens; k++) { - if (tokens[k] == infile_val) { - tokens[k] = NULL; - } - } - } - - if (outfile_val != NULL) { - for (int k = 0; k < total_tokens; k++) { - if (tokens[k] == outfile_val) { - tokens[k] = NULL; - } - } - } - - if (tokens[j] != NULL) { - j++; - } - stage_start = j; - si++; - } - - for (int i = 0; i < total_tokens; i++) { - free(tokens[i]); - } - - *num_stages = stage_count; - return stages; -} - -static void free_pipeline(PipelineStage *stages, int num_stages) { - for (int i = 0; i < num_stages; i++) { - free(stages[i].infile); - free(stages[i].outfile); - if (stages[i].argv != NULL) { - free_array_of_strings(stages[i].argv); - } - } - free(stages); -} - -int trigger_execute_pipeline(PipelineStage *stages, int num_stages) { - if (num_stages == 1 && stages[0].infile == NULL && stages[0].outfile == NULL) { - char **args = stages[0].argv; - - if (args[0] != NULL) { - for (int i = 0; i < trigger_num_builtins(); i++) { - if (strcmp(args[0], builtin_str[i]) == 0) { - int r = (*builtin_func[i])(args); - free_pipeline(stages, num_stages); - return r; - } - } - - int r = trigger_launch(args); - free_pipeline(stages, num_stages); - return r; - } - - free_pipeline(stages, num_stages); - return true; - } - - if (num_stages == 1 && (stages[0].infile != NULL || stages[0].outfile != NULL)) { - char **args = stages[0].argv; - - if (args[0] != NULL) { - for (int i = 0; i < trigger_num_builtins(); i++) { - if (strcmp(args[0], builtin_str[i]) == 0) { - int saved_stdin = dup(STDIN_FILENO); - int saved_stdout = dup(STDOUT_FILENO); - - if (stages[0].infile != NULL) { - int fd = open(stages[0].infile, O_RDONLY); - - if (fd < 0) { - perror("trigger"); - } else { - dup2(fd, STDIN_FILENO); - close(fd); - } - } - - if (stages[0].outfile != NULL) { - int flags = O_WRONLY | O_CREAT; - - flags |= stages[0].append ? O_APPEND : O_TRUNC; - int fd = open(stages[0].outfile, flags, 0644); - - if (fd < 0) { - perror("trigger"); - } else { - dup2(fd, STDOUT_FILENO); - close(fd); - } - } - - int r = (*builtin_func[i])(args); - - dup2(saved_stdin, STDIN_FILENO); - close(saved_stdin); - dup2(saved_stdout, STDOUT_FILENO); - close(saved_stdout); - - free_pipeline(stages, num_stages); - return r; - } - } - } - } - - int pipe_count = num_stages - 1; - int *pipe_fds = NULL; - - if (pipe_count > 0) { - pipe_fds = malloc((unsigned long) (2 * pipe_count) * sizeof(int)); - - if (pipe_fds == NULL) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - for (int i = 0; i < pipe_count; i++) { - if (pipe(pipe_fds + (2 * i)) < 0) { - perror("trigger"); - - for (int j = 0; j < i; j++) { - close(pipe_fds[2 * j]); - close(pipe_fds[(2 * j) + 1]); - } - - free(pipe_fds); - free_pipeline(stages, num_stages); - return true; - } - } - } - - pid_t *pids = malloc(num_stages * sizeof(pid_t)); - - if (pids == NULL) { - fprintf(stderr, "allocation error\n"); - free(pipe_fds); - free_pipeline(stages, num_stages); - exit(EXIT_FAILURE); - } - - for (int i = 0; i < num_stages; i++) { - pids[i] = -1; - } - - for (int i = 0; i < num_stages; i++) { - pid_t pid = fork(); - - if (pid == CHILD_PROCESS_EXITED) { - if (i > 0) { - dup2(pipe_fds[2 * (i - 1)], STDIN_FILENO); - } else if (stages[i].infile != NULL) { - int fd = open(stages[i].infile, O_RDONLY); - - if (fd < 0) { - perror("trigger"); - exit(EXIT_FAILURE); - } - dup2(fd, STDIN_FILENO); - close(fd); - } - - if (i < num_stages - 1) { - dup2(pipe_fds[(2 * i) + 1], STDOUT_FILENO); - } else if (stages[i].outfile != NULL) { - int flags = O_WRONLY | O_CREAT; - - flags |= stages[i].append ? O_APPEND : O_TRUNC; - int fd = open(stages[i].outfile, flags, 0644); - - if (fd < 0) { - perror("trigger"); - exit(EXIT_FAILURE); - } - dup2(fd, STDOUT_FILENO); - close(fd); - } - - for (int j = 0; j < 2 * pipe_count; j++) { - close(pipe_fds[j]); - } - - char **args = stages[i].argv; - - if (args[0] != NULL) { - for (int j = 0; j < trigger_num_builtins(); j++) { - if (strcmp(args[0], builtin_str[j]) == 0) { - (*builtin_func[j])(args); - exit(EXIT_SUCCESS); - } - } - - if (execvp(args[0], args) < 0) { - perror("trigger"); - } - } - exit(EXIT_FAILURE); - } else if (pid < 0) { - perror("trigger"); - } else { - pids[i] = pid; - } - } - - for (int j = 0; j < 2 * pipe_count; j++) { - close(pipe_fds[j]); - } - free(pipe_fds); - - int status; - - for (int i = 0; i < num_stages; i++) { - if (pids[i] == -1) { - continue; - } - do { - waitpid(pids[i], &status, WUNTRACED); - } while (!WIFEXITED(status) && !WIFSIGNALED(status)); - } - - free(pids); - free_pipeline(stages, num_stages); - return true; -} +#include "pipeline.h" +#include "builtins.h" +#include "execute.h" +#include "utils/utils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ARGV_INIT_CAP 8 + +Operator classify_operator(const char *token) { + if (token == NULL) { + return OP_NONE; + } + if (strcmp(token, "|") == 0) { + return OP_PIPE; + } + if (strcmp(token, "<") == 0) { + return OP_REDIR_IN; + } + if (strcmp(token, ">") == 0) { + return OP_REDIR_OUT; + } + if (strcmp(token, ">>") == 0) { + return OP_REDIR_APPEND; + } + return OP_NONE; +} + +static int is_operator_token(const char *token, const int *glob_eligible, int idx) { + if (token == NULL) { + return 0; + } + if (glob_eligible != NULL && !glob_eligible[idx]) { + return 0; + } + return classify_operator(token) != OP_NONE; +} + +static void argv_grow(char ***argv, size_t *cap, size_t count) { + if (count < *cap) { + return; + } + if (*cap > SIZE_MAX / 2) { + fprintf(stderr, "allocation error\n"); + exit(EXIT_FAILURE); + } + *cap *= 2; + *argv = xrealloc(*argv, (*cap + 1) * sizeof(char *)); +} + +PipelineStage *trigger_parse_pipeline(char **tokens, const int *glob_eligible, int *num_stages) { + int stage_count = 1; + int error = 0; + + for (int i = 0; tokens[i] != NULL; i++) { + if (is_operator_token(tokens[i], glob_eligible, i) && + classify_operator(tokens[i]) == OP_PIPE) { + stage_count++; + } + } + + PipelineStage *stages = xmalloc(stage_count * sizeof(PipelineStage)); + + int si = 0; + int stage_start = 0; + + while (si < stage_count) { + stages[si].argv = NULL; + stages[si].infile = NULL; + stages[si].outfile = NULL; + stages[si].append = 0; + + char **argv = xmalloc((ARGV_INIT_CAP + 1) * sizeof(char *)); + size_t argv_cap = ARGV_INIT_CAP; + size_t ai = 0; + + char *infile_val = NULL; + char *outfile_val = NULL; + int outfile_append = 0; + + int j = stage_start; + + while (tokens[j] != NULL && !(is_operator_token(tokens[j], glob_eligible, j) && + classify_operator(tokens[j]) == OP_PIPE)) { + + if (!is_operator_token(tokens[j], glob_eligible, j)) { + argv_grow(&argv, &argv_cap, ai); + argv[ai++] = tokens[j]; + tokens[j] = NULL; + j++; + continue; + } + + Operator op = classify_operator(tokens[j]); + + if (op == OP_REDIR_IN || op == OP_REDIR_OUT || op == OP_REDIR_APPEND) { + if (tokens[j + 1] == NULL || + is_operator_token(tokens[j + 1], glob_eligible, j + 1)) { + fprintf(stderr, "trigger: syntax error near '%s'\n", tokens[j]); + error = 1; + break; + } + + if (op == OP_REDIR_IN) { + infile_val = tokens[j + 1]; + } else { + outfile_val = tokens[j + 1]; + outfile_append = (op == OP_REDIR_APPEND); + } + free(tokens[j]); + tokens[j] = NULL; + j += 2; + } else { + free(tokens[j]); + tokens[j] = NULL; + j++; + } + } + + if (error) { + for (size_t i = 0; i < ai; i++) { + free(argv[i]); + } + free(argv); + int k; + for (k = 0; k < si; k++) { + free_array_of_strings(stages[k].argv); + } + free(stages); + *num_stages = 0; + return NULL; + } + + argv[ai] = NULL; + stages[si].argv = argv; + stages[si].infile = infile_val; + stages[si].outfile = outfile_val; + stages[si].append = outfile_append; + + for (int t = 0; t < j; t++) { + if (tokens[t] == infile_val) { + tokens[t] = NULL; + } + if (tokens[t] == outfile_val) { + tokens[t] = NULL; + } + } + + if (tokens[j] != NULL) { + free(tokens[j]); + tokens[j] = NULL; + j++; + } + stage_start = j; + si++; + } + + *num_stages = stage_count; + return stages; +} + +void trigger_free_pipeline(PipelineStage *stages, int num_stages) { + for (int i = 0; i < num_stages; i++) { + free(stages[i].infile); + free(stages[i].outfile); + if (stages[i].argv != NULL) { + free_array_of_strings(stages[i].argv); + } + } + free(stages); +} + +static int apply_redirection(int infile_override, const char *infile, const char *outfile, + int append) { + if (infile_override >= 0 && infile != NULL) { + int fd = open(infile, O_RDONLY); + if (fd < 0) { + perror("trigger"); + return -1; + } + if (dup2(fd, STDIN_FILENO) == -1) { + perror("trigger"); + close(fd); + return -1; + } + close(fd); + } + + if (outfile != NULL) { + int flags = O_WRONLY | O_CREAT; + flags |= append ? O_APPEND : O_TRUNC; + int fd = open(outfile, flags, FILE_MODE); + if (fd < 0) { + perror("trigger"); + return -1; + } + if (dup2(fd, STDOUT_FILENO) == -1) { + perror("trigger"); + close(fd); + return -1; + } + close(fd); + } + + return 0; +} + +PipelineResult trigger_execute_pipeline(PipelineStage *stages, int num_stages) { + PipelineResult result = {0}; + + if (num_stages == 0 || stages == NULL) { + result.last_status = 1; + return result; + } + + if (num_stages == 1 && stages[0].infile == NULL && stages[0].outfile == NULL) { + char **args = stages[0].argv; + + if (args[0] != NULL) { + const Builtin *b = find_builtin(args[0]); + if (b != NULL) { + result.last_status = b->fn(args); + trigger_free_pipeline(stages, num_stages); + return result; + } + + result.last_status = trigger_launch(args); + trigger_free_pipeline(stages, num_stages); + return result; + } + + trigger_free_pipeline(stages, num_stages); + return result; + } + + if (num_stages == 1 && (stages[0].infile != NULL || stages[0].outfile != NULL)) { + char **args = stages[0].argv; + + if (args[0] != NULL) { + const Builtin *b = find_builtin(args[0]); + if (b != NULL) { + int saved_stdin = dup(STDIN_FILENO); + int saved_stdout = dup(STDOUT_FILENO); + + if (saved_stdin == -1 || saved_stdout == -1) { + perror("trigger"); + if (saved_stdin != -1) { + close(saved_stdin); + } + trigger_free_pipeline(stages, num_stages); + result.last_status = 1; + return result; + } + + int redir_error = + apply_redirection(1, stages[0].infile, stages[0].outfile, stages[0].append); + + if (redir_error == 0) { + result.last_status = b->fn(args); + } else { + result.last_status = 1; + } + + if (dup2(saved_stdin, STDIN_FILENO) == -1) { + perror("trigger"); + } + close(saved_stdin); + if (dup2(saved_stdout, STDOUT_FILENO) == -1) { + perror("trigger"); + } + close(saved_stdout); + + trigger_free_pipeline(stages, num_stages); + return result; + } + } + } + + int pipe_count = num_stages - 1; + int *pipe_fds = NULL; + + if (pipe_count > 0) { + pipe_fds = xmalloc((unsigned long) (2 * pipe_count) * sizeof(int)); + + for (int i = 0; i < pipe_count; i++) { + if (pipe(pipe_fds + (2 * i)) < 0) { + perror("trigger"); + + for (int j = 0; j < i; j++) { + close(pipe_fds[2 * j]); + close(pipe_fds[(2 * j) + 1]); + } + + free(pipe_fds); + trigger_free_pipeline(stages, num_stages); + result.last_status = 1; + return result; + } + } + } + + pid_t *pids = xmalloc(num_stages * sizeof(pid_t)); + + for (int i = 0; i < num_stages; i++) { + pids[i] = -1; + } + + for (int i = 0; i < num_stages; i++) { + pid_t pid = fork(); + + if (pid == 0) { + signal(SIGINT, SIG_DFL); + signal(SIGQUIT, SIG_DFL); + + if (i > 0) { + if (dup2(pipe_fds[2 * (i - 1)], STDIN_FILENO) == -1) { + perror("trigger"); + _exit(EXIT_FAILURE); + } + } else { + int redir_err = apply_redirection(1, stages[i].infile, NULL, 0); + if (redir_err != 0) { + _exit(EXIT_FAILURE); + } + } + + if (i < num_stages - 1) { + if (dup2(pipe_fds[(2 * i) + 1], STDOUT_FILENO) == -1) { + perror("trigger"); + _exit(EXIT_FAILURE); + } + } else { + int redir_err = apply_redirection(0, NULL, stages[i].outfile, stages[i].append); + if (redir_err != 0) { + _exit(EXIT_FAILURE); + } + } + + for (int j = 0; j < 2 * pipe_count; j++) { + close(pipe_fds[j]); + } + + char **args = stages[i].argv; + + if (args[0] != NULL) { + const Builtin *b = find_builtin(args[0]); + if (b != NULL) { + int r = b->fn(args); + _exit(r); + } + + if (execvp(args[0], args) < 0) { + perror("trigger"); + } + } + _exit(127); + } else if (pid < 0) { + perror("trigger"); + } else { + pids[i] = pid; + } + } + + for (int j = 0; j < 2 * pipe_count; j++) { + close(pipe_fds[j]); + } + free(pipe_fds); + + int status; + int last_exit_status = 0; + + for (int i = 0; i < num_stages; i++) { + if (pids[i] == -1) { + continue; + } + int w; + do { + w = waitpid(pids[i], &status, 0); + } while (w == -1 && errno == EINTR); + + if (w == -1) { + perror("trigger"); + continue; + } + + if (i == num_stages - 1) { + if (WIFEXITED(status)) { + last_exit_status = WEXITSTATUS(status); + } else if (WIFSIGNALED(status)) { + last_exit_status = 128 + WTERMSIG(status); + } + } + } + + free(pids); + trigger_free_pipeline(stages, num_stages); + result.last_status = last_exit_status; + return result; +} diff --git a/src/utils/utils.c b/src/utils/utils.c index ba877ad..313706c 100644 --- a/src/utils/utils.c +++ b/src/utils/utils.c @@ -1,252 +1,243 @@ -#include "utils.h" -#include -#include -#include - -void free_array_of_strings(char **array) { - if (array) { - for (int i = 0; array[i] != NULL; i++) { - free(array[i]); - } - free(array); - } -} - -int is_whitespace(const char c) { - return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\a'; -} - -void add_char_to_token(char **token_buffer, int *token_pos, int *token_size, const char c) { - if (*token_pos >= *token_size - 1) { - *token_size *= 2; - char *new_buffer = realloc(*token_buffer, *token_size); - - if (!new_buffer) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - *token_buffer = new_buffer; - } - (*token_buffer)[(*token_pos)++] = c; -} - -void finalize_token(char ***tokens_ptr, int *position, int *buffer_size, char **token_buffer, - int *token_pos, int *token_size, int force, int **out_glob_eligible, - int glob_eligible) { - if (*token_pos > 0 || force) { - char **tokens = *tokens_ptr; - - (*token_buffer)[*token_pos] = '\0'; - - if (*position >= *buffer_size) { - *buffer_size += TRIGGER_TOK_BUFFER_SIZE; - char **new_tokens = realloc(tokens, *buffer_size * sizeof(char *)); - - if (!new_tokens) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - tokens = new_tokens; - *tokens_ptr = tokens; - - if (out_glob_eligible != NULL) { - int *new_glob = realloc(*out_glob_eligible, *buffer_size * sizeof(int)); - - if (!new_glob) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - *out_glob_eligible = new_glob; - } - } - - tokens[*position] = strdup(*token_buffer); - - if (out_glob_eligible != NULL) { - (*out_glob_eligible)[*position] = glob_eligible; - } - - (*position)++; - - *token_pos = 0; - } -} - -static void process_character(const char c, const char next_c, ParserState *state, - char **token_buffer, int *token_pos, int *token_size, - char ***tokens_ptr, int *position, int *buffer_size, int *skip_next, - int *glob_eligible, int **out_glob_eligible) { - *skip_next = 0; - - switch (*state) { - case STATE_NORMAL: - if (c == '\\') { - *state = STATE_ESCAPED; - } else if (c == '\'') { - *state = STATE_IN_SINGLE_QUOTE; - } else if (c == '"') { - *state = STATE_IN_DOUBLE_QUOTE; - } else if (is_whitespace(c)) { - finalize_token(tokens_ptr, position, buffer_size, token_buffer, token_pos, token_size, - false, out_glob_eligible, *glob_eligible); - *glob_eligible = 1; - } else { - add_char_to_token(token_buffer, token_pos, token_size, c); - } - break; - - case STATE_ESCAPED: - add_char_to_token(token_buffer, token_pos, token_size, c); - *glob_eligible = 0; - *state = STATE_NORMAL; - break; - - case STATE_IN_SINGLE_QUOTE: - if (c == '\'') { - *state = STATE_NORMAL; - if (next_c == '\0' || is_whitespace(next_c)) { - finalize_token(tokens_ptr, position, buffer_size, token_buffer, token_pos, - token_size, true, out_glob_eligible, *glob_eligible); - *glob_eligible = 1; - } - } else { - add_char_to_token(token_buffer, token_pos, token_size, c); - *glob_eligible = 0; - } - break; - - case STATE_IN_DOUBLE_QUOTE: - if (c == '"') { - *state = STATE_NORMAL; - if (next_c == '\0' || is_whitespace(next_c)) { - finalize_token(tokens_ptr, position, buffer_size, token_buffer, token_pos, - token_size, true, out_glob_eligible, *glob_eligible); - *glob_eligible = 1; - } - } else if (c == '\\' && next_c != '\0') { - if (next_c == '"' || next_c == '\\' || next_c == '$' || next_c == ' ') { - add_char_to_token(token_buffer, token_pos, token_size, next_c); - *glob_eligible = 0; - *skip_next = true; - } else { - add_char_to_token(token_buffer, token_pos, token_size, c); - *glob_eligible = 0; - } - } else { - add_char_to_token(token_buffer, token_pos, token_size, c); - *glob_eligible = 0; - } - break; - } -} - -char **parse_line_with_quotes(const char *line, int **out_glob_eligible) { - - int buffer_size = TRIGGER_TOK_BUFFER_SIZE; - int position = 0; - char **tokens = malloc(buffer_size * sizeof(char *)); - - if (!tokens) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - int *glob_eligible_arr = NULL; - - if (out_glob_eligible != NULL) { - glob_eligible_arr = malloc(buffer_size * sizeof(int)); - - if (!glob_eligible_arr) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - *out_glob_eligible = glob_eligible_arr; - } - - int token_size = 128; - int token_pos = 0; - char *token_buffer = malloc(token_size); - - if (!token_buffer) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - ParserState state = STATE_NORMAL; - int glob_eligible = 1; - int **pc_out_glob = (out_glob_eligible != NULL) ? &glob_eligible_arr : NULL; - int i = 0; - - while (line[i] != '\0') { - int skip_next = false; - const char next_char = line[i + 1]; - - process_character(line[i], next_char, &state, &token_buffer, &token_pos, &token_size, - &tokens, &position, &buffer_size, &skip_next, &glob_eligible, - pc_out_glob); - - if (skip_next) { - i++; - } - i++; - } - - if (state == STATE_IN_SINGLE_QUOTE) { - fprintf(stderr, "Error: Unclosed single quote\n"); - free(token_buffer); - tokens[position] = NULL; - free_array_of_strings(tokens); - free(glob_eligible_arr); - - if (out_glob_eligible != NULL) { - *out_glob_eligible = NULL; - } - - return NULL; - } - - if (state == STATE_IN_DOUBLE_QUOTE) { - fprintf(stderr, "Error: Unclosed double quote\n"); - free(token_buffer); - tokens[position] = NULL; - free_array_of_strings(tokens); - free(glob_eligible_arr); - - if (out_glob_eligible != NULL) { - *out_glob_eligible = NULL; - } - - return NULL; - } - - finalize_token(&tokens, &position, &buffer_size, &token_buffer, &token_pos, &token_size, false, - pc_out_glob, glob_eligible); - - if (position >= buffer_size) { - buffer_size += TRIGGER_TOK_BUFFER_SIZE; - char **new_tokens = realloc(tokens, buffer_size * sizeof(char *)); - - if (!new_tokens) { - fprintf(stderr, "allocation error\n"); - exit(EXIT_FAILURE); - } - - tokens = new_tokens; - } - - tokens[position] = NULL; - - if (out_glob_eligible != NULL) { - *out_glob_eligible = glob_eligible_arr; - } else { - free(glob_eligible_arr); - } - - free(token_buffer); - - return tokens; -} +#include "utils.h" +#include +#include +#include +#include + +#define INITIAL_TOKEN_BUF_SIZE 128 + +void *xmalloc(size_t size) { + void *p = malloc(size); + if (p == NULL) { + fprintf(stderr, "allocation error\n"); + exit(EXIT_FAILURE); + } + return p; +} + +void *xrealloc(void *ptr, size_t size) { + void *p = realloc(ptr, size); + if (p == NULL) { + fprintf(stderr, "allocation error\n"); + exit(EXIT_FAILURE); + } + return p; +} + +char *xstrdup(const char *s) { + char *d = strdup(s); + if (d == NULL) { + fprintf(stderr, "allocation error\n"); + exit(EXIT_FAILURE); + } + return d; +} + +void free_array_of_strings(char **array) { + if (array) { + for (int i = 0; array[i] != NULL; i++) { + free(array[i]); + } + free(array); + } +} + +void token_list_free(TokenList *tl) { + if (tl) { + for (size_t i = 0; i < tl->count; i++) { + free(tl->argv[i]); + } + free(tl->argv); + free(tl->glob_eligible); + free(tl); + } +} + +static int is_whitespace(char c) { + return c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == '\a'; +} + +static void add_char_to_token(char **token_buffer, size_t *token_pos, size_t *token_size, char c) { + if (*token_pos >= *token_size - 1) { + if (*token_size > SIZE_MAX / 2) { + fprintf(stderr, "allocation error\n"); + exit(EXIT_FAILURE); + } + *token_size *= 2; + *token_buffer = xrealloc(*token_buffer, *token_size); + } + (*token_buffer)[(*token_pos)++] = c; +} + +static void finalize_token(char ***tokens_ptr, size_t *position, size_t *buffer_size, + char **token_buffer, size_t *token_pos, + size_t *token_size __attribute__((unused)), int force, + int **out_glob_eligible, int glob_eligible) { + if (*token_pos > 0 || force) { + char **tokens = *tokens_ptr; + + (*token_buffer)[*token_pos] = '\0'; + + if (*position >= *buffer_size) { + if (*buffer_size > SIZE_MAX / 2) { + fprintf(stderr, "allocation error\n"); + exit(EXIT_FAILURE); + } + *buffer_size *= 2; + tokens = xrealloc(tokens, *buffer_size * sizeof(char *)); + *tokens_ptr = tokens; + + if (out_glob_eligible != NULL) { + *out_glob_eligible = xrealloc(*out_glob_eligible, *buffer_size * sizeof(int)); + } + } + + tokens[*position] = xstrdup(*token_buffer); + + if (out_glob_eligible != NULL) { + (*out_glob_eligible)[*position] = glob_eligible; + } + + (*position)++; + *token_pos = 0; + } +} + +static void process_character(const char c, const char next_c, ParserState *state, + char **token_buffer, size_t *token_pos, size_t *token_size, + char ***tokens_ptr, size_t *position, size_t *buffer_size, + int *skip_next, int *glob_eligible, int **out_glob_eligible) { + *skip_next = 0; + + switch (*state) { + case STATE_NORMAL: + if (c == '\\') { + *state = STATE_ESCAPED; + } else if (c == '\'') { + *state = STATE_IN_SINGLE_QUOTE; + } else if (c == '"') { + *state = STATE_IN_DOUBLE_QUOTE; + } else if (is_whitespace(c)) { + finalize_token(tokens_ptr, position, buffer_size, token_buffer, token_pos, token_size, + 0, out_glob_eligible, *glob_eligible); + *glob_eligible = 1; + } else { + add_char_to_token(token_buffer, token_pos, token_size, c); + } + break; + + case STATE_ESCAPED: + add_char_to_token(token_buffer, token_pos, token_size, c); + *glob_eligible = 0; + *state = STATE_NORMAL; + break; + + case STATE_IN_SINGLE_QUOTE: + if (c == '\'') { + *state = STATE_NORMAL; + if (next_c == '\0' || is_whitespace(next_c)) { + finalize_token(tokens_ptr, position, buffer_size, token_buffer, token_pos, + token_size, 1, out_glob_eligible, *glob_eligible); + *glob_eligible = 1; + } + } else { + add_char_to_token(token_buffer, token_pos, token_size, c); + *glob_eligible = 0; + } + break; + + case STATE_IN_DOUBLE_QUOTE: + if (c == '"') { + *state = STATE_NORMAL; + if (next_c == '\0' || is_whitespace(next_c)) { + finalize_token(tokens_ptr, position, buffer_size, token_buffer, token_pos, + token_size, 1, out_glob_eligible, *glob_eligible); + *glob_eligible = 1; + } + } else if (c == '\\' && next_c != '\0') { + if (next_c == '"' || next_c == '\\' || next_c == '$' || next_c == ' ') { + add_char_to_token(token_buffer, token_pos, token_size, next_c); + *glob_eligible = 0; + *skip_next = 1; + } else { + add_char_to_token(token_buffer, token_pos, token_size, c); + *glob_eligible = 0; + } + } else { + add_char_to_token(token_buffer, token_pos, token_size, c); + *glob_eligible = 0; + } + break; + } +} + +TokenList *parse_line_with_quotes(const char *line) { + size_t buffer_size = TRIGGER_TOK_BUFFER_SIZE; + size_t position = 0; + char **tokens = xmalloc(buffer_size * sizeof(char *)); + + int *glob_eligible_arr = xmalloc(buffer_size * sizeof(int)); + + size_t token_size = INITIAL_TOKEN_BUF_SIZE; + size_t token_pos = 0; + char *token_buffer = xmalloc(token_size); + + ParserState state = STATE_NORMAL; + int glob_eligible = 1; + size_t i = 0; + + while (line[i] != '\0') { + int skip_next = 0; + const char next_char = line[i + 1]; + + process_character(line[i], next_char, &state, &token_buffer, &token_pos, &token_size, + &tokens, &position, &buffer_size, &skip_next, &glob_eligible, + &glob_eligible_arr); + + if (skip_next) { + i++; + } + i++; + } + + if (state == STATE_IN_SINGLE_QUOTE) { + fprintf(stderr, "Error: Unclosed single quote\n"); + free(token_buffer); + tokens[position] = NULL; + free_array_of_strings(tokens); + free(glob_eligible_arr); + return NULL; + } + + if (state == STATE_IN_DOUBLE_QUOTE) { + fprintf(stderr, "Error: Unclosed double quote\n"); + free(token_buffer); + tokens[position] = NULL; + free_array_of_strings(tokens); + free(glob_eligible_arr); + return NULL; + } + + finalize_token(&tokens, &position, &buffer_size, &token_buffer, &token_pos, &token_size, 0, + &glob_eligible_arr, glob_eligible); + + if (position >= buffer_size) { + if (buffer_size > SIZE_MAX / 2) { + fprintf(stderr, "allocation error\n"); + exit(EXIT_FAILURE); + } + buffer_size *= 2; + tokens = xrealloc(tokens, buffer_size * sizeof(char *)); + } + + tokens[position] = NULL; + + TokenList *tl = xmalloc(sizeof(TokenList)); + tl->argv = tokens; + tl->glob_eligible = glob_eligible_arr; + tl->count = position; + + free(token_buffer); + + return tl; +} diff --git a/src/utils/utils.h b/src/utils/utils.h index 3fcbdd1..e0a362a 100644 --- a/src/utils/utils.h +++ b/src/utils/utils.h @@ -1,28 +1,31 @@ -#ifndef UTILS_H -#define UTILS_H - -#define true 1 -#define false 0 - -#define TRIGGER_TOK_BUFFER_SIZE 64 - -typedef enum { - STATE_NORMAL, - STATE_IN_SINGLE_QUOTE, - STATE_IN_DOUBLE_QUOTE, - STATE_ESCAPED -} ParserState; - -void free_array_of_strings(char **array); - -int is_whitespace(char c); - -void add_char_to_token(char **token_buffer, int *token_pos, int *token_size, char c); - -void finalize_token(char ***tokens_ptr, int *position, int *buffer_size, char **token_buffer, - int *token_pos, int *token_size, int force, int **out_glob_eligible, - int glob_eligible); - -char **parse_line_with_quotes(const char *line, int **out_glob_eligible); - -#endif +#ifndef UTILS_H +#define UTILS_H + +#include +#include + +#define TRIGGER_TOK_BUFFER_SIZE 64 + +typedef struct TokenList { + char **argv; + int *glob_eligible; + size_t count; +} TokenList; + +typedef enum { + STATE_NORMAL, + STATE_IN_SINGLE_QUOTE, + STATE_IN_DOUBLE_QUOTE, + STATE_ESCAPED +} ParserState; + +void *xmalloc(size_t size); +void *xrealloc(void *ptr, size_t size); +char *xstrdup(const char *s); + +void free_array_of_strings(char **array); +void token_list_free(TokenList *tl); + +TokenList *parse_line_with_quotes(const char *line); + +#endif diff --git a/tests/test_builtins.c b/tests/test_builtins.c index 4ec8170..d2f17f5 100644 --- a/tests/test_builtins.c +++ b/tests/test_builtins.c @@ -1,145 +1,138 @@ -#include "../include/builtins.h" -#include "test_framework.h" -#include -#include -#include -#include - -void test_builtin_arrays_match() { - int count = trigger_num_builtins(); - - ASSERT_TRUE(count == 7, "Should have 7 built-in commands"); - - int found_cd = 0, found_help = 0, found_exit = 0; - int found_pwd = 0, found_echo = 0, found_export = 0, found_unset = 0; - for (int i = 0; i < count; i++) { - if (strcmp(builtin_str[i], "cd") == 0) - found_cd = 1; - if (strcmp(builtin_str[i], "help") == 0) - found_help = 1; - if (strcmp(builtin_str[i], "exit") == 0) - found_exit = 1; - if (strcmp(builtin_str[i], "pwd") == 0) - found_pwd = 1; - if (strcmp(builtin_str[i], "echo") == 0) - found_echo = 1; - if (strcmp(builtin_str[i], "export") == 0) - found_export = 1; - if (strcmp(builtin_str[i], "unset") == 0) - found_unset = 1; - } - - ASSERT_TRUE(found_cd, "Should have 'cd' built-in"); - ASSERT_TRUE(found_help, "Should have 'help' built-in"); - ASSERT_TRUE(found_exit, "Should have 'exit' built-in"); - ASSERT_TRUE(found_pwd, "Should have 'pwd' built-in"); - ASSERT_TRUE(found_echo, "Should have 'echo' built-in"); - ASSERT_TRUE(found_export, "Should have 'export' built-in"); - ASSERT_TRUE(found_unset, "Should have 'unset' built-in"); -} - -void test_cd_no_args() { - char *args[] = {"cd", NULL}; - int result = trigger_cd(args); - - // Should return true (1) even when no directory provided - ASSERT_TRUE(result, "cd with no args should return true"); -} - -void test_cd_to_tmp() { - char cwd_before[1024]; - char cwd_after[1024]; - - getcwd(cwd_before, sizeof(cwd_before)); - - char *args[] = {"cd", "/tmp", NULL}; - int result = trigger_cd(args); - - getcwd(cwd_after, sizeof(cwd_after)); - - ASSERT_TRUE(result, "cd to /tmp should return true"); - ASSERT_STR_EQUAL("/tmp", cwd_after, "Should be in /tmp directory"); - - // Restore original directory - chdir(cwd_before); -} - -void test_help_command() { - char *args[] = {"help", NULL}; - int result = trigger_help(args); - - ASSERT_TRUE(result, "help command should return true"); -} - -void test_exit_command() { - char *args[] = {"exit", NULL}; - int result = trigger_exit(args); - - ASSERT_EQUAL(EXIT_SUCCESS, result, "exit command should return EXIT_SUCCESS"); -} - -void test_pwd_command() { - char *args[] = {"pwd", NULL}; - - char cwd_before[4096]; - getcwd(cwd_before, sizeof(cwd_before)); - - int result = trigger_pwd(args); - ASSERT_TRUE(result, "pwd should return true"); - - char cwd_after[4096]; - getcwd(cwd_after, sizeof(cwd_after)); - ASSERT_STR_EQUAL(cwd_before, cwd_after, "pwd should not change current directory"); -} - -void test_echo_command() { - char *args[] = {"echo", "hello", "world", NULL}; - int result = trigger_echo(args); - ASSERT_TRUE(result, "echo should return true"); -} - -void test_echo_empty() { - char *args[] = {"echo", NULL}; - int result = trigger_echo(args); - ASSERT_TRUE(result, "echo with no args should return true"); -} - -void test_export_command() { - char *args[] = {"export", "TEST_VAR=hello", NULL}; - int result = trigger_export(args); - ASSERT_TRUE(result, "export should return true"); - ASSERT_STR_EQUAL("hello", getenv("TEST_VAR"), "exported variable should be available"); - unsetenv("TEST_VAR"); -} - -void test_export_no_value() { - char *args[] = {"export", "NOVAL", NULL}; - int result = trigger_export(args); - ASSERT_TRUE(result, "export of invalid format should still return true"); -} - -void test_unset_command() { - setenv("UNSET_VAR", "test_value", 1); - char *args[] = {"unset", "UNSET_VAR", NULL}; - int result = trigger_unset(args); - ASSERT_TRUE(result, "unset should return true"); - ASSERT_NULL(getenv("UNSET_VAR"), "unset variable should not be available"); -} - -int main() { - TEST_SUITE_START("Built-ins Module Tests"); - - test_builtin_arrays_match(); - test_cd_no_args(); - test_cd_to_tmp(); - test_help_command(); - test_exit_command(); - test_pwd_command(); - test_echo_command(); - test_echo_empty(); - test_export_command(); - test_export_no_value(); - test_unset_command(); - - TEST_SUITE_END(); -} +#include "../include/builtins.h" +#include "test_framework.h" +#include +#include +#include +#include + +void test_builtin_arrays_match() { + int count = trigger_num_builtins(); + + ASSERT_TRUE(count == 7, "Should have 7 built-in commands"); + + ASSERT_NOT_NULL(find_builtin("cd"), "Should find 'cd' built-in"); + ASSERT_NOT_NULL(find_builtin("help"), "Should find 'help' built-in"); + ASSERT_NOT_NULL(find_builtin("exit"), "Should find 'exit' built-in"); + ASSERT_NOT_NULL(find_builtin("pwd"), "Should find 'pwd' built-in"); + ASSERT_NOT_NULL(find_builtin("echo"), "Should find 'echo' built-in"); + ASSERT_NOT_NULL(find_builtin("export"), "Should find 'export' built-in"); + ASSERT_NOT_NULL(find_builtin("unset"), "Should find 'unset' built-in"); + + ASSERT_NULL(find_builtin("nonexistent"), "Should not find unknown built-in"); + + const Builtin *cd_b = find_builtin("cd"); + ASSERT_NOT_NULL(cd_b, "cd Builtin* should not be NULL"); + ASSERT_TRUE(cd_b->fn == trigger_cd, "cd Builtin should point to trigger_cd"); +} + +void test_cd_no_args() { + char *args[] = {"cd", NULL}; + int result = trigger_cd(args); + + ASSERT_EQUAL(1, result, "cd with no args should return 1 (error)"); +} + +void test_cd_to_tmp() { + char cwd_before[1024]; + char cwd_after[1024]; + + getcwd(cwd_before, sizeof(cwd_before)); + + char *args[] = {"cd", "/tmp", NULL}; + int result = trigger_cd(args); + + getcwd(cwd_after, sizeof(cwd_after)); + + ASSERT_EQUAL(0, result, "cd to /tmp should return 0"); + ASSERT_STR_EQUAL("/tmp", cwd_after, "Should be in /tmp directory"); + + chdir(cwd_before); +} + +void test_help_command() { + char *args[] = {"help", NULL}; + int result = trigger_help(args); + + ASSERT_EQUAL(0, result, "help command should return 0"); +} + +void test_exit_command() { + char *args[] = {"exit", NULL}; + int result = trigger_exit(args); + + ASSERT_EQUAL(EXIT_SUCCESS, result, "exit command should return EXIT_SUCCESS"); +} + +void test_exit_with_code() { + char *args[] = {"exit", "3", NULL}; + int result = trigger_exit(args); + + ASSERT_EQUAL(3, result, "exit 3 should return 3"); +} + +void test_pwd_command() { + char *args[] = {"pwd", NULL}; + + char cwd_before[4096]; + getcwd(cwd_before, sizeof(cwd_before)); + + int result = trigger_pwd(args); + ASSERT_EQUAL(0, result, "pwd should return 0"); + + char cwd_after[4096]; + getcwd(cwd_after, sizeof(cwd_after)); + ASSERT_STR_EQUAL(cwd_before, cwd_after, "pwd should not change current directory"); +} + +void test_echo_command() { + char *args[] = {"echo", "hello", "world", NULL}; + int result = trigger_echo(args); + ASSERT_EQUAL(0, result, "echo should return 0"); +} + +void test_echo_empty() { + char *args[] = {"echo", NULL}; + int result = trigger_echo(args); + ASSERT_EQUAL(0, result, "echo with no args should return 0"); +} + +void test_export_command() { + char *args[] = {"export", "TEST_VAR=hello", NULL}; + int result = trigger_export(args); + ASSERT_EQUAL(0, result, "export should return 0"); + ASSERT_STR_EQUAL("hello", getenv("TEST_VAR"), "exported variable should be available"); + unsetenv("TEST_VAR"); +} + +void test_export_no_value() { + char *args[] = {"export", "NOVAL", NULL}; + int result = trigger_export(args); + ASSERT_EQUAL(0, result, "export of invalid format should still return 0"); +} + +void test_unset_command() { + setenv("UNSET_VAR", "test_value", 1); + char *args[] = {"unset", "UNSET_VAR", NULL}; + int result = trigger_unset(args); + ASSERT_EQUAL(0, result, "unset should return 0"); + ASSERT_NULL(getenv("UNSET_VAR"), "unset variable should not be available"); +} + +int main() { + TEST_SUITE_START("Built-ins Module Tests"); + + test_builtin_arrays_match(); + test_cd_no_args(); + test_cd_to_tmp(); + test_help_command(); + test_exit_command(); + test_exit_with_code(); + test_pwd_command(); + test_echo_command(); + test_echo_empty(); + test_export_command(); + test_export_no_value(); + test_unset_command(); + + TEST_SUITE_END(); +} diff --git a/tests/test_execute.c b/tests/test_execute.c index 9ffe1c0..e7be31f 100644 --- a/tests/test_execute.c +++ b/tests/test_execute.c @@ -1,56 +1,85 @@ -#include "../include/execute.h" -#include "test_framework.h" -#include -#include -#include - -void test_execute_empty_command() { - char *args[] = {NULL}; - char **a = args; - int result = trigger_execute(&a, NULL); - - ASSERT_TRUE(result, "Empty command should return true"); -} - -void test_execute_builtin_help() { - char *args[] = {"help", NULL}; - char **a = args; - int result = trigger_execute(&a, NULL); - - ASSERT_TRUE(result, "Execute 'help' should return true"); -} - -void test_execute_builtin_exit() { - char *args[] = {"exit", NULL}; - char **a = args; - int result = trigger_execute(&a, NULL); - - ASSERT_EQUAL(EXIT_SUCCESS, result, "Execute 'exit' should return EXIT_SUCCESS"); -} - -void test_execute_external_command() { - char *args[] = {"/bin/true", NULL}; - char **a = args; - int result = trigger_execute(&a, NULL); - - ASSERT_TRUE(result, "Execute '/bin/true' should return true"); -} - -void test_launch_simple_command() { - char *args[] = {"/bin/echo", "test", NULL}; - int result = trigger_launch(args); - - ASSERT_TRUE(result, "Launch '/bin/echo test' should return true"); -} - -int main() { - TEST_SUITE_START("Execute Module Tests"); - - test_execute_empty_command(); - test_execute_builtin_help(); - test_execute_builtin_exit(); - test_execute_external_command(); - test_launch_simple_command(); - - TEST_SUITE_END(); -} +#include "../include/execute.h" +#include "../src/utils/utils.h" +#include "test_framework.h" +#include +#include +#include + +static TokenList *make_tokenlist(int argc, char *words[]) { + TokenList *tl = malloc(sizeof(TokenList)); + tl->argv = malloc((argc + 1) * sizeof(char *)); + int i; + for (i = 0; i < argc; i++) { + tl->argv[i] = words[i]; + } + tl->argv[argc] = NULL; + tl->glob_eligible = malloc((argc + 1) * sizeof(int)); + for (i = 0; i <= argc; i++) { + tl->glob_eligible[i] = 1; + } + tl->count = argc; + return tl; +} + +static void free_tokenlist(TokenList *tl) { + free(tl->argv); + free(tl->glob_eligible); + free(tl); +} + +void test_execute_empty_command() { + char *words[] = {NULL}; + TokenList *tl = make_tokenlist(0, words); + ExecuteResult r = trigger_execute(tl); + free_tokenlist(tl); + + ASSERT_FALSE(r.should_exit, "Empty command should not exit"); +} + +void test_execute_builtin_help() { + char *words[] = {"help", NULL}; + TokenList *tl = make_tokenlist(1, words); + ExecuteResult r = trigger_execute(tl); + free_tokenlist(tl); + + ASSERT_EQUAL(0, r.status, "Execute 'help' should return status 0"); + ASSERT_FALSE(r.should_exit, "help should not exit"); +} + +void test_execute_builtin_exit() { + char *words[] = {"exit", NULL}; + TokenList *tl = make_tokenlist(1, words); + ExecuteResult r = trigger_execute(tl); + free_tokenlist(tl); + + ASSERT_EQUAL(EXIT_SUCCESS, r.status, "Execute 'exit' should return EXIT_SUCCESS"); + ASSERT_TRUE(r.should_exit, "exit should set should_exit"); +} + +void test_execute_external_command() { + char *words[] = {"/bin/true", NULL}; + TokenList *tl = make_tokenlist(1, words); + ExecuteResult r = trigger_execute(tl); + free_tokenlist(tl); + + ASSERT_FALSE(r.should_exit, "Execute '/bin/true' should not exit"); +} + +void test_launch_simple_command() { + char *args[] = {"/bin/echo", "test", NULL}; + int result = trigger_launch(args); + + ASSERT_EQUAL(0, result, "Launch '/bin/echo test' should return status 0"); +} + +int main() { + TEST_SUITE_START("Execute Module Tests"); + + test_execute_empty_command(); + test_execute_builtin_help(); + test_execute_builtin_exit(); + test_execute_external_command(); + test_launch_simple_command(); + + TEST_SUITE_END(); +} diff --git a/tests/test_glob.c b/tests/test_glob.c index 65edfa9..e2ecf4b 100644 --- a/tests/test_glob.c +++ b/tests/test_glob.c @@ -1,227 +1,233 @@ -#include "../include/glob_expand.h" -#include "../include/input.h" -#include "../include/pipeline.h" -#include "../src/utils/utils.h" -#include "test_framework.h" -#include -#include -#include -#include -#include - -static void setup_fixture() { - mkdir("/tmp/trigger_glob_test", 0755); - FILE *f = fopen("/tmp/trigger_glob_test/a.txt", "w"); - if (f) - fclose(f); - f = fopen("/tmp/trigger_glob_test/b.txt", "w"); - if (f) - fclose(f); - f = fopen("/tmp/trigger_glob_test/c.md", "w"); - if (f) - fclose(f); - f = fopen("/tmp/trigger_glob_test/d.dat", "w"); - if (f) - fclose(f); - f = fopen("/tmp/trigger_glob_test/ax.txt", "w"); - if (f) - fclose(f); -} - -static void teardown_fixture() { - unlink("/tmp/trigger_glob_test/a.txt"); - unlink("/tmp/trigger_glob_test/b.txt"); - unlink("/tmp/trigger_glob_test/c.md"); - unlink("/tmp/trigger_glob_test/d.dat"); - unlink("/tmp/trigger_glob_test/ax.txt"); - rmdir("/tmp/trigger_glob_test"); -} - -void test_glob_txt_files() { - setup_fixture(); - - int *glob_eligible = NULL; - char **args = trigger_split_line_ex("echo /tmp/trigger_glob_test/*.txt", &glob_eligible); - ASSERT_NOT_NULL(args, "args should not be NULL"); - ASSERT_NOT_NULL(glob_eligible, "glob_eligible should not be NULL"); - - ASSERT_TRUE(glob_eligible[1], "*.txt is glob-eligible (unquoted)"); - - char **expanded = expand_globs(args, &glob_eligible); - - int count = 0; - while (expanded[count] != NULL) - count++; - ASSERT_EQUAL(4, count, "should have 4 entries: echo + a.txt + b.txt + ax.txt"); - - int found_a = 0, found_b = 0, found_ax = 0; - for (int i = 0; expanded[i] != NULL; i++) { - if (strstr(expanded[i], "a.txt")) - found_a = 1; - if (strstr(expanded[i], "b.txt")) - found_b = 1; - if (strstr(expanded[i], "ax.txt")) - found_ax = 1; - } - ASSERT_TRUE(found_a, "should have a.txt in expansion"); - ASSERT_TRUE(found_b, "should have b.txt in expansion"); - ASSERT_TRUE(found_ax, "should have ax.txt in expansion"); - - ASSERT_NOT_NULL(glob_eligible, "glob_eligible should still be valid after expansion"); - - free(glob_eligible); - free_array_of_strings(expanded); - teardown_fixture(); -} - -void test_quoted_glob_not_expanded() { - setup_fixture(); - - int *glob_eligible = NULL; - char **args = trigger_split_line_ex("echo '/tmp/trigger_glob_test/*.txt'", &glob_eligible); - ASSERT_NOT_NULL(args, "args should not be NULL"); - - ASSERT_FALSE(glob_eligible[1], "quoted *.txt is NOT glob-eligible"); - - char **expanded = expand_globs(args, &glob_eligible); - - ASSERT_TRUE(expanded == args, "should return args unchanged (no eligible tokens)"); - - ASSERT_STR_EQUAL("/tmp/trigger_glob_test/*.txt", expanded[1], - "quoted glob should stay as literal"); - - free(glob_eligible); - free_array_of_strings(expanded); - teardown_fixture(); -} - -void test_no_match_pattern() { - setup_fixture(); - - int *glob_eligible = NULL; - char **args = - trigger_split_line_ex("echo /tmp/trigger_glob_test/*.nonexistent", &glob_eligible); - ASSERT_NOT_NULL(args, "args should not be NULL"); - - char **expanded = expand_globs(args, &glob_eligible); - - ASSERT_STR_EQUAL("/tmp/trigger_glob_test/*.nonexistent", expanded[1], - "no-match pattern should pass through literally"); - - free(glob_eligible); - free_array_of_strings(expanded); - teardown_fixture(); -} - -void test_question_mark_glob() { - setup_fixture(); - - int *glob_eligible = NULL; - char **args = trigger_split_line_ex("echo /tmp/trigger_glob_test/?.txt", &glob_eligible); - ASSERT_NOT_NULL(args, "args should not be NULL"); - - char **expanded = expand_globs(args, &glob_eligible); - - ASSERT_STR_EQUAL("echo", expanded[0], "command is echo"); - ASSERT_STR_EQUAL("/tmp/trigger_glob_test/a.txt", expanded[1], "?.txt matches a.txt"); - ASSERT_STR_EQUAL("/tmp/trigger_glob_test/b.txt", expanded[2], "?.txt matches b.txt"); - ASSERT_NULL(expanded[3], "should not match ax.txt (two chars)"); - - free(glob_eligible); - free_array_of_strings(expanded); - teardown_fixture(); -} - -void test_no_glob_simple_args() { - int *glob_eligible = NULL; - char **args = trigger_split_line_ex("echo hello world", &glob_eligible); - ASSERT_NOT_NULL(args, "args should not be NULL"); - - char **expanded = expand_globs(args, &glob_eligible); - - ASSERT_TRUE(expanded == args, "args with no globs returns same pointer"); - - free(glob_eligible); - free_array_of_strings(expanded); -} - -void test_escaped_glob_not_expanded() { - setup_fixture(); - - int *glob_eligible = NULL; - char **args = trigger_split_line_ex("echo /tmp/trigger_glob_test/\\*.txt", &glob_eligible); - ASSERT_NOT_NULL(args, "args should not be NULL"); - - ASSERT_FALSE(glob_eligible[1], "backslash-escaped *.txt is NOT glob-eligible"); - - free(glob_eligible); - free_array_of_strings(args); - teardown_fixture(); -} - -void test_glob_eligible_after_expansion() { - setup_fixture(); - - int *glob_eligible = NULL; - char **args = trigger_split_line_ex("echo /tmp/trigger_glob_test/*.txt", &glob_eligible); - ASSERT_NOT_NULL(args, "args should not be NULL"); - - char **expanded = expand_globs(args, &glob_eligible); - - int count = 0; - while (expanded[count] != NULL) - count++; - ASSERT_EQUAL(4, count, "should have 4 entries"); - - ASSERT_TRUE(glob_eligible[0], "echo should keep original eligibility=1"); - - for (int i = 1; i < 4; i++) { - ASSERT_FALSE(glob_eligible[i], "glob-expanded filenames should be eligible=0"); - } - - free(glob_eligible); - free_array_of_strings(expanded); - teardown_fixture(); -} - -void test_glob_with_pipe_operator() { - setup_fixture(); - - int *glob_eligible = NULL; - char **args = trigger_split_line_ex("echo /tmp/trigger_glob_test/*.txt | wc x", &glob_eligible); - ASSERT_NOT_NULL(args, "args should not be NULL"); - - char **expanded = expand_globs(args, &glob_eligible); - - int count = 0; - while (expanded[count] != NULL) - count++; - - ASSERT_EQUAL(7, count, "should have 7 entries: echo a b ax | wc x"); - - ASSERT_STR_EQUAL("echo", expanded[0], "command is echo"); - ASSERT_STR_EQUAL("|", expanded[4], "pipe at index 4 after glob expansion"); - ASSERT_STR_EQUAL("wc", expanded[5], "wc at index 5"); - ASSERT_STR_EQUAL("x", expanded[6], "x at index 6"); - - ASSERT_TRUE(glob_eligible[4], "pipe operator after glob should be glob_eligible=1"); - - free(glob_eligible); - free_array_of_strings(expanded); - teardown_fixture(); -} - -int main() { - TEST_SUITE_START("Glob Expansion Tests"); - - test_glob_txt_files(); - test_quoted_glob_not_expanded(); - test_no_match_pattern(); - test_question_mark_glob(); - test_no_glob_simple_args(); - test_escaped_glob_not_expanded(); - test_glob_eligible_after_expansion(); - test_glob_with_pipe_operator(); - - TEST_SUITE_END(); -} +#include "../include/glob_expand.h" +#include "../include/input.h" +#include "../src/utils/utils.h" +#include "test_framework.h" +#include +#include +#include +#include +#include + +static void setup_fixture() { + mkdir("/tmp/trigger_glob_test", 0755); + FILE *f = fopen("/tmp/trigger_glob_test/a.txt", "w"); + if (f) + fclose(f); + f = fopen("/tmp/trigger_glob_test/b.txt", "w"); + if (f) + fclose(f); + f = fopen("/tmp/trigger_glob_test/c.md", "w"); + if (f) + fclose(f); + f = fopen("/tmp/trigger_glob_test/d.dat", "w"); + if (f) + fclose(f); + f = fopen("/tmp/trigger_glob_test/ax.txt", "w"); + if (f) + fclose(f); +} + +static void teardown_fixture() { + unlink("/tmp/trigger_glob_test/a.txt"); + unlink("/tmp/trigger_glob_test/b.txt"); + unlink("/tmp/trigger_glob_test/c.md"); + unlink("/tmp/trigger_glob_test/d.dat"); + unlink("/tmp/trigger_glob_test/ax.txt"); + rmdir("/tmp/trigger_glob_test"); +} + +static TokenList *make_tl(const char *line) { + int *glob_eligible = NULL; + char **argv = trigger_split_line_ex(line, &glob_eligible); + if (argv == NULL) { + return NULL; + } + + TokenList *tl = malloc(sizeof(TokenList)); + tl->argv = argv; + tl->glob_eligible = glob_eligible; + tl->count = 0; + while (argv[tl->count] != NULL) + tl->count++; + return tl; +} + +static void free_tl(TokenList *tl) { + free_array_of_strings(tl->argv); + free(tl->glob_eligible); + free(tl); +} + +void test_glob_txt_files() { + setup_fixture(); + + TokenList *tl = make_tl("echo /tmp/trigger_glob_test/*.txt"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + ASSERT_NOT_NULL(tl->glob_eligible, "glob_eligible should not be NULL"); + + ASSERT_TRUE(tl->glob_eligible[1], "*.txt is glob-eligible (unquoted)"); + + expand_globs(tl); + + int count = 0; + while (tl->argv[count] != NULL) + count++; + ASSERT_EQUAL(4, count, "should have 4 entries: echo + a.txt + b.txt + ax.txt"); + + int found_a = 0, found_b = 0, found_ax = 0; + for (int i = 0; tl->argv[i] != NULL; i++) { + if (strstr(tl->argv[i], "a.txt")) + found_a = 1; + if (strstr(tl->argv[i], "b.txt")) + found_b = 1; + if (strstr(tl->argv[i], "ax.txt")) + found_ax = 1; + } + ASSERT_TRUE(found_a, "should have a.txt in expansion"); + ASSERT_TRUE(found_b, "should have b.txt in expansion"); + ASSERT_TRUE(found_ax, "should have ax.txt in expansion"); + + ASSERT_NOT_NULL(tl->glob_eligible, "glob_eligible should still be valid after expansion"); + + free_tl(tl); + teardown_fixture(); +} + +void test_quoted_glob_not_expanded() { + setup_fixture(); + + TokenList *tl = make_tl("echo '/tmp/trigger_glob_test/*.txt'"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + + ASSERT_FALSE(tl->glob_eligible[1], "quoted *.txt is NOT glob-eligible"); + + char *before = tl->argv[1]; + expand_globs(tl); + + ASSERT_STR_EQUAL(before, tl->argv[1], "quoted glob should stay as literal"); + ASSERT_STR_EQUAL("/tmp/trigger_glob_test/*.txt", tl->argv[1], + "quoted glob should stay as literal"); + + free_tl(tl); + teardown_fixture(); +} + +void test_no_match_pattern() { + setup_fixture(); + + TokenList *tl = make_tl("echo /tmp/trigger_glob_test/*.nonexistent"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + + expand_globs(tl); + + ASSERT_STR_EQUAL("/tmp/trigger_glob_test/*.nonexistent", tl->argv[1], + "no-match pattern should pass through literally"); + + free_tl(tl); + teardown_fixture(); +} + +void test_question_mark_glob() { + setup_fixture(); + + TokenList *tl = make_tl("echo /tmp/trigger_glob_test/?.txt"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + + expand_globs(tl); + + ASSERT_STR_EQUAL("echo", tl->argv[0], "command is echo"); + ASSERT_STR_EQUAL("/tmp/trigger_glob_test/a.txt", tl->argv[1], "?.txt matches a.txt"); + ASSERT_STR_EQUAL("/tmp/trigger_glob_test/b.txt", tl->argv[2], "?.txt matches b.txt"); + ASSERT_NULL(tl->argv[3], "should not match ax.txt (two chars)"); + + free_tl(tl); + teardown_fixture(); +} + +void test_no_glob_simple_args() { + TokenList *tl = make_tl("echo hello world"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + + expand_globs(tl); + + ASSERT_STR_EQUAL("echo", tl->argv[0], "command is echo"); + ASSERT_STR_EQUAL("hello", tl->argv[1], "second arg is hello"); + ASSERT_STR_EQUAL("world", tl->argv[2], "third arg is world"); + + free_tl(tl); +} + +void test_escaped_glob_not_expanded() { + setup_fixture(); + + TokenList *tl = make_tl("echo /tmp/trigger_glob_test/\\*.txt"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + + ASSERT_FALSE(tl->glob_eligible[1], "backslash-escaped *.txt is NOT glob-eligible"); + + free_tl(tl); + teardown_fixture(); +} + +void test_glob_eligible_after_expansion() { + setup_fixture(); + + TokenList *tl = make_tl("echo /tmp/trigger_glob_test/*.txt"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + + expand_globs(tl); + + int count = 0; + while (tl->argv[count] != NULL) + count++; + ASSERT_EQUAL(4, count, "should have 4 entries"); + + ASSERT_TRUE(tl->glob_eligible[0], "echo should keep original eligibility=1"); + + for (int i = 1; i < 4; i++) { + ASSERT_FALSE(tl->glob_eligible[i], "glob-expanded filenames should be eligible=0"); + } + + free_tl(tl); + teardown_fixture(); +} + +void test_glob_with_pipe_operator() { + setup_fixture(); + + TokenList *tl = make_tl("echo /tmp/trigger_glob_test/*.txt | wc x"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + + expand_globs(tl); + + int count = 0; + while (tl->argv[count] != NULL) + count++; + + ASSERT_EQUAL(7, count, "should have 7 entries: echo a b ax | wc x"); + + ASSERT_STR_EQUAL("echo", tl->argv[0], "command is echo"); + ASSERT_STR_EQUAL("|", tl->argv[4], "pipe at index 4 after glob expansion"); + ASSERT_STR_EQUAL("wc", tl->argv[5], "wc at index 5"); + ASSERT_STR_EQUAL("x", tl->argv[6], "x at index 6"); + + ASSERT_TRUE(tl->glob_eligible[4], "pipe operator after glob should be glob_eligible=1"); + + free_tl(tl); + teardown_fixture(); +} + +int main() { + TEST_SUITE_START("Glob Expansion Tests"); + + test_glob_txt_files(); + test_quoted_glob_not_expanded(); + test_no_match_pattern(); + test_question_mark_glob(); + test_no_glob_simple_args(); + test_escaped_glob_not_expanded(); + test_glob_eligible_after_expansion(); + test_glob_with_pipe_operator(); + + TEST_SUITE_END(); +} diff --git a/tests/test_input.c b/tests/test_input.c index 4d6ad03..c1b803b 100644 --- a/tests/test_input.c +++ b/tests/test_input.c @@ -1,494 +1,548 @@ -#include "../include/input.h" -#include "../src/utils/utils.h" -#include "test_framework.h" -#include -#include -#include - -void test_split_line_basic() { - char *line = strdup("ls -la /home"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("ls", tokens[0], "First token should be 'ls'"); - ASSERT_STR_EQUAL("-la", tokens[1], "Second token should be '-la'"); - ASSERT_STR_EQUAL("/home", tokens[2], "Third token should be '/home'"); - ASSERT_NULL(tokens[3], "Fourth token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_empty() { - char *line = strdup(" \t\n "); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL for empty input"); - ASSERT_NULL(tokens[0], "First token should be NULL for empty input"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_single_command() { - char *line = strdup("pwd"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("pwd", tokens[0], "First token should be 'pwd'"); - ASSERT_NULL(tokens[1], "Second token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_multiple_spaces() { - char *line = strdup("echo hello world"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello", tokens[1], "Second token should be 'hello'"); - ASSERT_STR_EQUAL("world", tokens[2], "Third token should be 'world'"); - ASSERT_NULL(tokens[3], "Fourth token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_tabs() { - char *line = strdup("cat\tfile.txt\tfile2.txt"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("cat", tokens[0], "First token should be 'cat'"); - ASSERT_STR_EQUAL("file.txt", tokens[1], "Second token should be 'file.txt'"); - ASSERT_STR_EQUAL("file2.txt", tokens[2], "Third token should be 'file2.txt'"); - ASSERT_NULL(tokens[3], "Fourth token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -// ==================== DOUBLE QUOTE TESTS ==================== - -void test_split_line_double_quotes_basic() { - char *line = strdup("echo \"hello world\""); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello world", tokens[1], - "Second token should be 'hello world' (with space preserved)"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_double_quotes_empty() { - char *line = strdup("echo \"\""); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("", tokens[1], "Second token should be empty string"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_double_quotes_multiple() { - char *line = strdup("echo \"hello\" \"world\""); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello", tokens[1], "Second token should be 'hello'"); - ASSERT_STR_EQUAL("world", tokens[2], "Third token should be 'world'"); - ASSERT_NULL(tokens[3], "Fourth token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_double_quotes_with_tabs() { - char *line = strdup("echo \"hello\tworld\""); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello\tworld", tokens[1], "Second token should preserve tab character"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_double_quotes_concatenated() { - char *line = strdup("echo hello\"world\"test"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("helloworldtest", tokens[1], - "Second token should be 'helloworldtest' (concatenated)"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -// ==================== SINGLE QUOTE TESTS ==================== - -void test_split_line_single_quotes_basic() { - char *line = strdup("echo 'hello world'"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello world", tokens[1], - "Second token should be 'hello world' (with space preserved)"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_single_quotes_empty() { - char *line = strdup("echo ''"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("", tokens[1], "Second token should be empty string"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_single_quotes_with_double_quotes() { - char *line = strdup("echo 'hello \"world\"'"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello \"world\"", tokens[1], - "Second token should preserve double quotes inside single quotes"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_single_quotes_concatenated() { - char *line = strdup("echo test'hello world'end"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("testhello worldend", tokens[1], "Second token should be concatenated"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -// ==================== BACKSLASH ESCAPING TESTS ==================== - -void test_split_line_backslash_space() { - char *line = strdup("echo hello\\ world"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello world", tokens[1], - "Second token should be 'hello world' (escaped space)"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_backslash_multiple_spaces() { - char *line = strdup("echo hello\\ \\ world"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello world", tokens[1], "Second token should have two spaces"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_backslash_quote() { - char *line = strdup("echo \\\"hello\\\""); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("\"hello\"", tokens[1], "Second token should contain literal quotes"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_backslash_single_quote() { - char *line = strdup("echo \\'hello\\'"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("'hello'", tokens[1], "Second token should contain literal single quotes"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_backslash_backslash() { - char *line = strdup("echo \\\\test"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("\\test", tokens[1], "Second token should contain literal backslash"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_backslash_tab() { - char *line = strdup("echo hello\\\tworld"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello\tworld", tokens[1], "Second token should contain escaped tab"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_backslash_newline() { - char *line = strdup("echo hello\\\nworld"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello\nworld", tokens[1], "Second token should contain escaped newline"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -// ==================== MIXED QUOTING TESTS ==================== - -void test_split_line_mixed_quotes() { - char *line = strdup("echo \"hello 'world'\""); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello 'world'", tokens[1], - "Second token should preserve single quotes in double quotes"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_mixed_quotes_reversed() { - char *line = strdup("echo 'hello \"world\"'"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello \"world\"", tokens[1], - "Second token should preserve double quotes in single quotes"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_quotes_and_backslash() { - char *line = strdup("echo \"hello\\ world\""); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello world", tokens[1], "Second token should have escaped space in quotes"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_complex_mixed() { - char *line = strdup("cmd arg1 \"arg 2\" 'arg 3' arg\\ 4"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("cmd", tokens[0], "First token should be 'cmd'"); - ASSERT_STR_EQUAL("arg1", tokens[1], "Second token should be 'arg1'"); - ASSERT_STR_EQUAL("arg 2", tokens[2], "Third token should be 'arg 2'"); - ASSERT_STR_EQUAL("arg 3", tokens[3], "Fourth token should be 'arg 3'"); - ASSERT_STR_EQUAL("arg 4", tokens[4], "Fifth token should be 'arg 4'"); - ASSERT_NULL(tokens[5], "Sixth token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -// ==================== EDGE CASES ==================== - -void test_split_line_adjacent_quotes() { - char *line = strdup("echo \"hello\"\"world\""); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("helloworld", tokens[1], "Second token should be 'helloworld'"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_quote_at_start() { - char *line = strdup("\"echo\" hello"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("hello", tokens[1], "Second token should be 'hello'"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_only_quotes() { - char *line = strdup("\"\""); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("", tokens[0], "First token should be empty string"); - ASSERT_NULL(tokens[1], "Second token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_backslash_at_end() { - char *line = strdup("echo test\\"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("test", tokens[1], - "Second token should be 'test' (trailing backslash ignored or handled)"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_special_characters_in_quotes() { - char *line = strdup("echo \"!@#$%^&*()\""); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); - ASSERT_STR_EQUAL("!@#$%^&*()", tokens[1], "Second token should preserve special characters"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_path_with_spaces() { - char *line = strdup("cd \"/home/user/my documents\""); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("cd", tokens[0], "First token should be 'cd'"); - ASSERT_STR_EQUAL("/home/user/my documents", tokens[1], - "Second token should preserve path with spaces"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -void test_split_line_filename_with_escaped_spaces() { - char *line = strdup("cat my\\ file\\ name.txt"); - char **tokens = trigger_split_line(line); - - ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); - ASSERT_STR_EQUAL("cat", tokens[0], "First token should be 'cat'"); - ASSERT_STR_EQUAL("my file name.txt", tokens[1], "Second token should be filename with spaces"); - ASSERT_NULL(tokens[2], "Third token should be NULL"); - - free_array_of_strings(tokens); - free(line); -} - -int main() { - TEST_SUITE_START("Input Module Tests"); - - // Basic tests - test_split_line_basic(); - test_split_line_empty(); - test_split_line_single_command(); - test_split_line_multiple_spaces(); - test_split_line_tabs(); - - // Double quote tests - test_split_line_double_quotes_basic(); - test_split_line_double_quotes_empty(); - test_split_line_double_quotes_multiple(); - test_split_line_double_quotes_with_tabs(); - test_split_line_double_quotes_concatenated(); - - // Single quote tests - test_split_line_single_quotes_basic(); - test_split_line_single_quotes_empty(); - test_split_line_single_quotes_with_double_quotes(); - test_split_line_single_quotes_concatenated(); - - // Backslash escaping tests - test_split_line_backslash_space(); - test_split_line_backslash_multiple_spaces(); - test_split_line_backslash_quote(); - test_split_line_backslash_single_quote(); - test_split_line_backslash_backslash(); - test_split_line_backslash_tab(); - test_split_line_backslash_newline(); - - // Mixed quoting tests - test_split_line_mixed_quotes(); - test_split_line_mixed_quotes_reversed(); - test_split_line_quotes_and_backslash(); - test_split_line_complex_mixed(); - - // Edge cases - test_split_line_adjacent_quotes(); - test_split_line_quote_at_start(); - test_split_line_only_quotes(); - test_split_line_backslash_at_end(); - test_split_line_special_characters_in_quotes(); - test_split_line_path_with_spaces(); - test_split_line_filename_with_escaped_spaces(); - - TEST_SUITE_END(); -} +#include "../include/input.h" +#include "../src/utils/utils.h" +#include "test_framework.h" +#include +#include +#include + +void test_split_line_basic() { + char *line = strdup("ls -la /home"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("ls", tokens[0], "First token should be 'ls'"); + ASSERT_STR_EQUAL("-la", tokens[1], "Second token should be '-la'"); + ASSERT_STR_EQUAL("/home", tokens[2], "Third token should be '/home'"); + ASSERT_NULL(tokens[3], "Fourth token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_empty() { + char *line = strdup(" \t\n "); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL for empty input"); + ASSERT_NULL(tokens[0], "First token should be NULL for empty input"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_single_command() { + char *line = strdup("pwd"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("pwd", tokens[0], "First token should be 'pwd'"); + ASSERT_NULL(tokens[1], "Second token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_multiple_spaces() { + char *line = strdup("echo hello world"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello", tokens[1], "Second token should be 'hello'"); + ASSERT_STR_EQUAL("world", tokens[2], "Third token should be 'world'"); + ASSERT_NULL(tokens[3], "Fourth token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_tabs() { + char *line = strdup("cat\tfile.txt\tfile2.txt"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("cat", tokens[0], "First token should be 'cat'"); + ASSERT_STR_EQUAL("file.txt", tokens[1], "Second token should be 'file.txt'"); + ASSERT_STR_EQUAL("file2.txt", tokens[2], "Third token should be 'file2.txt'"); + ASSERT_NULL(tokens[3], "Fourth token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +// ==================== DOUBLE QUOTE TESTS ==================== + +void test_split_line_double_quotes_basic() { + char *line = strdup("echo \"hello world\""); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello world", tokens[1], + "Second token should be 'hello world' (with space preserved)"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_double_quotes_empty() { + char *line = strdup("echo \"\""); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("", tokens[1], "Second token should be empty string"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_double_quotes_multiple() { + char *line = strdup("echo \"hello\" \"world\""); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello", tokens[1], "Second token should be 'hello'"); + ASSERT_STR_EQUAL("world", tokens[2], "Third token should be 'world'"); + ASSERT_NULL(tokens[3], "Fourth token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_double_quotes_with_tabs() { + char *line = strdup("echo \"hello\tworld\""); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello\tworld", tokens[1], "Second token should preserve tab character"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_double_quotes_concatenated() { + char *line = strdup("echo hello\"world\"test"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("helloworldtest", tokens[1], + "Second token should be 'helloworldtest' (concatenated)"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +// ==================== SINGLE QUOTE TESTS ==================== + +void test_split_line_single_quotes_basic() { + char *line = strdup("echo 'hello world'"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello world", tokens[1], + "Second token should be 'hello world' (with space preserved)"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_single_quotes_empty() { + char *line = strdup("echo ''"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("", tokens[1], "Second token should be empty string"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_single_quotes_with_double_quotes() { + char *line = strdup("echo 'hello \"world\"'"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello \"world\"", tokens[1], + "Second token should preserve double quotes inside single quotes"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_single_quotes_concatenated() { + char *line = strdup("echo test'hello world'end"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("testhello worldend", tokens[1], "Second token should be concatenated"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +// ==================== BACKSLASH ESCAPING TESTS ==================== + +void test_split_line_backslash_space() { + char *line = strdup("echo hello\\ world"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello world", tokens[1], + "Second token should be 'hello world' (escaped space)"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_backslash_multiple_spaces() { + char *line = strdup("echo hello\\ \\ world"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello world", tokens[1], "Second token should have two spaces"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_backslash_quote() { + char *line = strdup("echo \\\"hello\\\""); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("\"hello\"", tokens[1], "Second token should contain literal quotes"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_backslash_single_quote() { + char *line = strdup("echo \\'hello\\'"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("'hello'", tokens[1], "Second token should contain literal single quotes"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_backslash_backslash() { + char *line = strdup("echo \\\\test"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("\\test", tokens[1], "Second token should contain literal backslash"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_backslash_tab() { + char *line = strdup("echo hello\\\tworld"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello\tworld", tokens[1], "Second token should contain escaped tab"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_backslash_newline() { + char *line = strdup("echo hello\\\nworld"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello\nworld", tokens[1], "Second token should contain escaped newline"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +// ==================== MIXED QUOTING TESTS ==================== + +void test_split_line_mixed_quotes() { + char *line = strdup("echo \"hello 'world'\""); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello 'world'", tokens[1], + "Second token should preserve single quotes in double quotes"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_mixed_quotes_reversed() { + char *line = strdup("echo 'hello \"world\"'"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello \"world\"", tokens[1], + "Second token should preserve double quotes in single quotes"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_quotes_and_backslash() { + char *line = strdup("echo \"hello\\ world\""); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello world", tokens[1], "Second token should have escaped space in quotes"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_complex_mixed() { + char *line = strdup("cmd arg1 \"arg 2\" 'arg 3' arg\\ 4"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("cmd", tokens[0], "First token should be 'cmd'"); + ASSERT_STR_EQUAL("arg1", tokens[1], "Second token should be 'arg1'"); + ASSERT_STR_EQUAL("arg 2", tokens[2], "Third token should be 'arg 2'"); + ASSERT_STR_EQUAL("arg 3", tokens[3], "Fourth token should be 'arg 3'"); + ASSERT_STR_EQUAL("arg 4", tokens[4], "Fifth token should be 'arg 4'"); + ASSERT_NULL(tokens[5], "Sixth token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +// ==================== EDGE CASES ==================== + +void test_split_line_adjacent_quotes() { + char *line = strdup("echo \"hello\"\"world\""); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("helloworld", tokens[1], "Second token should be 'helloworld'"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_quote_at_start() { + char *line = strdup("\"echo\" hello"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("hello", tokens[1], "Second token should be 'hello'"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_only_quotes() { + char *line = strdup("\"\""); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("", tokens[0], "First token should be empty string"); + ASSERT_NULL(tokens[1], "Second token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_backslash_at_end() { + char *line = strdup("echo test\\"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("test", tokens[1], + "Second token should be 'test' (trailing backslash ignored or handled)"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_special_characters_in_quotes() { + char *line = strdup("echo \"!@#$%^&*()\""); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL("!@#$%^&*()", tokens[1], "Second token should preserve special characters"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_path_with_spaces() { + char *line = strdup("cd \"/home/user/my documents\""); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("cd", tokens[0], "First token should be 'cd'"); + ASSERT_STR_EQUAL("/home/user/my documents", tokens[1], + "Second token should preserve path with spaces"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_filename_with_escaped_spaces() { + char *line = strdup("cat my\\ file\\ name.txt"); + char **tokens = trigger_split_line(line); + + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("cat", tokens[0], "First token should be 'cat'"); + ASSERT_STR_EQUAL("my file name.txt", tokens[1], "Second token should be filename with spaces"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +void test_split_line_long_token(void) { + char *long_str = malloc(10001); + ASSERT_NOT_NULL(long_str, "malloc should succeed"); + memset(long_str, 'x', 10000); + long_str[10000] = '\0'; + + char *line = malloc(10006); + ASSERT_NOT_NULL(line, "malloc should succeed"); + strcpy(line, "echo "); + strcat(line, long_str); + + char **tokens = trigger_split_line(line); + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + ASSERT_STR_EQUAL("echo", tokens[0], "First token should be 'echo'"); + ASSERT_STR_EQUAL(long_str, tokens[1], "Second token should be 10k x chars"); + ASSERT_NULL(tokens[2], "Third token should be NULL"); + + free_array_of_strings(tokens); + free(line); + free(long_str); +} + +void test_split_line_many_tokens(void) { + size_t buf_size = 1; + char *line = malloc(buf_size); + ASSERT_NOT_NULL(line, "malloc should succeed"); + line[0] = '\0'; + + for (int i = 0; i < 200; i++) { + char chunk[16]; + snprintf(chunk, sizeof(chunk), " t%d", i); + buf_size += strlen(chunk); + line = realloc(line, buf_size); + ASSERT_NOT_NULL(line, "realloc should succeed"); + strcat(line, chunk); + } + + char **tokens = trigger_split_line(line); + ASSERT_NOT_NULL(tokens, "trigger_split_line should return non-NULL"); + + ASSERT_STR_EQUAL("t0", tokens[0], "First token should be 't0'"); + + int count = 0; + while (tokens[count] != NULL) + count++; + ASSERT_TRUE(count >= 199, "should have at least 199 tokens"); + ASSERT_NULL(tokens[count], "Last should be NULL"); + + free_array_of_strings(tokens); + free(line); +} + +int main() { + TEST_SUITE_START("Input Module Tests"); + + // Basic tests + test_split_line_basic(); + test_split_line_empty(); + test_split_line_single_command(); + test_split_line_multiple_spaces(); + test_split_line_tabs(); + + // Double quote tests + test_split_line_double_quotes_basic(); + test_split_line_double_quotes_empty(); + test_split_line_double_quotes_multiple(); + test_split_line_double_quotes_with_tabs(); + test_split_line_double_quotes_concatenated(); + + // Single quote tests + test_split_line_single_quotes_basic(); + test_split_line_single_quotes_empty(); + test_split_line_single_quotes_with_double_quotes(); + test_split_line_single_quotes_concatenated(); + + // Backslash escaping tests + test_split_line_backslash_space(); + test_split_line_backslash_multiple_spaces(); + test_split_line_backslash_quote(); + test_split_line_backslash_single_quote(); + test_split_line_backslash_backslash(); + test_split_line_backslash_tab(); + test_split_line_backslash_newline(); + + // Mixed quoting tests + test_split_line_mixed_quotes(); + test_split_line_mixed_quotes_reversed(); + test_split_line_quotes_and_backslash(); + test_split_line_complex_mixed(); + + // Edge cases + test_split_line_adjacent_quotes(); + test_split_line_quote_at_start(); + test_split_line_only_quotes(); + test_split_line_backslash_at_end(); + test_split_line_special_characters_in_quotes(); + test_split_line_path_with_spaces(); + test_split_line_filename_with_escaped_spaces(); + test_split_line_long_token(); + test_split_line_many_tokens(); + + TEST_SUITE_END(); +} diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 2590b86..bee3168 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -1,224 +1,258 @@ -#include "../include/builtins.h" -#include "../include/execute.h" -#include "../include/input.h" -#include "../include/pipeline.h" -#include "../src/utils/utils.h" -#include "test_framework.h" -#include -#include -#include -#include -#include - -static char **make_tokens(const char *input) { - extern char **parse_line_with_quotes(const char *line, int **out_glob_eligible); - return parse_line_with_quotes(input, NULL); -} - -static void free_parse_result(PipelineStage *stages, int num_stages) { - for (int i = 0; i < num_stages; i++) { - free(stages[i].infile); - free(stages[i].outfile); - if (stages[i].argv != NULL) { - free_array_of_strings(stages[i].argv); - } - } - free(stages); -} - -static int run_command(const char *input) { - int *glob_eligible = NULL; - char **args = trigger_split_line_ex(input, &glob_eligible); - ASSERT_NOT_NULL(args, input); - int result = trigger_execute(&args, &glob_eligible); - free(glob_eligible); - if (args != NULL) { - free_array_of_strings(args); - } - return result; -} - -void test_parse_simple_pipe() { - char **tokens = make_tokens("ls | grep src"); - ASSERT_NOT_NULL(tokens, "tokens should not be NULL"); - - int num_stages = 0; - PipelineStage *stages = trigger_parse_pipeline(tokens, &num_stages); - free(tokens); - - ASSERT_EQUAL(2, num_stages, "should have 2 stages"); - ASSERT_STR_EQUAL("ls", stages[0].argv[0], "first stage command is ls"); - ASSERT_NULL(stages[0].argv[1], "first stage has one arg"); - ASSERT_STR_EQUAL("grep", stages[1].argv[0], "second stage command is grep"); - ASSERT_STR_EQUAL("src", stages[1].argv[1], "second stage has src arg"); - ASSERT_NULL(stages[1].argv[2], "second stage has two args"); - - free_parse_result(stages, num_stages); -} - -void test_parse_redirect_out() { - char **tokens = make_tokens("echo hello > /tmp/out.txt"); - ASSERT_NOT_NULL(tokens, "tokens should not be NULL"); - - int num_stages = 0; - PipelineStage *stages = trigger_parse_pipeline(tokens, &num_stages); - free(tokens); - - ASSERT_EQUAL(1, num_stages, "should have 1 stage"); - ASSERT_STR_EQUAL("echo", stages[0].argv[0], "command is echo"); - ASSERT_STR_EQUAL("hello", stages[0].argv[1], "arg is hello"); - ASSERT_NULL(stages[0].argv[2], "two args"); - ASSERT_STR_EQUAL("/tmp/out.txt", stages[0].outfile, "outfile is /tmp/out.txt"); - ASSERT_EQUAL(0, stages[0].append, "append should be 0"); - ASSERT_NULL(stages[0].infile, "no infile"); - - free_parse_result(stages, num_stages); -} - -void test_parse_redirect_append() { - char **tokens = make_tokens("echo hello >> /tmp/out.txt"); - ASSERT_NOT_NULL(tokens, "tokens should not be NULL"); - - int num_stages = 0; - PipelineStage *stages = trigger_parse_pipeline(tokens, &num_stages); - free(tokens); - - ASSERT_EQUAL(1, num_stages, "should have 1 stage"); - ASSERT_STR_EQUAL("/tmp/out.txt", stages[0].outfile, "outfile is /tmp/out.txt"); - ASSERT_EQUAL(1, stages[0].append, "append should be 1"); - - free_parse_result(stages, num_stages); -} - -void test_parse_redirect_in() { - char **tokens = make_tokens("wc -l < /tmp/input.txt"); - ASSERT_NOT_NULL(tokens, "tokens should not be NULL"); - - int num_stages = 0; - PipelineStage *stages = trigger_parse_pipeline(tokens, &num_stages); - free(tokens); - - ASSERT_EQUAL(1, num_stages, "should have 1 stage"); - ASSERT_STR_EQUAL("wc", stages[0].argv[0], "command is wc"); - ASSERT_STR_EQUAL("-l", stages[0].argv[1], "arg is -l"); - ASSERT_NULL(stages[0].argv[2], "two args"); - ASSERT_STR_EQUAL("/tmp/input.txt", stages[0].infile, "infile is /tmp/input.txt"); - ASSERT_NULL(stages[0].outfile, "no outfile"); - - free_parse_result(stages, num_stages); -} - -void test_parse_pipe_with_redirect() { - char **tokens = make_tokens("cat < /etc/hostname | wc -c > /tmp/count.txt"); - ASSERT_NOT_NULL(tokens, "tokens should not be NULL"); - - int num_stages = 0; - PipelineStage *stages = trigger_parse_pipeline(tokens, &num_stages); - free(tokens); - - ASSERT_EQUAL(2, num_stages, "should have 2 stages"); - ASSERT_STR_EQUAL("cat", stages[0].argv[0], "first stage: cat"); - ASSERT_STR_EQUAL("/etc/hostname", stages[0].infile, "first stage: infile"); - ASSERT_NULL(stages[0].outfile, "first stage: no outfile"); - ASSERT_STR_EQUAL("wc", stages[1].argv[0], "second stage: wc"); - ASSERT_STR_EQUAL("-c", stages[1].argv[1], "second stage: -c"); - ASSERT_STR_EQUAL("/tmp/count.txt", stages[1].outfile, "second stage: outfile"); - ASSERT_NULL(stages[1].infile, "second stage: no infile"); - - free_parse_result(stages, num_stages); -} - -void test_e2e_redirect_out() { - unlink("/tmp/trigger_t_e2e_out"); - - int result = run_command("/bin/echo test_output > /tmp/trigger_t_e2e_out"); - ASSERT_TRUE(result, "echo with redirect should return true"); - - FILE *f = fopen("/tmp/trigger_t_e2e_out", "r"); - ASSERT_NOT_NULL(f, "output file should exist"); - - char buf[256]; - ASSERT_NOT_NULL(fgets(buf, sizeof(buf), f), "should read from output file"); - fclose(f); - - ASSERT_STR_EQUAL("test_output\n", buf, "output file should contain test_output"); - unlink("/tmp/trigger_t_e2e_out"); -} - -void test_e2e_redirect_append() { - unlink("/tmp/trigger_t_e2e_append"); - - int result = run_command("/bin/echo first > /tmp/trigger_t_e2e_append"); - ASSERT_TRUE(result, "first redirect should succeed"); - - result = run_command("/bin/echo second >> /tmp/trigger_t_e2e_append"); - ASSERT_TRUE(result, "append redirect should succeed"); - - FILE *f = fopen("/tmp/trigger_t_e2e_append", "r"); - ASSERT_NOT_NULL(f, "append file should exist"); - - char b1[256], b2[256]; - ASSERT_NOT_NULL(fgets(b1, sizeof(b1), f), "should read first line"); - ASSERT_NOT_NULL(fgets(b2, sizeof(b2), f), "should read second line"); - fclose(f); - - ASSERT_STR_EQUAL("first\n", b1, "first line should be first"); - ASSERT_STR_EQUAL("second\n", b2, "second line should be second"); - unlink("/tmp/trigger_t_e2e_append"); -} - -void test_e2e_redirect_in() { - FILE *f = fopen("/tmp/trigger_t_e2e_in", "w"); - fprintf(f, "hello\nworld\n"); - fclose(f); - - int result = run_command("/usr/bin/wc -l < /tmp/trigger_t_e2e_in"); - ASSERT_TRUE(result, "input redirect should return true"); - unlink("/tmp/trigger_t_e2e_in"); -} - -void test_e2e_pipe() { - int result = run_command("/bin/echo hello | /bin/cat"); - ASSERT_TRUE(result, "pipe should succeed"); -} - -void test_e2e_builtin_in_pipeline() { - char cwd_before[4096]; - ASSERT_NOT_NULL(getcwd(cwd_before, sizeof(cwd_before)), "getcwd should succeed"); - - int result = run_command("cd /tmp | /bin/true"); - ASSERT_TRUE(result, "cd in pipe should succeed"); - - char cwd_after[4096]; - ASSERT_NOT_NULL(getcwd(cwd_after, sizeof(cwd_after)), "getcwd should succeed"); - ASSERT_STR_EQUAL(cwd_before, cwd_after, "cd in pipeline should not affect parent cwd"); -} - -void test_quoted_pipe_not_operator() { - int result = run_command("/bin/echo \"|\""); - ASSERT_TRUE(result, "echo of quoted pipe char should not be parsed as pipeline"); -} - -void test_quoted_redirect_not_operator() { - int result = run_command("/bin/echo \">\""); - ASSERT_TRUE(result, "echo of quoted > should not be parsed as redirect"); -} - -int main() { - TEST_SUITE_START("Pipeline Tests"); - - test_parse_simple_pipe(); - test_parse_redirect_out(); - test_parse_redirect_append(); - test_parse_redirect_in(); - test_parse_pipe_with_redirect(); - test_e2e_redirect_out(); - test_e2e_redirect_append(); - test_e2e_redirect_in(); - test_e2e_pipe(); - test_e2e_builtin_in_pipeline(); - test_quoted_pipe_not_operator(); - test_quoted_redirect_not_operator(); - - TEST_SUITE_END(); -} +#include "../include/builtins.h" +#include "../include/execute.h" +#include "../include/input.h" +#include "../include/pipeline.h" +#include "../src/utils/utils.h" +#include "test_framework.h" +#include +#include +#include +#include +#include + +static TokenList *make_tokenlist(const char *input) { return parse_line_with_quotes(input); } + +static void free_parse_result(PipelineStage *stages, int num_stages) { + trigger_free_pipeline(stages, num_stages); +} + +static int run_command(const char *input) { + TokenList *tl = parse_line_with_quotes(input); + ASSERT_NOT_NULL(tl, input); + ExecuteResult r = trigger_execute(tl); + token_list_free(tl); + return r.status == 0; +} + +void test_parse_simple_pipe() { + TokenList *tl = make_tokenlist("ls | grep src"); + ASSERT_NOT_NULL(tl, "tokens should not be NULL"); + + int num_stages = 0; + PipelineStage *stages = trigger_parse_pipeline(tl->argv, tl->glob_eligible, &num_stages); + token_list_free(tl); + + ASSERT_EQUAL(2, num_stages, "should have 2 stages"); + ASSERT_STR_EQUAL("ls", stages[0].argv[0], "first stage command is ls"); + ASSERT_NULL(stages[0].argv[1], "first stage has one arg"); + ASSERT_STR_EQUAL("grep", stages[1].argv[0], "second stage command is grep"); + ASSERT_STR_EQUAL("src", stages[1].argv[1], "second stage has src arg"); + ASSERT_NULL(stages[1].argv[2], "second stage has two args"); + + free_parse_result(stages, num_stages); +} + +void test_parse_redirect_out() { + TokenList *tl = make_tokenlist("echo hello > /tmp/out.txt"); + ASSERT_NOT_NULL(tl, "tokens should not be NULL"); + + int num_stages = 0; + PipelineStage *stages = trigger_parse_pipeline(tl->argv, tl->glob_eligible, &num_stages); + token_list_free(tl); + + ASSERT_EQUAL(1, num_stages, "should have 1 stage"); + ASSERT_STR_EQUAL("echo", stages[0].argv[0], "command is echo"); + ASSERT_STR_EQUAL("hello", stages[0].argv[1], "arg is hello"); + ASSERT_NULL(stages[0].argv[2], "two args"); + ASSERT_STR_EQUAL("/tmp/out.txt", stages[0].outfile, "outfile is /tmp/out.txt"); + ASSERT_EQUAL(0, stages[0].append, "append should be 0"); + ASSERT_NULL(stages[0].infile, "no infile"); + + free_parse_result(stages, num_stages); +} + +void test_parse_redirect_append() { + TokenList *tl = make_tokenlist("echo hello >> /tmp/out.txt"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + + int num_stages = 0; + PipelineStage *stages = trigger_parse_pipeline(tl->argv, tl->glob_eligible, &num_stages); + token_list_free(tl); + + ASSERT_EQUAL(1, num_stages, "should have 1 stage"); + ASSERT_STR_EQUAL("/tmp/out.txt", stages[0].outfile, "outfile is /tmp/out.txt"); + ASSERT_EQUAL(1, stages[0].append, "append should be 1"); + + free_parse_result(stages, num_stages); +} + +void test_parse_redirect_in() { + TokenList *tl = make_tokenlist("wc -l < /tmp/input.txt"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + + int num_stages = 0; + PipelineStage *stages = trigger_parse_pipeline(tl->argv, tl->glob_eligible, &num_stages); + token_list_free(tl); + + ASSERT_EQUAL(1, num_stages, "should have 1 stage"); + ASSERT_STR_EQUAL("wc", stages[0].argv[0], "command is wc"); + ASSERT_STR_EQUAL("-l", stages[0].argv[1], "arg is -l"); + ASSERT_NULL(stages[0].argv[2], "two args"); + ASSERT_STR_EQUAL("/tmp/input.txt", stages[0].infile, "infile is /tmp/input.txt"); + ASSERT_NULL(stages[0].outfile, "no outfile"); + + free_parse_result(stages, num_stages); +} + +void test_parse_pipe_with_redirect() { + TokenList *tl = make_tokenlist("cat < /etc/hostname | wc -c > /tmp/count.txt"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + + int num_stages = 0; + PipelineStage *stages = trigger_parse_pipeline(tl->argv, tl->glob_eligible, &num_stages); + token_list_free(tl); + + ASSERT_EQUAL(2, num_stages, "should have 2 stages"); + ASSERT_STR_EQUAL("cat", stages[0].argv[0], "first stage: cat"); + ASSERT_STR_EQUAL("/etc/hostname", stages[0].infile, "first stage: infile"); + ASSERT_NULL(stages[0].outfile, "first stage: no outfile"); + ASSERT_STR_EQUAL("wc", stages[1].argv[0], "second stage: wc"); + ASSERT_STR_EQUAL("-c", stages[1].argv[1], "second stage: -c"); + ASSERT_STR_EQUAL("/tmp/count.txt", stages[1].outfile, "second stage: outfile"); + ASSERT_NULL(stages[1].infile, "second stage: no infile"); + + free_parse_result(stages, num_stages); +} + +void test_e2e_redirect_out() { + unlink("/tmp/trigger_t_e2e_out"); + + int result = run_command("/bin/echo test_output > /tmp/trigger_t_e2e_out"); + ASSERT_TRUE(result, "echo with redirect should return true"); + + FILE *f = fopen("/tmp/trigger_t_e2e_out", "r"); + ASSERT_NOT_NULL(f, "output file should exist"); + + char buf[256]; + ASSERT_NOT_NULL(fgets(buf, sizeof(buf), f), "should read from output file"); + fclose(f); + + ASSERT_STR_EQUAL("test_output\n", buf, "output file should contain test_output"); + unlink("/tmp/trigger_t_e2e_out"); +} + +void test_e2e_redirect_append() { + unlink("/tmp/trigger_t_e2e_append"); + + int result = run_command("/bin/echo first > /tmp/trigger_t_e2e_append"); + ASSERT_TRUE(result, "first redirect should succeed"); + + result = run_command("/bin/echo second >> /tmp/trigger_t_e2e_append"); + ASSERT_TRUE(result, "append redirect should succeed"); + + FILE *f = fopen("/tmp/trigger_t_e2e_append", "r"); + ASSERT_NOT_NULL(f, "append file should exist"); + + char b1[256], b2[256]; + ASSERT_NOT_NULL(fgets(b1, sizeof(b1), f), "should read first line"); + ASSERT_NOT_NULL(fgets(b2, sizeof(b2), f), "should read second line"); + fclose(f); + + ASSERT_STR_EQUAL("first\n", b1, "first line should be first"); + ASSERT_STR_EQUAL("second\n", b2, "second line should be second"); + unlink("/tmp/trigger_t_e2e_append"); +} + +void test_e2e_redirect_in() { + FILE *f = fopen("/tmp/trigger_t_e2e_in", "w"); + fprintf(f, "hello\nworld\n"); + fclose(f); + + int result = run_command("/usr/bin/wc -l < /tmp/trigger_t_e2e_in"); + ASSERT_TRUE(result, "input redirect should return true"); + unlink("/tmp/trigger_t_e2e_in"); +} + +void test_e2e_pipe() { + int result = run_command("/bin/echo hello | /bin/cat"); + ASSERT_TRUE(result, "pipe should succeed"); +} + +void test_e2e_builtin_in_pipeline() { + char cwd_before[4096]; + ASSERT_NOT_NULL(getcwd(cwd_before, sizeof(cwd_before)), "getcwd should succeed"); + + int result = run_command("cd /tmp | /bin/true"); + ASSERT_TRUE(result, "cd in pipe should succeed"); + + char cwd_after[4096]; + ASSERT_NOT_NULL(getcwd(cwd_after, sizeof(cwd_after)), "getcwd should succeed"); + ASSERT_STR_EQUAL(cwd_before, cwd_after, "cd in pipeline should not affect parent cwd"); +} + +void test_quoted_pipe_not_operator() { + int result = run_command("/bin/echo \"|\""); + ASSERT_TRUE(result, "echo of quoted pipe char should not be parsed as pipeline"); +} + +void test_quoted_redirect_not_operator() { + int result = run_command("/bin/echo \">\""); + ASSERT_TRUE(result, "echo of quoted > should not be parsed as redirect"); +} + +void test_parse_redirect_missing_filename() { + TokenList *tl = make_tokenlist("cmd >"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + + int num_stages = 0; + PipelineStage *stages = trigger_parse_pipeline(tl->argv, tl->glob_eligible, &num_stages); + token_list_free(tl); + + ASSERT_NULL(stages, "parse should return NULL on missing filename"); + ASSERT_EQUAL(0, num_stages, "num_stages should be 0 on syntax error"); +} + +void test_parse_redirect_no_arg_pipe() { + TokenList *tl = make_tokenlist("cmd > | next"); + ASSERT_NOT_NULL(tl, "TokenList should not be NULL"); + + int num_stages = 0; + PipelineStage *stages = trigger_parse_pipeline(tl->argv, tl->glob_eligible, &num_stages); + token_list_free(tl); + + ASSERT_NULL(stages, "parse should return NULL on >| syntax error"); + ASSERT_EQUAL(0, num_stages, "num_stages should be 0 on error"); +} + +void test_quoted_pipe_split_correctly() { + int *glob_eligible = NULL; + char **args = trigger_split_line_ex("/bin/echo \"|\" | /bin/cat", &glob_eligible); + ASSERT_NOT_NULL(args, "args should not be NULL"); + + int num_stages = 0; + PipelineStage *stages = trigger_parse_pipeline(args, glob_eligible, &num_stages); + free(args); + free(glob_eligible); + + ASSERT_NOT_NULL(stages, "parse should succeed"); + ASSERT_EQUAL(2, num_stages, "should have 2 stages: echo with literal pipe | cat"); + ASSERT_STR_EQUAL("/bin/echo", stages[0].argv[0], "first stage cmd: echo"); + ASSERT_STR_EQUAL("|", stages[0].argv[1], "first stage arg: literal |"); + ASSERT_NULL(stages[0].argv[2], "first stage has 2 tokens"); + ASSERT_STR_EQUAL("/bin/cat", stages[1].argv[0], "second stage cmd: cat"); + ASSERT_NULL(stages[1].argv[1], "second stage has 1 token"); + + trigger_free_pipeline(stages, num_stages); +} + +int main() { + TEST_SUITE_START("Pipeline Tests"); + + test_parse_simple_pipe(); + test_parse_redirect_out(); + test_parse_redirect_append(); + test_parse_redirect_in(); + test_parse_pipe_with_redirect(); + test_e2e_redirect_out(); + test_e2e_redirect_append(); + test_e2e_redirect_in(); + test_e2e_pipe(); + test_e2e_builtin_in_pipeline(); + test_quoted_pipe_not_operator(); + test_quoted_redirect_not_operator(); + test_parse_redirect_missing_filename(); + test_parse_redirect_no_arg_pipe(); + test_quoted_pipe_split_correctly(); + + TEST_SUITE_END(); +}