From dadbebfceba8c00f7a9f5710ffe1339977bcd0a6 Mon Sep 17 00:00:00 2001 From: "william.valenduc" Date: Fri, 16 Jan 2026 21:48:27 +0000 Subject: [PATCH 01/17] feat(expansion): parse_var_name and tests --- src/expansion/expansion.c | 102 ++++++++++++++++------- src/expansion/expansion.h | 11 +++ tests/unit/expansion/parse_var.c | 138 +++++++++++++++++++++++++++++++ 3 files changed, 223 insertions(+), 28 deletions(-) create mode 100644 tests/unit/expansion/parse_var.c diff --git a/src/expansion/expansion.c b/src/expansion/expansion.c index fe28dd0..81867a1 100644 --- a/src/expansion/expansion.c +++ b/src/expansion/expansion.c @@ -1,3 +1,4 @@ +#define _POSIX_C_SOURCE 200809L #include #include #include @@ -6,13 +7,75 @@ #include "../utils/ast/ast.h" -// static size_t var_len(char *start) -// { -// char *iter = start; -// while (*iter != ' ' && *iter != 0) -// *iter++; -// return iter - start; -// } +static bool is_var_start_char(char c) +{ + return isalpha(c) || c == '_'; +} + +static bool is_var_char(char c) +{ + return isalnum(c) || c == '_'; +} + +static bool is_special_var_char(char c) +{ + return c == '@' || c == '*' || c == '?' || c == '$' || isdigit(c) + || c == '#'; +} + +size_t parse_var_name(char *str, char **res) +{ + char *brace = NULL; + size_t i = 1; // skip the '$' + + if (str[i] == '{' && str[i + 1] != 0 && str[i + 1] != '}') + { + if (is_special_var_char(str[i + 1]) && str[i + 2] == '}') + { + // Special variable like ${1}, ${?} + *res = strndup(str + i + 1, 1); + return 4; // length of ${X} + } + + brace = str + i; + i++; // skip the '{' + } + else if (is_special_var_char(str[i])) + { + *res = strndup(str + i, 1); + return 2; // length of $X + } + + if (!is_var_start_char(str[i])) + { + // Not a valid variable start + *res = NULL; + return 0; + } + + while (1) + { + if (str[i] == '}' && *brace == '{') + { + *res = strndup(str + 2, i - 2); + return i + 1; + } + else if (!is_var_char(str[i])) + { + if (brace != NULL) + { + // Missing closing '}' + *res = NULL; + return 0; + } + break; + } + i++; + } + + *res = strndup(str + 1, i - 1); + return i; +} struct ast_command *expand(struct ast_command *command) { @@ -34,21 +97,18 @@ struct ast_command *expand(struct ast_command *command) { if (in_quotes) { - // do nothing + continue; // do nothing } else if (str[i] == '\'') { + // remove quote in_quotes = !in_quotes; memmove(&str[i], &str[i + 1], strlen(&str[i + 1]) + 1); + i--; } else if (str[i] == '$' && str[i + 1] != 0 && !isspace(str[i + 1])) { - // size_t len = var_len(str + i + 1); - // char *end = str + i + len + 1; - // char c = *end; - // *end = 0; - // printf("var: %s\n", str + i + 1); - // *end = c; + // variable expansion } } @@ -71,17 +131,3 @@ struct ast_command *expand(struct ast_command *command) } return command; } - -// int main() -// { -// printf("Expansion module test\n"); -// struct ast_command ast_command; -// // char str[] = "echo Hello $?"; -// char str[] = "echo Hello $AE86"; -// ast_command.command = list_append(NULL, str); - -// struct ast_command *command2 = expand(&ast_command); -// printf("command2: %s\n", (char *)command2->command->data); - -// return 0; -// } diff --git a/src/expansion/expansion.h b/src/expansion/expansion.h index b10b198..756e75e 100644 --- a/src/expansion/expansion.h +++ b/src/expansion/expansion.h @@ -1,4 +1,15 @@ #ifndef EXPANSION_H #define EXPANSION_H +#include + +/** + * Parse a variable from a string starting with '$'. + * @param str The input string starting with '$'. It must start with '$'. + * @param res Pointer to a char pointer that will be set to the extracted + * variable name. + * @return The number of characters processed in the input string. + */ +size_t parse_var_name(char *str, char **res); + #endif /* ! EXPANSION_H */ diff --git a/tests/unit/expansion/parse_var.c b/tests/unit/expansion/parse_var.c new file mode 100644 index 0000000..f588415 --- /dev/null +++ b/tests/unit/expansion/parse_var.c @@ -0,0 +1,138 @@ +#include +#include +#include + +#include "../../../src/expansion/expansion.h" + +TestSuite(parse_var_name); + +// char *input = "$MY$VAR"; +// char *input = "$MY$VAR$"; +// char *input = "$MY$VAR${}"; +// char *input = "$MY$VAR${1}"; + +Test(parse_var_name, basic_variable) +{ + char *input = "$MY_VAR"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 7); + cr_expect_str_eq(extracted_var, "MY_VAR"); + free(extracted_var); +} + +Test(parse_var_name, multi_basic_variable) +{ + char *input = "$MY$VAR"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 3); + cr_expect_str_eq(extracted_var, "MY"); + free(extracted_var); + + input += r; + r = parse_var_name(input, &extracted_var); + + cr_expect(r == 4); + cr_expect_str_eq(extracted_var, "VAR"); + free(extracted_var); +} + +Test(parse_var_name, variable_with_braces) +{ + char *input = "${MY_VAR}"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 9); + cr_expect_str_eq(extracted_var, "MY_VAR"); + free(extracted_var); +} + +Test(parse_var_name, special_variable) +{ + char *input = "$1"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 2); + cr_expect_str_eq(extracted_var, "1"); + free(extracted_var); +} + +Test(parse_var_name, incomplete_braces) +{ + char *input = "${MY_VAR"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_var_name, empty_braces) +{ + char *input = "${}"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_var_name, dollar_sign_only) +{ + char *input = "$"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_var_name, variable_followed_by_dollar) +{ + char *input = "$MY$VAR$"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 3); + cr_expect_str_eq(extracted_var, "MY"); + free(extracted_var); + + input += r; + r = parse_var_name(input, &extracted_var); + + cr_expect(r == 4); + cr_expect_str_eq(extracted_var, "VAR"); + free(extracted_var); + + input += r; + r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_var_name, special_variable_followed_by_text) +{ + char *input = "$1VAR"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 2); + cr_expect_str_eq(extracted_var, "1"); + free(extracted_var); +} + +Test(parse_var_name, bad_variable_with_braces) +{ + char *input = "${1VAR}"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} From a4fcce530cb0153d3eae472826823d195ad491c3 Mon Sep 17 00:00:00 2001 From: matteo Date: Sat, 17 Jan 2026 10:43:23 +0100 Subject: [PATCH 02/17] rebased lexer on dev --- src/lexer/lexer.c | 82 +++++++++++++++++++++++++++++++++++------------ src/lexer/lexer.h | 26 +++++++++++---- 2 files changed, 82 insertions(+), 26 deletions(-) diff --git a/src/lexer/lexer.c b/src/lexer/lexer.c index 68366cf..1df0ec5 100644 --- a/src/lexer/lexer.c +++ b/src/lexer/lexer.c @@ -16,11 +16,10 @@ static bool at_beginning = true; static struct token *last_token; static struct token *current_token; - /* @brief: sets the current_token to [tok]. * this function is called by token_peek(). */ -static void update_current_token(struct token* tok) +static void update_current_token(struct token *tok) { current_token = tok; } @@ -30,7 +29,7 @@ static void update_current_token(struct token* tok) * this function is called by token_pop(). * */ -static void update_last_token(struct token* tok) +static void update_last_token(struct token *tok) { free_token(&last_token); last_token = tok; @@ -51,43 +50,86 @@ static void save_state(char *stream, ssize_t i, struct token *tok) /* @return: true if a special character from the grammar was found, * false otherwise. - * */ static bool is_special_char(char c) { - return c == '\'' || c == '\n' || c == ';' || c == EOF; + if (c == EOF) + return true; + + char special_chars[] = "\n'\"`;#|&\\$(){}<>*"; + return strchr(special_chars, c) != NULL; } /* @brief: if a special character is found at [begin], * [tok->token_type] is set accordingly - * */ static void set_token_spechar(struct token *tok, char *begin, ssize_t size) { if (size != 1) return; - if (begin[0] == EOF) + switch (begin[0]) { + case EOF: tok->type = TOKEN_EOF; - remaining_chars = 0; - } - else if (begin[0] == ';') - { - tok->type = TOKEN_NEWLINE; - } - else if (begin[0] == '\'') - { - tok->type = TOKEN_QUOTE; - } - else if (begin[0] == ';') - { + break; + case ';': tok->type = TOKEN_SEMICOLON; + break; + case '\n': + tok->type = TOKEN_NEWLINE; + break; + case '\'': + tok->type = TOKEN_QUOTE; + break; + case '"': + tok->type = TOKEN_DOUBLE_QUOTE; + break; + case '`': + tok->type = TOKEN_GRAVE; + break; + case '#': + tok->type = TOKEN_COMMENT; + break; + case '|': + tok->type = TOKEN_PIPE; + break; + case '&': + tok->type = TOKEN_AMPERSAND; + break; + case '\\': + tok->type = TOKEN_BACKSLASH; + break; + case '$': + tok->type = TOKEN_DOLLAR; + break; + case '(': + tok->type = TOKEN_LEFT_PAREN; + break; + case ')': + tok->type = TOKEN_RIGHT_PAREN; + break; + case '{': + tok->type = TOKEN_LEFT_BRACKET; + break; + case '}': + tok->type = TOKEN_RIGHT_BRACKET; + break; + case '<': + tok->type = TOKEN_LESS; + break; + case '>': + tok->type = TOKEN_GREATER; + break; + case '*': + tok->type = TOKEN_STAR; + break; + default: + break; } } /* @brief: if a keyword is found at [begin], * [tok->token_type] is set accordingly - * */ static void set_token_keyword(struct token *tok, char *begin, ssize_t size) { diff --git a/src/lexer/lexer.h b/src/lexer/lexer.h index 1b46523..31a329a 100644 --- a/src/lexer/lexer.h +++ b/src/lexer/lexer.h @@ -5,12 +5,29 @@ enum token_type { + // Special characters TOKEN_NULL = 0, TOKEN_EOF, TOKEN_WORD, TOKEN_NEWLINE, TOKEN_QUOTE, + TOKEN_DOUBLE_QUOTE, + TOKEN_GRAVE, TOKEN_SEMICOLON, + TOKEN_COMMENT, + TOKEN_PIPE, + TOKEN_AMPERSAND, + TOKEN_BACKSLASH, + TOKEN_DOLLAR, + TOKEN_LEFT_PAREN, + TOKEN_RIGHT_PAREN, + TOKEN_LEFT_BRACKET, + TOKEN_RIGHT_BRACKET, + TOKEN_LESS, + TOKEN_GREATER, + TOKEN_STAR, + + // Keywords TOKEN_IF, TOKEN_THEN, TOKEN_ELSE, @@ -28,7 +45,6 @@ struct token * @brief: returns the next (newly allocated) token without consuming it. * if end of input is reached, enters in EOF looping node, * returning only the same token of type TOKEN_EOF. - * */ struct token *peek_token(void); @@ -36,6 +52,7 @@ struct token *peek_token(void); * @brief: returns the next (newly allocated) token and consumes it. * if end of input is reached, returns a token of type TOKEN_EOF. * It also frees the last token created if there was one. + * * @warning: if the last returned token was a token EOF, it frees it * and returns NULL. This means that after peeking a token EOF * in the parser, there must be EXACTLY ONE call to pop_token(). @@ -43,10 +60,9 @@ struct token *peek_token(void); */ struct token *pop_token(void); -/* - * @warning: NOT IMPLEMENTED. +/* @note: maybe usefull for subshells. * - * @note: maybe usefull for subshells. + * @warning: NOT IMPLEMENTED. */ struct token *get_token_str(void); @@ -56,12 +72,10 @@ struct token *get_token_str(void); * The data contains [size] char, starting from [begin]. * * @return: NULL on error, a token otherwise. - * */ struct token *new_token(char *begin, ssize_t size); /* @brief: frees the token given in argument - * */ void free_token(struct token **tok); From f475f4d4ec4518f0b43c65f568f70ea23c5245b4 Mon Sep 17 00:00:00 2001 From: Guillem George Date: Fri, 16 Jan 2026 20:05:49 +0100 Subject: [PATCH 03/17] fix: updated main to a parse-execute loop and changed error codes from an enum to macros --- src/main.c | 58 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/src/main.c b/src/main.c index b664786..399b874 100644 --- a/src/main.c +++ b/src/main.c @@ -1,4 +1,5 @@ -// All the includes +// === Includes +#include #include #include @@ -7,22 +8,24 @@ #include "parser/parser.h" #include "utils/args/args.h" -enum return_codes -{ - SUCCESS = 0, - ERR_IO_BACKEND = 2, - ERR_MALLOC = 3 -}; +// === Error codes + +#define SUCCESS 0 +#define ERR_INPUT_PROCESSING 2 +#define ERR_MALLOC 3 +#define ERR_GENERIC 4 + +// === Functions int main(int argc, char **argv) { // Create the options struct (with argument handler) struct args_options options; - int r = args_handler(argc, argv, &options); - if (r != 0) + int return_code = args_handler(argc, argv, &options); + if (return_code != 0) { print_usage(stderr, argv[0]); - return ERR_IO_BACKEND; + return ERR_INPUT_PROCESSING; } // args_print(&options); @@ -36,33 +39,42 @@ int main(int argc, char **argv) } // Convert args_options to iob_context - r = iob_config_from_args(&options, io_context); - if (r != 0) + return_code = iob_config_from_args(&options, io_context); + if (return_code != 0) { fprintf(stderr, "Error: Failed to configure IO Backend from arguments\n"); free(io_context); - return ERR_IO_BACKEND; + return ERR_INPUT_PROCESSING; } // Init IO Backend (with the context struct) - r = iob_init(io_context); - if (r != 0) + return_code = iob_init(io_context); + if (return_code != 0) { fprintf(stderr, - "Error: IO Backend initialization failed with code %d\n", r); + "Error: IO Backend initialization failed with code %d\n", + return_code); free(io_context); - return ERR_IO_BACKEND; + return ERR_INPUT_PROCESSING; } free(io_context); - // Call the parser to get the AST - struct ast *command_ast = get_ast(); // We'll pass the options later + // Retrieve and build first AST + struct ast *command_ast = get_ast(); - // Call the executor with the AST - r = execution(command_ast); + // Main parse-execute loop + while (command_ast != NULL && command_ast->type != AST_END) + { + // Execute AST + return_code = execution(command_ast); - // Return the execution return code (last command executed) - return r; + // Retrieve and build next AST + command_ast = get_ast(); + } + if (command_ast == NULL) + return ERR_INPUT_PROCESSING; + + return return_code; } From 2c1f210d3711170cff4ae7bfc5f55b648018e222 Mon Sep 17 00:00:00 2001 From: matteo Date: Sat, 17 Jan 2026 11:06:50 +0100 Subject: [PATCH 04/17] fix(lexer): some memory leaks + removed useless [at_begining] variable --- src/lexer/lexer.c | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/lexer/lexer.c b/src/lexer/lexer.c index 1df0ec5..cbb2b1b 100644 --- a/src/lexer/lexer.c +++ b/src/lexer/lexer.c @@ -12,7 +12,6 @@ static char *end_last_token; static ssize_t remaining_chars; -static bool at_beginning = true; static struct token *last_token; static struct token *current_token; @@ -35,17 +34,19 @@ static void update_last_token(struct token *tok) last_token = tok; } -/* @brief: saves state for the next call to the the lexer. - * this function is called by token_pop(). - * +/* @brief: updates the current position in the stream. + * [stream] += [i] + * Also saves the last token sent (so we can free it afterwards). + * Also sets the current token to NULL. + * This function is called by token_pop(). */ static void save_state(char *stream, ssize_t i, struct token *tok) { remaining_chars -= i; end_last_token = stream + i; - at_beginning = false; update_last_token(tok); + update_current_token(NULL); } /* @return: true if a special character from the grammar was found, @@ -204,10 +205,9 @@ char *stream_init(void) { char *stream; - if (at_beginning) + if (last_token == NULL) // at the begining { remaining_chars = stream_read(&stream); - // at_beginning = true; } else { @@ -223,8 +223,8 @@ char *stream_init(void) struct token *peek_token(void) { - // EOF looping mode - if (current_token != NULL && current_token->type == TOKEN_EOF) + // we already created the upcoming token during the previous call to peek() + if (current_token != NULL) { return current_token; } From acbcb860a45ba9cb73d4a1452724b18dda471634 Mon Sep 17 00:00:00 2001 From: matteo Date: Sat, 17 Jan 2026 11:38:23 +0100 Subject: [PATCH 05/17] fix(lexer): memory leaks --- src/lexer/lexer.c | 139 +++++++++++++++++++++++++--------------------- 1 file changed, 75 insertions(+), 64 deletions(-) diff --git a/src/lexer/lexer.c b/src/lexer/lexer.c index cbb2b1b..28f098c 100644 --- a/src/lexer/lexer.c +++ b/src/lexer/lexer.c @@ -36,16 +36,16 @@ static void update_last_token(struct token *tok) /* @brief: updates the current position in the stream. * [stream] += [i] - * Also saves the last token sent (so we can free it afterwards). - * Also sets the current token to NULL. + * Also frees the last sent token, and sets it to current_token. + * Current token is then set to NULL. * This function is called by token_pop(). */ -static void save_state(char *stream, ssize_t i, struct token *tok) +static void save_state(char *stream, ssize_t i) { remaining_chars -= i; end_last_token = stream + i; - update_last_token(tok); + update_last_token(current_token); update_current_token(NULL); } @@ -70,62 +70,62 @@ static void set_token_spechar(struct token *tok, char *begin, ssize_t size) return; switch (begin[0]) { - case EOF: - tok->type = TOKEN_EOF; - break; - case ';': - tok->type = TOKEN_SEMICOLON; - break; - case '\n': - tok->type = TOKEN_NEWLINE; - break; - case '\'': - tok->type = TOKEN_QUOTE; - break; - case '"': - tok->type = TOKEN_DOUBLE_QUOTE; - break; - case '`': - tok->type = TOKEN_GRAVE; - break; - case '#': - tok->type = TOKEN_COMMENT; - break; - case '|': - tok->type = TOKEN_PIPE; - break; - case '&': - tok->type = TOKEN_AMPERSAND; - break; - case '\\': - tok->type = TOKEN_BACKSLASH; - break; - case '$': - tok->type = TOKEN_DOLLAR; - break; - case '(': - tok->type = TOKEN_LEFT_PAREN; - break; - case ')': - tok->type = TOKEN_RIGHT_PAREN; - break; - case '{': - tok->type = TOKEN_LEFT_BRACKET; - break; - case '}': - tok->type = TOKEN_RIGHT_BRACKET; - break; - case '<': - tok->type = TOKEN_LESS; - break; - case '>': - tok->type = TOKEN_GREATER; - break; - case '*': - tok->type = TOKEN_STAR; - break; - default: - break; + case EOF: + tok->type = TOKEN_EOF; + break; + case ';': + tok->type = TOKEN_SEMICOLON; + break; + case '\n': + tok->type = TOKEN_NEWLINE; + break; + case '\'': + tok->type = TOKEN_QUOTE; + break; + case '"': + tok->type = TOKEN_DOUBLE_QUOTE; + break; + case '`': + tok->type = TOKEN_GRAVE; + break; + case '#': + tok->type = TOKEN_COMMENT; + break; + case '|': + tok->type = TOKEN_PIPE; + break; + case '&': + tok->type = TOKEN_AMPERSAND; + break; + case '\\': + tok->type = TOKEN_BACKSLASH; + break; + case '$': + tok->type = TOKEN_DOLLAR; + break; + case '(': + tok->type = TOKEN_LEFT_PAREN; + break; + case ')': + tok->type = TOKEN_RIGHT_PAREN; + break; + case '{': + tok->type = TOKEN_LEFT_BRACKET; + break; + case '}': + tok->type = TOKEN_RIGHT_BRACKET; + break; + case '<': + tok->type = TOKEN_LESS; + break; + case '>': + tok->type = TOKEN_GREATER; + break; + case '*': + tok->type = TOKEN_STAR; + break; + default: + break; } } @@ -157,6 +157,10 @@ static void set_token_keyword(struct token *tok, char *begin, ssize_t size) tok->type = TOKEN_ELIF; } + // no keywords found. + if (tok->type == TOKEN_NULL) + return; + tok->data = calloc(size + 1, sizeof(char)); if (tok->data == NULL) return; @@ -255,9 +259,11 @@ struct token *peek_token(void) struct token *pop_token(void) { - if (last_token != NULL && last_token->type == TOKEN_EOF) + if (current_token != NULL && current_token->type == TOKEN_EOF) { + // we reached end of input, frees all the token still allocated. free_token(&last_token); + free_token(¤t_token); return NULL; } char *stream = stream_init(); @@ -279,8 +285,13 @@ struct token *pop_token(void) i++; } - struct token *tok = new_token(stream, i); - save_state(stream, i, tok); + // just in case peek() was not called before poping. + // (this should never happen) + if (current_token == NULL) + { + current_token = new_token(stream, i); + } + save_state(stream, i); - return tok; + return last_token; } From e8d10523ae0667e7a407c86540e6c34ed6e466d7 Mon Sep 17 00:00:00 2001 From: matteo Date: Sat, 17 Jan 2026 17:53:37 +0100 Subject: [PATCH 06/17] feat(testsuite): autotools working, testsuite and debug implemented and working --- README.md | 26 ++++---- src/Makefile.am | 44 ++++++++----- src/main.c | 2 +- tests/unit/lexer/lexer_tests.c | 116 +++++++++++++++++++++++---------- 4 files changed, 125 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index be08206..f9130ce 100644 --- a/README.md +++ b/README.md @@ -7,22 +7,26 @@ TODO ### Build -run this command: - `autoreconf --force --verbose --install` - -### Test -run this command: - `./configure CFLAGS='-std=c99 -Werror -Wall -Wextra -Wvla'` -then: +run theses commands: + `autoreconf --force --verbose --install && ./configure` `make` + `./src/42sh --help` + +### Testing -#### asan run this command: - `./configure CFLAGS='-std=c99 -Werror -Wall -Wextra -Wvla -g -fsanitize=address'` - -then: `make check` +#### debug (asan) + +run this command: + `./src/debug` + +#### testsuite + +run this command: + `./src/testsuite` + ## Authors - Matteo Flebus diff --git a/src/Makefile.am b/src/Makefile.am index 411f4cb..c5c19e7 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,4 +1,3 @@ -# define the subdirectories SUBDIRS = \ parser \ lexer \ @@ -9,6 +8,8 @@ SUBDIRS = \ bin_PROGRAMS = 42sh +42sh_CFLAGS = -std=c99 -Werror -Wall -Wextra -Wvla + 42sh_SOURCES = main.c 42sh_CPPFLAGS = -I%D% @@ -21,18 +22,31 @@ bin_PROGRAMS = 42sh execution/libexecution.a \ utils/libutils.a +# ================ TESTS ================ -################################################# Test -# -#42sh_asan_SOURCES = main.c -# -#42sh_asan_CPPFLAGS = -I%D% -# -#42sh_asan_LDADD = \ -# ast/lib_asan_ast.a \ -# parser/lib_asan_parser.a \ -# lexer/lib_asan_lexer.a \ -# io_backend/lib_asan_io_backend.a \ -# expansion/lib_asan_expansion.a \ -# execution/lib_asan_execution.a \ -# utils/lib_asan_utils.a +# ------------- Unit tests -------------- + +check_PROGRAMS = testsuite + +testsuite_CFLAGS = $(42sh_CFLAGS) +testsuite_CFLAGS += $(CRITERION_CFLAGS) + +testsuite_SOURCES = ../tests/unit/lexer/lexer_tests.c \ + ../tests/unit/utils/utils_tests.c + +testsuite_CPPFLAGS = $(42sh_CPPFLAGS) + +testsuite_LDADD = $(42sh_LDADD) $(CRITERION_LIBS) + +# ------------- asan debug -------------- + +check_PROGRAMS += debug + +debug_CFLAGS = $(42sh_CFLAGS) +debug_CFLAGS += -fsanitize=address -g + +debug_SOURCES = $(42sh_SOURCES) + +debug_CPPFLAGS = $(42sh_CPPFLAGS) + +debug_LDADD = $(42sh_LDADD) diff --git a/src/main.c b/src/main.c index 399b874..423a141 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,5 @@ // === Includes -#include +//#include #include #include diff --git a/tests/unit/lexer/lexer_tests.c b/tests/unit/lexer/lexer_tests.c index d9046d4..910a81a 100644 --- a/tests/unit/lexer/lexer_tests.c +++ b/tests/unit/lexer/lexer_tests.c @@ -6,51 +6,95 @@ #include #include "lexer/lexer.h" +#include "io_backend/io_backend.h" -TestSuite(token_creation); +TestSuite(peek_token); -Test(token_creation, basic) +Test(peek_token, basic_empty) { - char input[] = "Hello World"; + // simulates input + char command[] = ""; + struct iob_context context = + { + IOB_MODE_CMD, + command + }; + iob_init(&context); - char actual[] = new_token(input, 5); - char expected[] = "Hello"; - cr_expect(eq(str, actual, expected)); - free(actual); + // test + struct token *tok = peek_token(); + + // expected + enum token_type type_expected = TOKEN_EOF; + char *string_expected = NULL; + + cr_assert(eq(str, string_expected, tok->data)); + cr_assert(type_expected == tok->type); + + free_token(&tok); } -Test(token_creation, nul) +Test(peek_token, basic_word) { - char input[] = NULL; + // simulates input + char command[] = "hello"; + struct iob_context context = + { + IOB_MODE_CMD, + command + }; + iob_init(&context); - char actual[] = new_token(input, 5); - cr_expect(actual == NULL); + // test + struct token *tok = peek_token(); + + // expected + enum token_type type_expected = TOKEN_WORD; + char string_expected[] = "hello"; + + cr_assert(eq(str, string_expected, tok->data)); + cr_assert(type_expected == tok->type); + free_token(&tok); } - -Test(token_creation, too_large) +Test(peek_token, basic_words) { - char input[] = "Hel"; + // simulates input + char command[] = "echo hello there"; + struct iob_context context = + { + IOB_MODE_CMD, + command + }; + iob_init(&context); - char actual[] = new_token(input, 5); - cr_expect(actual == NULL); -} - -Test(token_creation, empty) -{ - char input[] = ""; - - char actual[] = new_token(input, 5); - cr_expect(actual == NULL); -} - -Test(token_creation, basic_long) -{ - char input[] = "Hello World! This project is a mini shell, I love BIG G."; - - char actual[] = new_token(input, 42); - char expected[] = calloc(42 + 1, sizeof(char)); - strncpy(input, expected, 42); - cr_expect(eq(str, actual, expected)); - free(actual); - free(expected); + // ======= echo + + struct token *tok = peek_token(); + + enum token_type type_expected = TOKEN_WORD; + char string_expected[] = "echo"; + + cr_assert(eq(str, string_expected, tok->data)); + cr_assert(type_expected == tok->type); + free_token(&tok); + + // ======= hello + + tok = peek_token(); + + char string_expected2[] = "hello"; + + cr_assert(eq(str, string_expected2, tok->data)); + cr_assert(type_expected == tok->type); + free_token(&tok); + + // ======= there + + tok = peek_token(); + + char string_expected3[] = "there"; + + cr_assert(eq(str, string_expected3, tok->data)); + cr_assert(type_expected == tok->type); + free_token(&tok); } From 446d1ebd7acd9ebb250347ec728c2c1c4b4e18e4 Mon Sep 17 00:00:00 2001 From: matteo Date: Sat, 17 Jan 2026 18:00:34 +0100 Subject: [PATCH 07/17] fix(main): remove unecessary unknown header --- src/main.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main.c b/src/main.c index 423a141..8a81efb 100644 --- a/src/main.c +++ b/src/main.c @@ -1,5 +1,4 @@ // === Includes -//#include #include #include From cf7825aaf017e5a274f4f2dfe80962ff4c088261 Mon Sep 17 00:00:00 2001 From: matteo Date: Sat, 17 Jan 2026 18:02:00 +0100 Subject: [PATCH 08/17] feat(gitignore): debug and testsuite --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 64c4a76..9cdb480 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # Project specific 42sh +testsuite +debug # Prerequisites *.d From e9b6d39760bc96a9c79d208f32d0460d82d5cdc8 Mon Sep 17 00:00:00 2001 From: Jean Herail Date: Sat, 17 Jan 2026 17:33:22 +0100 Subject: [PATCH 09/17] Implemented some ast handlings... ast lists, and_or, redirection and builtins --- src/execution/execution.c | 231 +++++++++++++++++++++++++++++++++++-- src/utils/Makefile.am | 2 + src/utils/ast/ast.c | 4 + src/utils/ast/ast.h | 2 + src/utils/ast/ast_and_or.c | 35 ++++++ src/utils/ast/ast_and_or.h | 23 ++++ src/utils/ast/ast_base.h | 6 +- src/utils/ast/ast_list.c | 5 +- src/utils/ast/ast_redir.c | 36 ++++++ src/utils/ast/ast_redir.h | 29 +++++ 10 files changed, 359 insertions(+), 14 deletions(-) create mode 100644 src/utils/ast/ast_and_or.c create mode 100644 src/utils/ast/ast_and_or.h create mode 100644 src/utils/ast/ast_redir.c create mode 100644 src/utils/ast/ast_redir.h diff --git a/src/execution/execution.c b/src/execution/execution.c index 0066681..33389cb 100644 --- a/src/execution/execution.c +++ b/src/execution/execution.c @@ -5,9 +5,14 @@ #include #include #include +#include +#include +#include #include "../utils/ast/ast.h" +// --- Helpers --- + /** * @brief converts a linked list of command arguments to an argv array. Don't * forget to free the result @@ -44,6 +49,94 @@ static char **list_to_argv(struct list *command_list) return argv; } +// --- Builtins --- + +static int builtin_echo(char **argv) +{ + bool newline = true; + int i = 1; + + if (argv[1] && strcmp(argv[1], "-n") == 0) + { + newline = false; + i++; + } + + for (; argv[i]; i++) + { + printf("%s", argv[i]); + if (argv[i + 1]) + printf(" "); + } + if (newline) + printf("\n"); + + fflush(stdout); + return 0; +} + +static int builtin_true(char **argv) +{ + (void)argv; + return 0; +} + +static int builtin_false(char **argv) +{ + (void)argv; + return 1; +} + +static int builtin_exit(char **argv) +{ + int exit_val = 0; + if (argv[1]) + exit_val = atoi(argv[1]); + exit(exit_val); + return exit_val; +} + +static int builtin_cd(char **argv) +{ + const char *path = argv[1]; + if (!path) + { + path = getenv("HOME"); + if (!path) + { + fprintf(stderr, "cd: HOME not set\n"); + return 1; + } + } + if (chdir(path) != 0) + { + perror("cd"); + return 1; + } + return 0; +} + +static int try_builtin(char **argv) +{ + if (!argv || !argv[0]) + return 0; + + if (strcmp(argv[0], "echo") == 0) + return builtin_echo(argv); + if (strcmp(argv[0], "true") == 0) + return builtin_true(argv); + if (strcmp(argv[0], "false") == 0) + return builtin_false(argv); + if (strcmp(argv[0], "exit") == 0) + return builtin_exit(argv); + if (strcmp(argv[0], "cd") == 0) + return builtin_cd(argv); + + return -1; +} + +// --- Execution Core --- + /** * @brief Executes a command represented by an ast_command structure * @@ -54,7 +147,7 @@ static int exec_command(struct ast_command *command) { if (!command || !(command->command)) { - return -1; + return 1; } char **argv = list_to_argv(command->command); @@ -62,23 +155,30 @@ static int exec_command(struct ast_command *command) if (!argv || !(argv[0])) { free(argv); - return -1; + return 0; + } + + int builtin_ret = try_builtin(argv); + if (builtin_ret != -1) + { + free(argv); + return builtin_ret; } pid_t pid = fork(); - if (pid < 0) // Fork failed + if (pid < 0) { perror("fork"); free(argv); - return -1; + return 1; } - if (pid == 0) // If child process + if (pid == 0) { execvp(argv[0], argv); - perror("execvp"); // If execvp returns, there was an error - exit(EXIT_FAILURE); // Exit child process + perror("execvp"); + _exit(127); } int status = 0; @@ -90,7 +190,7 @@ static int exec_command(struct ast_command *command) return WEXITSTATUS(status); } - return -1; // Should not happen + return 1; } /** @@ -113,7 +213,7 @@ int execution(struct ast *ast) } case AST_CMD: { struct ast_command *command = ast_get_command(ast); - return exec_command(command); // It's recursive + return exec_command(command); } case AST_IF: { struct ast_if *if_node = ast_get_if(ast); @@ -127,8 +227,117 @@ int execution(struct ast *ast) return execution(if_node->else_clause); } } + case AST_LIST: { + struct ast_list *list_node = ast_get_list(ast); + struct list *cur = list_node->children; + int ret = 0; + while (cur) + { + struct ast *child = (struct ast *)cur->data; + ret = execution(child); + cur = cur->next; + } + return ret; + } + case AST_AND_OR: { + struct ast_and_or *ao_node = ast_get_and_or(ast); + int left_ret = execution(ao_node->left); + + if (ao_node->type == AST_AND_OR_TYPE_AND) + { + if (left_ret == 0) + return execution(ao_node->right); + return left_ret; + } + else // OR + { + if (left_ret != 0) + return execution(ao_node->right); + return left_ret; + } + } + case AST_REDIR: { + struct ast_redir *redir = ast_get_redir(ast); + + int fd_target = redir->io_number; + if (fd_target == -1) + { + if (redir->type == AST_REDIR_TYPE_LESS || redir->type == AST_REDIR_TYPE_DLESS || redir->type == AST_REDIR_TYPE_LESSAND) + fd_target = 0; + else + fd_target = 1; + } + + int saved_fd = dup(fd_target); + + int new_fd = -1; + int flags = 0; + int mode = 0644; + + if (redir->type == AST_REDIR_TYPE_GREAT || redir->type == AST_REDIR_TYPE_CLOBBER) + { + flags = O_WRONLY | O_CREAT | O_TRUNC; + new_fd = open(redir->filename, flags, mode); + } + else if (redir->type == AST_REDIR_TYPE_DGREAT) + { + flags = O_WRONLY | O_CREAT | O_APPEND; + new_fd = open(redir->filename, flags, mode); + } + else if (redir->type == AST_REDIR_TYPE_LESS) + { + flags = O_RDONLY; + new_fd = open(redir->filename, flags); + } + else if (redir->type == AST_REDIR_TYPE_GREATAND || redir->type == AST_REDIR_TYPE_LESSAND) + { + // Simple fd duplication + new_fd = atoi(redir->filename); + // Verify new_fd is valid? dup2 will check. + if (dup2(new_fd, fd_target) == -1) + { + perror("dup2"); + if (saved_fd != -1) close(saved_fd); + return 1; + } + new_fd = -2; // Mark as "already duped" + } + + if (new_fd == -1) + { + perror("open"); + if (saved_fd != -1) close(saved_fd); + return 1; + } + + if (new_fd != -2) + { + if (dup2(new_fd, fd_target) == -1) + { + perror("dup2"); + close(new_fd); + if (saved_fd != -1) close(saved_fd); + return 1; + } + close(new_fd); + } + + int ret = execution(redir->child); + + if (saved_fd != -1) + { + dup2(saved_fd, fd_target); + close(saved_fd); + } + else + { + close(fd_target); + } + + return ret; + } default: { - return -1; // Should not happen + return 127; } } -} +} \ No newline at end of file diff --git a/src/utils/Makefile.am b/src/utils/Makefile.am index d1aa675..6cd107d 100644 --- a/src/utils/Makefile.am +++ b/src/utils/Makefile.am @@ -6,6 +6,8 @@ libutils_a_SOURCES = \ ast/ast_if.c \ ast/ast_command.c \ ast/ast_list.c \ + ast/ast_and_or.c \ + ast/ast_redir.c \ ast/ast_void.c \ ast/ast_end.c \ lists/lists.c \ diff --git a/src/utils/ast/ast.c b/src/utils/ast/ast.c index 2d0ce84..c4bece9 100644 --- a/src/utils/ast/ast.c +++ b/src/utils/ast/ast.c @@ -13,6 +13,10 @@ void ast_free(struct ast **node) ast_free_command(ast_get_command(*node)); else if (ast_is_list(*node)) ast_free_list(ast_get_list(*node)); + else if (ast_is_and_or(*node)) + ast_free_and_or(ast_get_and_or(*node)); + else if (ast_is_redir(*node)) + ast_free_redir(ast_get_redir(*node)); free(*node); *node = NULL; diff --git a/src/utils/ast/ast.h b/src/utils/ast/ast.h index 3984667..1062782 100644 --- a/src/utils/ast/ast.h +++ b/src/utils/ast/ast.h @@ -4,6 +4,8 @@ #include "utils/ast/ast_base.h" #include "utils/ast/ast_command.h" #include "utils/ast/ast_if.h" +#include "utils/ast/ast_and_or.h" +#include "utils/ast/ast_redir.h" #include "utils/ast/ast_list.h" #include "utils/ast/ast_end.h" #include "utils/ast/ast_void.h" diff --git a/src/utils/ast/ast_and_or.c b/src/utils/ast/ast_and_or.c new file mode 100644 index 0000000..d07eaf5 --- /dev/null +++ b/src/utils/ast/ast_and_or.c @@ -0,0 +1,35 @@ +#include "utils/ast/ast_and_or.h" +#include + +bool ast_is_and_or(struct ast *node) +{ + return node != NULL && node->type == AST_AND_OR; +} + +struct ast_and_or *ast_get_and_or(struct ast *node) +{ + if (ast_is_and_or(node)) + return (struct ast_and_or *)node->data; + return NULL; +} + +struct ast *ast_create_and_or(struct ast *left, struct ast *right, enum ast_and_or_type type) +{ + struct ast_and_or *and_or = malloc(sizeof(struct ast_and_or)); + if (!and_or) + return NULL; + and_or->left = left; + and_or->right = right; + and_or->type = type; + + return ast_create(AST_AND_OR, and_or); +} + +void ast_free_and_or(struct ast_and_or *and_or) +{ + if (!and_or) + return; + ast_free(&and_or->left); + ast_free(&and_or->right); + free(and_or); +} diff --git a/src/utils/ast/ast_and_or.h b/src/utils/ast/ast_and_or.h new file mode 100644 index 0000000..57a9668 --- /dev/null +++ b/src/utils/ast/ast_and_or.h @@ -0,0 +1,23 @@ +#ifndef AST_AND_OR_H +#define AST_AND_OR_H + +#include +#include "utils/ast/ast_base.h" + +enum ast_and_or_type { + AST_AND_OR_TYPE_AND, + AST_AND_OR_TYPE_OR +}; + +struct ast_and_or { + struct ast *left; + struct ast *right; + enum ast_and_or_type type; +}; + +bool ast_is_and_or(struct ast *node); +struct ast_and_or *ast_get_and_or(struct ast *node); +struct ast *ast_create_and_or(struct ast *left, struct ast *right, enum ast_and_or_type type); +void ast_free_and_or(struct ast_and_or *and_or); + +#endif /* ! AST_AND_OR_H */ diff --git a/src/utils/ast/ast_base.h b/src/utils/ast/ast_base.h index 432eb2c..e09b25d 100644 --- a/src/utils/ast/ast_base.h +++ b/src/utils/ast/ast_base.h @@ -8,6 +8,8 @@ enum ast_type AST_END, AST_LIST, AST_IF, + AST_AND_OR, + AST_REDIR, AST_VOID, AST_CMD }; @@ -20,7 +22,9 @@ struct ast * Data associated with this AST node. It can be one of the following: * - NULL (AST_END) * - struct ast_if* (AST_IF) - * - struct ast_cmd* (AST_CMD) + * - struct ast_command* (AST_CMD) + * - struct ast_and_or* (AST_AND_OR) + * - struct ast_redir* (AST_REDIR) */ void *data; }; diff --git a/src/utils/ast/ast_list.c b/src/utils/ast/ast_list.c index 95e7047..4fc87f5 100644 --- a/src/utils/ast/ast_list.c +++ b/src/utils/ast/ast_list.c @@ -29,7 +29,7 @@ void ast_free_list(struct ast_list *ast_list) if (ast_list == NULL) return; - list_deep_destroy(ast_list->children); + ast_list_deep_destroy(ast_list->children); free(ast_list); } @@ -41,7 +41,8 @@ void ast_list_deep_destroy(struct list *l) { next_elt = elt->next; - ast_free(elt->data); + struct ast *node = (struct ast *)elt->data; + ast_free(&node); free(elt); elt = next_elt; } diff --git a/src/utils/ast/ast_redir.c b/src/utils/ast/ast_redir.c new file mode 100644 index 0000000..c5eec63 --- /dev/null +++ b/src/utils/ast/ast_redir.c @@ -0,0 +1,36 @@ +#include "utils/ast/ast_redir.h" +#include + +bool ast_is_redir(struct ast *node) +{ + return node != NULL && node->type == AST_REDIR; +} + +struct ast_redir *ast_get_redir(struct ast *node) +{ + if (ast_is_redir(node)) + return (struct ast_redir *)node->data; + return NULL; +} + +struct ast *ast_create_redir(struct ast *child, char *filename, int io_number, enum ast_redir_type type) +{ + struct ast_redir *redir = malloc(sizeof(struct ast_redir)); + if (!redir) + return NULL; + redir->child = child; + redir->filename = filename; // Takes ownership? Usually yes in simple ASTs, or dup. Let's assume pointer copy for now, but user must manage memory. + redir->io_number = io_number; + redir->type = type; + + return ast_create(AST_REDIR, redir); +} + +void ast_free_redir(struct ast_redir *redir) +{ + if (!redir) + return; + ast_free(&redir->child); + free(redir->filename); + free(redir); +} diff --git a/src/utils/ast/ast_redir.h b/src/utils/ast/ast_redir.h new file mode 100644 index 0000000..5f5ffb5 --- /dev/null +++ b/src/utils/ast/ast_redir.h @@ -0,0 +1,29 @@ +#ifndef AST_REDIR_H +#define AST_REDIR_H + +#include +#include "utils/ast/ast_base.h" + +enum ast_redir_type { + AST_REDIR_TYPE_LESS, // < + AST_REDIR_TYPE_GREAT, // > + AST_REDIR_TYPE_DLESS, // << + AST_REDIR_TYPE_DGREAT, // >> + AST_REDIR_TYPE_LESSAND, // <& + AST_REDIR_TYPE_GREATAND, // >& + AST_REDIR_TYPE_CLOBBER // >| +}; + +struct ast_redir { + struct ast *child; + char *filename; + int io_number; // The FD being redirected (default -1 if not specified, implies 0 or 1 based on type) + enum ast_redir_type type; +}; + +bool ast_is_redir(struct ast *node); +struct ast_redir *ast_get_redir(struct ast *node); +struct ast *ast_create_redir(struct ast *child, char *filename, int io_number, enum ast_redir_type type); +void ast_free_redir(struct ast_redir *redir); + +#endif /* ! AST_REDIR_H */ From c2998825860180e900a75019e9feefb968824f0c Mon Sep 17 00:00:00 2001 From: matteo Date: Sat, 17 Jan 2026 19:11:55 +0100 Subject: [PATCH 10/17] fix: now compiles and works for simple commands + clang format --- README.md | 26 ++++---- src/Makefile.am | 8 +-- src/execution/execution.c | 49 ++++++++------- src/io_backend/io_backend.h | 1 + src/lexer/lexer.c | 112 ++++++++++++++++----------------- src/main.c | 5 ++ src/parser/parser.c | 2 +- src/parser/parsing_utils.c | 10 ++- src/parser/parsing_utils.h | 8 +-- src/utils/ast/ast.h | 8 +-- src/utils/ast/ast_and_or.c | 4 +- src/utils/ast/ast_and_or.h | 10 ++- src/utils/ast/ast_command.c | 2 +- src/utils/ast/ast_command.h | 2 +- src/utils/ast/ast_end.c | 2 +- src/utils/ast/ast_end.h | 2 +- src/utils/ast/ast_if.c | 2 +- src/utils/ast/ast_list.c | 6 +- src/utils/ast/ast_redir.c | 8 ++- src/utils/ast/ast_redir.h | 27 ++++---- src/utils/ast/ast_void.c | 2 +- src/utils/ast/ast_void.h | 2 +- tests/unit/lexer/lexer_tests.c | 20 ++---- 23 files changed, 168 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index f9130ce..be08206 100644 --- a/README.md +++ b/README.md @@ -7,26 +7,22 @@ TODO ### Build -run theses commands: - `autoreconf --force --verbose --install && ./configure` +run this command: + `autoreconf --force --verbose --install` + +### Test +run this command: + `./configure CFLAGS='-std=c99 -Werror -Wall -Wextra -Wvla'` +then: `make` - `./src/42sh --help` - -### Testing +#### asan run this command: + `./configure CFLAGS='-std=c99 -Werror -Wall -Wextra -Wvla -g -fsanitize=address'` + +then: `make check` -#### debug (asan) - -run this command: - `./src/debug` - -#### testsuite - -run this command: - `./src/testsuite` - ## Authors - Matteo Flebus diff --git a/src/Makefile.am b/src/Makefile.am index c5c19e7..bd5d691 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -28,8 +28,8 @@ bin_PROGRAMS = 42sh check_PROGRAMS = testsuite -testsuite_CFLAGS = $(42sh_CFLAGS) -testsuite_CFLAGS += $(CRITERION_CFLAGS) +#testsuite_CFLAGS = $(42sh_CFLAGS) +#testsuite_CFLAGS += $(CRITERION_CFLAGS) testsuite_SOURCES = ../tests/unit/lexer/lexer_tests.c \ ../tests/unit/utils/utils_tests.c @@ -42,8 +42,8 @@ testsuite_LDADD = $(42sh_LDADD) $(CRITERION_LIBS) check_PROGRAMS += debug -debug_CFLAGS = $(42sh_CFLAGS) -debug_CFLAGS += -fsanitize=address -g +#debug_CFLAGS = $(42sh_CFLAGS) +#debug_CFLAGS += -fsanitize=address -g debug_SOURCES = $(42sh_SOURCES) diff --git a/src/execution/execution.c b/src/execution/execution.c index 33389cb..a882397 100644 --- a/src/execution/execution.c +++ b/src/execution/execution.c @@ -1,13 +1,13 @@ #include "execution.h" +#include +#include #include #include +#include #include #include #include -#include -#include -#include #include "../utils/ast/ast.h" @@ -70,7 +70,7 @@ static int builtin_echo(char **argv) } if (newline) printf("\n"); - + fflush(stdout); return 0; } @@ -120,7 +120,7 @@ static int try_builtin(char **argv) { if (!argv || !argv[0]) return 0; - + if (strcmp(argv[0], "echo") == 0) return builtin_echo(argv); if (strcmp(argv[0], "true") == 0) @@ -242,7 +242,7 @@ int execution(struct ast *ast) case AST_AND_OR: { struct ast_and_or *ao_node = ast_get_and_or(ast); int left_ret = execution(ao_node->left); - + if (ao_node->type == AST_AND_OR_TYPE_AND) { if (left_ret == 0) @@ -258,38 +258,42 @@ int execution(struct ast *ast) } case AST_REDIR: { struct ast_redir *redir = ast_get_redir(ast); - + int fd_target = redir->io_number; if (fd_target == -1) { - if (redir->type == AST_REDIR_TYPE_LESS || redir->type == AST_REDIR_TYPE_DLESS || redir->type == AST_REDIR_TYPE_LESSAND) + if (redir->type == AST_REDIR_TYPE_LESS + || redir->type == AST_REDIR_TYPE_DLESS + || redir->type == AST_REDIR_TYPE_LESSAND) fd_target = 0; else fd_target = 1; } - + int saved_fd = dup(fd_target); - + int new_fd = -1; int flags = 0; int mode = 0644; - - if (redir->type == AST_REDIR_TYPE_GREAT || redir->type == AST_REDIR_TYPE_CLOBBER) + + if (redir->type == AST_REDIR_TYPE_GREAT + || redir->type == AST_REDIR_TYPE_CLOBBER) { flags = O_WRONLY | O_CREAT | O_TRUNC; new_fd = open(redir->filename, flags, mode); } else if (redir->type == AST_REDIR_TYPE_DGREAT) { - flags = O_WRONLY | O_CREAT | O_APPEND; - new_fd = open(redir->filename, flags, mode); + flags = O_WRONLY | O_CREAT | O_APPEND; + new_fd = open(redir->filename, flags, mode); } else if (redir->type == AST_REDIR_TYPE_LESS) { - flags = O_RDONLY; - new_fd = open(redir->filename, flags); + flags = O_RDONLY; + new_fd = open(redir->filename, flags); } - else if (redir->type == AST_REDIR_TYPE_GREATAND || redir->type == AST_REDIR_TYPE_LESSAND) + else if (redir->type == AST_REDIR_TYPE_GREATAND + || redir->type == AST_REDIR_TYPE_LESSAND) { // Simple fd duplication new_fd = atoi(redir->filename); @@ -297,16 +301,18 @@ int execution(struct ast *ast) if (dup2(new_fd, fd_target) == -1) { perror("dup2"); - if (saved_fd != -1) close(saved_fd); + if (saved_fd != -1) + close(saved_fd); return 1; } new_fd = -2; // Mark as "already duped" } - + if (new_fd == -1) { perror("open"); - if (saved_fd != -1) close(saved_fd); + if (saved_fd != -1) + close(saved_fd); return 1; } @@ -316,7 +322,8 @@ int execution(struct ast *ast) { perror("dup2"); close(new_fd); - if (saved_fd != -1) close(saved_fd); + if (saved_fd != -1) + close(saved_fd); return 1; } close(new_fd); diff --git a/src/io_backend/io_backend.h b/src/io_backend/io_backend.h index 7b9e9b2..413dced 100644 --- a/src/io_backend/io_backend.h +++ b/src/io_backend/io_backend.h @@ -2,6 +2,7 @@ #define IO_BACKEND_H #include + #include "utils/args/args.h" // Error codes diff --git a/src/lexer/lexer.c b/src/lexer/lexer.c index 28f098c..aae9cf8 100644 --- a/src/lexer/lexer.c +++ b/src/lexer/lexer.c @@ -70,62 +70,62 @@ static void set_token_spechar(struct token *tok, char *begin, ssize_t size) return; switch (begin[0]) { - case EOF: - tok->type = TOKEN_EOF; - break; - case ';': - tok->type = TOKEN_SEMICOLON; - break; - case '\n': - tok->type = TOKEN_NEWLINE; - break; - case '\'': - tok->type = TOKEN_QUOTE; - break; - case '"': - tok->type = TOKEN_DOUBLE_QUOTE; - break; - case '`': - tok->type = TOKEN_GRAVE; - break; - case '#': - tok->type = TOKEN_COMMENT; - break; - case '|': - tok->type = TOKEN_PIPE; - break; - case '&': - tok->type = TOKEN_AMPERSAND; - break; - case '\\': - tok->type = TOKEN_BACKSLASH; - break; - case '$': - tok->type = TOKEN_DOLLAR; - break; - case '(': - tok->type = TOKEN_LEFT_PAREN; - break; - case ')': - tok->type = TOKEN_RIGHT_PAREN; - break; - case '{': - tok->type = TOKEN_LEFT_BRACKET; - break; - case '}': - tok->type = TOKEN_RIGHT_BRACKET; - break; - case '<': - tok->type = TOKEN_LESS; - break; - case '>': - tok->type = TOKEN_GREATER; - break; - case '*': - tok->type = TOKEN_STAR; - break; - default: - break; + case EOF: + tok->type = TOKEN_EOF; + break; + case ';': + tok->type = TOKEN_SEMICOLON; + break; + case '\n': + tok->type = TOKEN_NEWLINE; + break; + case '\'': + tok->type = TOKEN_QUOTE; + break; + case '"': + tok->type = TOKEN_DOUBLE_QUOTE; + break; + case '`': + tok->type = TOKEN_GRAVE; + break; + case '#': + tok->type = TOKEN_COMMENT; + break; + case '|': + tok->type = TOKEN_PIPE; + break; + case '&': + tok->type = TOKEN_AMPERSAND; + break; + case '\\': + tok->type = TOKEN_BACKSLASH; + break; + case '$': + tok->type = TOKEN_DOLLAR; + break; + case '(': + tok->type = TOKEN_LEFT_PAREN; + break; + case ')': + tok->type = TOKEN_RIGHT_PAREN; + break; + case '{': + tok->type = TOKEN_LEFT_BRACKET; + break; + case '}': + tok->type = TOKEN_RIGHT_BRACKET; + break; + case '<': + tok->type = TOKEN_LESS; + break; + case '>': + tok->type = TOKEN_GREATER; + break; + case '*': + tok->type = TOKEN_STAR; + break; + default: + break; } } diff --git a/src/main.c b/src/main.c index 8a81efb..a3e9527 100644 --- a/src/main.c +++ b/src/main.c @@ -69,9 +69,14 @@ int main(int argc, char **argv) // Execute AST return_code = execution(command_ast); + ast_free(&command_ast); + // Retrieve and build next AST command_ast = get_ast(); } + + ast_free(&command_ast); + if (command_ast == NULL) return ERR_INPUT_PROCESSING; diff --git a/src/parser/parser.c b/src/parser/parser.c index 0cce2fb..cf7b052 100644 --- a/src/parser/parser.c +++ b/src/parser/parser.c @@ -7,8 +7,8 @@ #include #include "lexer/lexer.h" -#include "utils/lists/lists.h" #include "parser/parsing_utils.h" +#include "utils/lists/lists.h" // === Static functions // ... diff --git a/src/parser/parsing_utils.c b/src/parser/parsing_utils.c index c07653e..9ac32d8 100644 --- a/src/parser/parsing_utils.c +++ b/src/parser/parsing_utils.c @@ -1,9 +1,11 @@ +#define _POSIX_C_SOURCE 200809L // === Includes #include "parsing_utils.h" #include #include #include +#include #include "lexer/lexer.h" #include "utils/ast/ast.h" @@ -58,7 +60,8 @@ struct ast *parse_simple_command(void) while (!isterminator(token)) { token = POP_TOKEN(); - command_elements = list_append(command_elements, token->data); + char *word = strdup(token->data); + command_elements = list_append(command_elements, word); token = PEEK_TOKEN(); } @@ -86,6 +89,7 @@ struct ast *parse_list(void) } token = PEEK_TOKEN(); } + result_list = list_append(result_list, current_node); return ast_create_list(result_list); } @@ -163,7 +167,9 @@ struct ast *parse_compound_list(void) return NULL; } - command_elements = list_append(command_elements, token->data); + char *word = strdup(token->data); + + command_elements = list_append(command_elements, word); token = POP_TOKEN(); } diff --git a/src/parser/parsing_utils.h b/src/parser/parsing_utils.h index a380a85..6fe6b89 100644 --- a/src/parser/parsing_utils.h +++ b/src/parser/parsing_utils.h @@ -28,7 +28,7 @@ struct ast *parse_list(void); */ struct ast *parse_simple_command(void); -/* +/* */ struct ast *parse_if_rule(void); @@ -39,14 +39,14 @@ struct ast *parse_shell_command(void); /* @brief parses commands inside if/else clauses and returns the corresponding * AST list */ -struct ast* parse_compound_list(void); +struct ast *parse_compound_list(void); /* */ -struct ast* parse_and_or(void); +struct ast *parse_and_or(void); /* */ -struct ast* parse_else_clause(void); +struct ast *parse_else_clause(void); #endif /* ! PARSING_UTILS_H */ diff --git a/src/utils/ast/ast.h b/src/utils/ast/ast.h index 1062782..5a06cf8 100644 --- a/src/utils/ast/ast.h +++ b/src/utils/ast/ast.h @@ -1,13 +1,13 @@ #ifndef AST_H #define AST_H +#include "utils/ast/ast_and_or.h" #include "utils/ast/ast_base.h" #include "utils/ast/ast_command.h" -#include "utils/ast/ast_if.h" -#include "utils/ast/ast_and_or.h" -#include "utils/ast/ast_redir.h" -#include "utils/ast/ast_list.h" #include "utils/ast/ast_end.h" +#include "utils/ast/ast_if.h" +#include "utils/ast/ast_list.h" +#include "utils/ast/ast_redir.h" #include "utils/ast/ast_void.h" #endif /* ! AST_H */ diff --git a/src/utils/ast/ast_and_or.c b/src/utils/ast/ast_and_or.c index d07eaf5..6b6038b 100644 --- a/src/utils/ast/ast_and_or.c +++ b/src/utils/ast/ast_and_or.c @@ -1,4 +1,5 @@ #include "utils/ast/ast_and_or.h" + #include bool ast_is_and_or(struct ast *node) @@ -13,7 +14,8 @@ struct ast_and_or *ast_get_and_or(struct ast *node) return NULL; } -struct ast *ast_create_and_or(struct ast *left, struct ast *right, enum ast_and_or_type type) +struct ast *ast_create_and_or(struct ast *left, struct ast *right, + enum ast_and_or_type type) { struct ast_and_or *and_or = malloc(sizeof(struct ast_and_or)); if (!and_or) diff --git a/src/utils/ast/ast_and_or.h b/src/utils/ast/ast_and_or.h index 57a9668..e370558 100644 --- a/src/utils/ast/ast_and_or.h +++ b/src/utils/ast/ast_and_or.h @@ -2,14 +2,17 @@ #define AST_AND_OR_H #include + #include "utils/ast/ast_base.h" -enum ast_and_or_type { +enum ast_and_or_type +{ AST_AND_OR_TYPE_AND, AST_AND_OR_TYPE_OR }; -struct ast_and_or { +struct ast_and_or +{ struct ast *left; struct ast *right; enum ast_and_or_type type; @@ -17,7 +20,8 @@ struct ast_and_or { bool ast_is_and_or(struct ast *node); struct ast_and_or *ast_get_and_or(struct ast *node); -struct ast *ast_create_and_or(struct ast *left, struct ast *right, enum ast_and_or_type type); +struct ast *ast_create_and_or(struct ast *left, struct ast *right, + enum ast_and_or_type type); void ast_free_and_or(struct ast_and_or *and_or); #endif /* ! AST_AND_OR_H */ diff --git a/src/utils/ast/ast_command.c b/src/utils/ast/ast_command.c index 920b1ff..89ed379 100644 --- a/src/utils/ast/ast_command.c +++ b/src/utils/ast/ast_command.c @@ -1,8 +1,8 @@ #include "utils/ast/ast_command.h" -#include #include #include +#include #include "utils/lists/lists.h" diff --git a/src/utils/ast/ast_command.h b/src/utils/ast/ast_command.h index 08c62ba..b83bade 100644 --- a/src/utils/ast/ast_command.h +++ b/src/utils/ast/ast_command.h @@ -3,8 +3,8 @@ #include -#include "utils/lists/lists.h" #include "utils/ast/ast_base.h" +#include "utils/lists/lists.h" struct ast_command { diff --git a/src/utils/ast/ast_end.c b/src/utils/ast/ast_end.c index 3be1eeb..a74fdb3 100644 --- a/src/utils/ast/ast_end.c +++ b/src/utils/ast/ast_end.c @@ -1,8 +1,8 @@ #include "utils/ast/ast_end.h" -#include #include #include +#include bool ast_is_end(struct ast *node) { diff --git a/src/utils/ast/ast_end.h b/src/utils/ast/ast_end.h index f86e477..df24d8b 100644 --- a/src/utils/ast/ast_end.h +++ b/src/utils/ast/ast_end.h @@ -3,8 +3,8 @@ #include -#include "utils/lists/lists.h" #include "utils/ast/ast_base.h" +#include "utils/lists/lists.h" /** * Checks if the given AST node is of type AST_END. diff --git a/src/utils/ast/ast_if.c b/src/utils/ast/ast_if.c index 6a25a9c..400a390 100644 --- a/src/utils/ast/ast_if.c +++ b/src/utils/ast/ast_if.c @@ -1,8 +1,8 @@ #include "utils/ast/ast_if.h" -#include #include #include +#include struct ast *ast_create_if(struct ast *condition, struct ast *then_clause, struct ast *else_clause) diff --git a/src/utils/ast/ast_list.c b/src/utils/ast/ast_list.c index 4fc87f5..0455698 100644 --- a/src/utils/ast/ast_list.c +++ b/src/utils/ast/ast_list.c @@ -1,7 +1,7 @@ -#include "utils/ast/ast.h" - #include +#include "utils/ast/ast.h" + struct ast *ast_create_list(struct list *list) { struct ast_list *ast_list = malloc(sizeof(struct ast_list)); @@ -16,7 +16,7 @@ struct ast *ast_create_list(struct list *list) struct ast_list *ast_get_list(struct ast *node) { assert(node != NULL); - return (struct ast_list*)node->data; + return (struct ast_list *)node->data; } bool ast_is_list(struct ast *node) diff --git a/src/utils/ast/ast_redir.c b/src/utils/ast/ast_redir.c index c5eec63..e4c92f3 100644 --- a/src/utils/ast/ast_redir.c +++ b/src/utils/ast/ast_redir.c @@ -1,4 +1,5 @@ #include "utils/ast/ast_redir.h" + #include bool ast_is_redir(struct ast *node) @@ -13,13 +14,16 @@ struct ast_redir *ast_get_redir(struct ast *node) return NULL; } -struct ast *ast_create_redir(struct ast *child, char *filename, int io_number, enum ast_redir_type type) +struct ast *ast_create_redir(struct ast *child, char *filename, int io_number, + enum ast_redir_type type) { struct ast_redir *redir = malloc(sizeof(struct ast_redir)); if (!redir) return NULL; redir->child = child; - redir->filename = filename; // Takes ownership? Usually yes in simple ASTs, or dup. Let's assume pointer copy for now, but user must manage memory. + redir->filename = + filename; // Takes ownership? Usually yes in simple ASTs, or dup. Let's + // assume pointer copy for now, but user must manage memory. redir->io_number = io_number; redir->type = type; diff --git a/src/utils/ast/ast_redir.h b/src/utils/ast/ast_redir.h index 5f5ffb5..65be4a2 100644 --- a/src/utils/ast/ast_redir.h +++ b/src/utils/ast/ast_redir.h @@ -2,28 +2,33 @@ #define AST_REDIR_H #include + #include "utils/ast/ast_base.h" -enum ast_redir_type { - AST_REDIR_TYPE_LESS, // < - AST_REDIR_TYPE_GREAT, // > - AST_REDIR_TYPE_DLESS, // << - AST_REDIR_TYPE_DGREAT, // >> - AST_REDIR_TYPE_LESSAND, // <& - AST_REDIR_TYPE_GREATAND, // >& - AST_REDIR_TYPE_CLOBBER // >| +enum ast_redir_type +{ + AST_REDIR_TYPE_LESS, // < + AST_REDIR_TYPE_GREAT, // > + AST_REDIR_TYPE_DLESS, // << + AST_REDIR_TYPE_DGREAT, // >> + AST_REDIR_TYPE_LESSAND, // <& + AST_REDIR_TYPE_GREATAND, // >& + AST_REDIR_TYPE_CLOBBER // >| }; -struct ast_redir { +struct ast_redir +{ struct ast *child; char *filename; - int io_number; // The FD being redirected (default -1 if not specified, implies 0 or 1 based on type) + int io_number; // The FD being redirected (default -1 if not specified, + // implies 0 or 1 based on type) enum ast_redir_type type; }; bool ast_is_redir(struct ast *node); struct ast_redir *ast_get_redir(struct ast *node); -struct ast *ast_create_redir(struct ast *child, char *filename, int io_number, enum ast_redir_type type); +struct ast *ast_create_redir(struct ast *child, char *filename, int io_number, + enum ast_redir_type type); void ast_free_redir(struct ast_redir *redir); #endif /* ! AST_REDIR_H */ diff --git a/src/utils/ast/ast_void.c b/src/utils/ast/ast_void.c index 1396121..3368c7d 100644 --- a/src/utils/ast/ast_void.c +++ b/src/utils/ast/ast_void.c @@ -1,8 +1,8 @@ #include "utils/ast/ast_void.h" -#include #include #include +#include bool ast_is_void(struct ast *node) { diff --git a/src/utils/ast/ast_void.h b/src/utils/ast/ast_void.h index 866a8d8..6abaef8 100644 --- a/src/utils/ast/ast_void.h +++ b/src/utils/ast/ast_void.h @@ -3,8 +3,8 @@ #include -#include "utils/lists/lists.h" #include "utils/ast/ast_base.h" +#include "utils/lists/lists.h" /** * Checks if the given AST node is of type AST_VOID. diff --git a/tests/unit/lexer/lexer_tests.c b/tests/unit/lexer/lexer_tests.c index 910a81a..7cb84a5 100644 --- a/tests/unit/lexer/lexer_tests.c +++ b/tests/unit/lexer/lexer_tests.c @@ -5,8 +5,8 @@ #include #include -#include "lexer/lexer.h" #include "io_backend/io_backend.h" +#include "lexer/lexer.h" TestSuite(peek_token); @@ -14,11 +14,7 @@ Test(peek_token, basic_empty) { // simulates input char command[] = ""; - struct iob_context context = - { - IOB_MODE_CMD, - command - }; + struct iob_context context = { IOB_MODE_CMD, command }; iob_init(&context); // test @@ -38,11 +34,7 @@ Test(peek_token, basic_word) { // simulates input char command[] = "hello"; - struct iob_context context = - { - IOB_MODE_CMD, - command - }; + struct iob_context context = { IOB_MODE_CMD, command }; iob_init(&context); // test @@ -60,11 +52,7 @@ Test(peek_token, basic_words) { // simulates input char command[] = "echo hello there"; - struct iob_context context = - { - IOB_MODE_CMD, - command - }; + struct iob_context context = { IOB_MODE_CMD, command }; iob_init(&context); // ======= echo From 78068f53bd5a11ee671556bd5d7189bdc1629dea Mon Sep 17 00:00:00 2001 From: "william.valenduc" Date: Sat, 17 Jan 2026 22:45:27 +0000 Subject: [PATCH 11/17] feat(hash_map): hash_map --- src/utils/hash_map/hash_map.c | 173 ++++++++++++++++++++++++++++++++++ src/utils/hash_map/hash_map.h | 34 +++++++ 2 files changed, 207 insertions(+) create mode 100644 src/utils/hash_map/hash_map.c create mode 100644 src/utils/hash_map/hash_map.h diff --git a/src/utils/hash_map/hash_map.c b/src/utils/hash_map/hash_map.c new file mode 100644 index 0000000..dec698c --- /dev/null +++ b/src/utils/hash_map/hash_map.c @@ -0,0 +1,173 @@ +#include "hash_map.h" + +#include +#include +#include +#include +#include +#include + +/* +** Hash the key using FNV-1a 32 bits hash algorithm. +*/ +static size_t hash(const char *key) +{ + if (key == NULL) + return 0; + + uint32_t hash = 2166136261; // FNV offset basis + uint32_t prime = 16777619; // FNV prime + + while (*key != 0) + { + hash ^= *key; + hash *= prime; + key++; + } + + return hash; +} + +struct hash_map *hash_map_init(size_t size) +{ + struct hash_map *p = malloc(sizeof(struct hash_map)); + if (p != NULL) + { + p->data = calloc(size, sizeof(struct pair_list *)); + if (p->data == NULL) + { + free(p); + return NULL; + } + p->size = size; + } + return p; +} + +bool hash_map_insert(struct hash_map *hash_map, const char *key, void *value, + bool *updated) +{ + if (hash_map == NULL || hash_map->size == 0 || key == NULL) + return false; + + size_t h = hash(key); + struct pair_list **l = hash_map->data; + size_t i = h % hash_map->size; + + if (l[i] != NULL) + { + // check if key is in linked list + struct pair_list *iter = l[i]; + while (iter) + { + if (strcmp(iter->key, key) == 0) + { + // update + iter->value = value; + if (updated) + *updated = true; + return true; + } + iter = iter->next; + } + + // if not found => collision + } + struct pair_list *p = malloc(sizeof(struct pair_list)); + if (p == NULL) + return false; + + if (updated) + *updated = false; + + p->key = key; + p->value = value; + p->next = l[i]; + l[i] = p; + l[i] = p; + return true; +} + +void hash_map_free(struct hash_map *hash_map) +{ + struct pair_list *l; + struct pair_list *prev; + + if (hash_map) + { + for (size_t i = 0; i < hash_map->size; i++) + { + l = hash_map->data[i]; + while (l != NULL) + { + prev = l; + l = l->next; + free(prev); + } + } + free(hash_map->data); + free(hash_map); + } +} + +void hash_map_foreach(struct hash_map *hash_map, + void (*fn)(const char *, const void *)) +{ + if (hash_map == NULL) + return; + + struct pair_list *iter; + for (size_t i = 0; i < hash_map->size; i++) + { + iter = hash_map->data[i]; + while (iter != NULL) + { + fn(iter->key, iter->value); + iter = iter->next; + } + } +} + +const void *hash_map_get(const struct hash_map *hash_map, const char *key) +{ + if (hash_map == NULL || hash_map->size == 0) + return NULL; + + size_t h = hash(key); + size_t i = h % hash_map->size; + struct pair_list *l = hash_map->data[i]; + while (l != NULL) + { + if (strcmp(l->key, key) == 0) + return l->value; + l = l->next; + } + return NULL; +} + +bool hash_map_remove(struct hash_map *hash_map, const char *key) +{ + if (hash_map == NULL || hash_map->size == 0) + return false; + + size_t h = hash(key); + size_t i = h % hash_map->size; + struct pair_list *l = hash_map->data[i]; + struct pair_list *p = NULL; + + while (l != NULL) + { + if (strcmp(l->key, key) == 0) + { + if (p != NULL) + p->next = l->next; + else + hash_map->data[i] = l->next; + free(l); + return true; + } + p = l; + l = l->next; + } + return false; +} diff --git a/src/utils/hash_map/hash_map.h b/src/utils/hash_map/hash_map.h new file mode 100644 index 0000000..efdabc2 --- /dev/null +++ b/src/utils/hash_map/hash_map.h @@ -0,0 +1,34 @@ +#ifndef HASH_MAP_H +#define HASH_MAP_H + +#include +#include + +struct pair_list +{ + const char *key; + void *value; + struct pair_list *next; +}; + +struct hash_map +{ + struct pair_list **data; + size_t size; +}; + +struct hash_map *hash_map_init(size_t size); + +bool hash_map_insert(struct hash_map *hash_map, const char *key, void *value, + bool *updated); + +void hash_map_free(struct hash_map *hash_map); + +void hash_map_foreach(struct hash_map *hash_map, + void (*fn)(const char *, const void *)); + +const void *hash_map_get(const struct hash_map *hash_map, const char *key); + +bool hash_map_remove(struct hash_map *hash_map, const char *key); + +#endif /* ! HASH_MAP_H */ From 0c6c3571614b69a925ea832adecd87a7673d4602 Mon Sep 17 00:00:00 2001 From: "william.valenduc" Date: Sat, 17 Jan 2026 22:46:10 +0000 Subject: [PATCH 12/17] feat(vars): vars --- src/utils/vars/vars.c | 32 ++++++++++++++++++++++++++++++++ src/utils/vars/vars.h | 31 +++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/utils/vars/vars.c create mode 100644 src/utils/vars/vars.h diff --git a/src/utils/vars/vars.c b/src/utils/vars/vars.c new file mode 100644 index 0000000..70ab328 --- /dev/null +++ b/src/utils/vars/vars.c @@ -0,0 +1,32 @@ +#define _POSIX_C_SOURCE 200809L +#include "vars.h" + +#include +#include +#include + +#include "../hash_map/hash_map.h" + +#define VARS_INITIAL_SIZE 16 + +struct hash_map *vars_init(void) +{ + return hash_map_init(VARS_INITIAL_SIZE); +} + +char *get_var(const struct hash_map *vars, const char *key) +{ + return (char *)hash_map_get(vars, key); +} + +bool set_var(struct hash_map *vars, const char *key, const char *value) +{ + if (key == NULL || value == NULL) + return false; + return hash_map_insert(vars, key, (void *)value, NULL); +} + +bool set_var_copy(struct hash_map *vars, const char *key, const char *value) +{ + return set_var(vars, strdup(key), strdup(value)); +} diff --git a/src/utils/vars/vars.h b/src/utils/vars/vars.h new file mode 100644 index 0000000..97db704 --- /dev/null +++ b/src/utils/vars/vars.h @@ -0,0 +1,31 @@ +#ifndef VARS_H +#define VARS_H + +#include + +#include "../hash_map/hash_map.h" + +/** + * Initialize a new variables hash map. + */ +struct hash_map *vars_init(void); + +/** + * Get the value of a variable, NULL if not found. + */ +char *get_var(const struct hash_map *vars, const char *key); + +/** + * Set the value of a variable. Key and value ownership are transferred to + * the hash_map and need to be on the heap. Returns true on success, false on + * failure. + */ +bool set_var(struct hash_map *vars, const char *key, const char *value); + +/** + * Same as set_var, but makes copies of key and value so you don't have to worry + * about their memory management. Returns true on success, false on failure. + */ +bool set_var_copy(struct hash_map *vars, const char *key, const char *value); + +#endif /* ! VARS_H */ From cf5da6f231da6bb92ee280dc7bfcfbdc6b3cd152 Mon Sep 17 00:00:00 2001 From: "william.valenduc" Date: Sat, 17 Jan 2026 23:57:59 +0000 Subject: [PATCH 13/17] feat(hash_map): destroy_pair_list --- src/utils/hash_map/hash_map.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/utils/hash_map/hash_map.c b/src/utils/hash_map/hash_map.c index dec698c..46ac9d1 100644 --- a/src/utils/hash_map/hash_map.c +++ b/src/utils/hash_map/hash_map.c @@ -28,6 +28,14 @@ static size_t hash(const char *key) return hash; } +static void destroy_pair_list(struct pair_list **p) +{ + free((char *)(*p)->key); + free((*p)->value); + free((*p)); + *p = NULL; +} + struct hash_map *hash_map_init(size_t size) { struct hash_map *p = malloc(sizeof(struct hash_map)); @@ -102,7 +110,7 @@ void hash_map_free(struct hash_map *hash_map) { prev = l; l = l->next; - free(prev); + destroy_pair_list(&prev); } } free(hash_map->data); @@ -163,7 +171,7 @@ bool hash_map_remove(struct hash_map *hash_map, const char *key) p->next = l->next; else hash_map->data[i] = l->next; - free(l); + destroy_pair_list(&l); return true; } p = l; From 0c9c5dc1f8b2d89bff689cb2134de3082828e24b Mon Sep 17 00:00:00 2001 From: "william.valenduc" Date: Sat, 17 Jan 2026 23:58:15 +0000 Subject: [PATCH 14/17] feat(vars): get_var_or_env --- src/utils/vars/vars.c | 10 +++++++++- src/utils/vars/vars.h | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/utils/vars/vars.c b/src/utils/vars/vars.c index 70ab328..985e085 100644 --- a/src/utils/vars/vars.c +++ b/src/utils/vars/vars.c @@ -2,7 +2,7 @@ #include "vars.h" #include -#include +#include #include #include "../hash_map/hash_map.h" @@ -19,6 +19,14 @@ char *get_var(const struct hash_map *vars, const char *key) return (char *)hash_map_get(vars, key); } +char *get_var_or_env(const struct hash_map *vars, const char *key) +{ + char *value = (char *)hash_map_get(vars, key); + if (value == NULL) + value = getenv(key); + return value; +} + bool set_var(struct hash_map *vars, const char *key, const char *value) { if (key == NULL || value == NULL) diff --git a/src/utils/vars/vars.h b/src/utils/vars/vars.h index 97db704..de1c4c6 100644 --- a/src/utils/vars/vars.h +++ b/src/utils/vars/vars.h @@ -15,6 +15,12 @@ struct hash_map *vars_init(void); */ char *get_var(const struct hash_map *vars, const char *key); +/** + * Get the value of a variable, from the environment if not found in vars, + * NULL if not found in either. + */ +char *get_var_or_env(const struct hash_map *vars, const char *key); + /** * Set the value of a variable. Key and value ownership are transferred to * the hash_map and need to be on the heap. Returns true on success, false on From df7cc02eb914ddd0f47da136818f451e050fe639 Mon Sep 17 00:00:00 2001 From: "william.valenduc" Date: Sat, 17 Jan 2026 23:59:09 +0000 Subject: [PATCH 15/17] feat(string_utils): insert_into --- src/utils/string_utils/string_utils.c | 31 +++++++++- src/utils/string_utils/string_utils.h | 6 ++ tests/unit/utils/insert_into.c | 85 +++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 tests/unit/utils/insert_into.c diff --git a/src/utils/string_utils/string_utils.c b/src/utils/string_utils/string_utils.c index 8a8176a..e5b7040 100644 --- a/src/utils/string_utils/string_utils.c +++ b/src/utils/string_utils/string_utils.c @@ -1,7 +1,8 @@ #include "string_utils.h" #include -#include +#include +#include char *trim_blank_left(char *str) { @@ -13,3 +14,31 @@ char *trim_blank_left(char *str) return str; } + +char *insert_into(char *dest, const char *src, size_t pos, size_t len) +{ + size_t res_len = strlen(dest); + size_t prefix_len = pos; + size_t suffix_len = res_len - (pos + len); + size_t src_len = strlen(src); + size_t new_len = prefix_len + src_len + suffix_len; + + if (dest == NULL || src == NULL || pos + len > res_len) + return NULL; + + if (res_len < new_len) + { + char *p = realloc(dest, new_len + 1); + if (p == NULL) + return NULL; // allocation failure + dest = p; + } + + memmove(dest + pos + src_len, dest + pos + len, suffix_len); + memcpy(dest + pos, src, src_len); + dest[new_len] = 0; + + if (res_len > new_len) + return realloc(dest, new_len + 1); + return dest; +} diff --git a/src/utils/string_utils/string_utils.h b/src/utils/string_utils/string_utils.h index 496c1d5..e411f0e 100644 --- a/src/utils/string_utils/string_utils.h +++ b/src/utils/string_utils/string_utils.h @@ -12,4 +12,10 @@ */ char *trim_blank_left(char *str); +/** + * Inserts a substring into a destination string at a specified position, + * replacing a specified length of characters. + */ +char *insert_into(char *dest, const char *src, size_t pos, size_t len); + #endif /* STRING_UTILS_H */ diff --git a/tests/unit/utils/insert_into.c b/tests/unit/utils/insert_into.c new file mode 100644 index 0000000..0bcc833 --- /dev/null +++ b/tests/unit/utils/insert_into.c @@ -0,0 +1,85 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include + +#include "../../../src/utils/string_utils/string_utils.h" + +TestSuite(insert_into); + +Test(insert_into, basic) +{ + char *dest = strdup("The is nice."); + const char *src = "weather"; + size_t pos = 4; + + char *result = insert_into(dest, src, pos, 6); + + cr_expect(result != NULL); + cr_expect(eq(str, result, "The weather is nice.")); + + if (result) + free(result); +} + +Test(insert_into, begin) +{ + char *dest = strdup("Hello World!"); + const char *src = "Hi"; + size_t pos = 0; + + char *result = insert_into(dest, src, pos, 5); + + cr_expect(result != NULL); + cr_expect(eq(str, result, "Hi World!")); + + if (result) + free(result); +} + +Test(insert_into, end) +{ + char *dest = strdup("The number is 1024"); + const char *src = "2048"; + size_t pos = 14; + + char *result = insert_into(dest, src, pos, 4); + + cr_expect(result != NULL); + cr_expect(eq(str, result, "The number is 2048")); + + if (result) + free(result); +} + +Test(insert_into, big) +{ + char *dest = strdup("I could insert [VAR] here."); + const char *src = "a very very long string"; + size_t pos = 15; + + char *result = insert_into(dest, src, pos, 5); + + cr_expect(result != NULL); + cr_expect(eq(str, result, "I could insert a very very long string here.")); + + if (result) + free(result); +} + +Test(insert_into, small) +{ + char *dest = strdup("I could insert [VARNAME_IS_SO_LONG] string here."); + const char *src = "a short"; + size_t pos = 15; + + char *result = insert_into(dest, src, pos, 20); + + cr_expect(result != NULL); + cr_expect(eq(str, result, "I could insert a short string here.")); + + if (result) + free(result); +} From a16712e802d4600afe818c8211569798d767ceda Mon Sep 17 00:00:00 2001 From: "william.valenduc" Date: Sun, 18 Jan 2026 00:29:31 +0000 Subject: [PATCH 16/17] feat(expansion): special_variable_with_braces test --- tests/unit/expansion/parse_var.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/expansion/parse_var.c b/tests/unit/expansion/parse_var.c index f588415..ce4c11a 100644 --- a/tests/unit/expansion/parse_var.c +++ b/tests/unit/expansion/parse_var.c @@ -62,6 +62,17 @@ Test(parse_var_name, special_variable) free(extracted_var); } +Test(parse_var_name, special_variable_with_braces) +{ + char *input = "${1}"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 4); + cr_expect_str_eq(extracted_var, "1"); + free(extracted_var); +} + Test(parse_var_name, incomplete_braces) { char *input = "${MY_VAR"; From 34c741d86ec531de924b53706aa5b7a9bc4440d2 Mon Sep 17 00:00:00 2001 From: "william.valenduc" Date: Sun, 18 Jan 2026 01:27:31 +0000 Subject: [PATCH 17/17] feat(expansion): expand_var and expand --- src/expansion/expansion.c | 54 ++++++-- src/expansion/expansion.h | 11 ++ tests/unit/expansion/expand.c | 220 +++++++++++++++++++++++++++++++ tests/unit/expansion/parse_var.c | 5 - 4 files changed, 276 insertions(+), 14 deletions(-) create mode 100644 tests/unit/expansion/expand.c diff --git a/src/expansion/expansion.c b/src/expansion/expansion.c index 81867a1..dca13ec 100644 --- a/src/expansion/expansion.c +++ b/src/expansion/expansion.c @@ -6,6 +6,9 @@ #include #include "../utils/ast/ast.h" +#include "../utils/hash_map/hash_map.h" +#include "../utils/string_utils/string_utils.h" +#include "../utils/vars/vars.h" static bool is_var_start_char(char c) { @@ -77,14 +80,39 @@ size_t parse_var_name(char *str, char **res) return i; } -struct ast_command *expand(struct ast_command *command) +static bool expand_var(char **str, size_t pos, const struct hash_map *vars) +{ + char *var_name = NULL; + size_t r = parse_var_name(*str + pos, &var_name); + if (r > 0 && var_name != NULL) + { + char *value = get_var_or_env(vars, var_name); + if (value == NULL) + // Undefined variable: expand to empty string + value = ""; + + char *p = insert_into(*str, value, pos, r); + free(var_name); + if (p == NULL) + { + // error: insertion failed + return false; + } + *str = p; + return true; + } + return false; +} + +struct ast_command *expand(struct ast_command *command, + const struct hash_map *vars) { if (command == NULL) return NULL; - bool in_quotes = false; char *str; size_t len; + bool in_quotes; struct list *l = command->command; while (l != NULL) @@ -93,28 +121,35 @@ struct ast_command *expand(struct ast_command *command) str = (char *)l->data; len = strlen(str); - for (size_t i = 0; str[i] != '\0'; i++) + for (size_t i = 0; str[i] != 0; i++) { - if (in_quotes) - { - continue; // do nothing - } - else if (str[i] == '\'') + if (str[i] == '\'') { // remove quote in_quotes = !in_quotes; - memmove(&str[i], &str[i + 1], strlen(&str[i + 1]) + 1); + memmove(str + i, str + i + 1, strlen(str + i + 1) + 1); i--; } + else if (in_quotes) + { + continue; // do nothing + } else if (str[i] == '$' && str[i + 1] != 0 && !isspace(str[i + 1])) { // variable expansion + bool r = expand_var(&str, i, vars); + if (r == false || str == NULL) + return NULL; + + i--; // -1 because loop will increment i } } if (in_quotes) { // error: quote not closed + fprintf(stderr, "Error: quote not closed in string: %s\n", str); + return NULL; } if (len != strlen(str)) @@ -123,6 +158,7 @@ struct ast_command *expand(struct ast_command *command) if (new_str == NULL) { // error: realloc fail + return NULL; } l->data = new_str; } diff --git a/src/expansion/expansion.h b/src/expansion/expansion.h index 756e75e..7211ae3 100644 --- a/src/expansion/expansion.h +++ b/src/expansion/expansion.h @@ -3,6 +3,8 @@ #include +#include "../utils/hash_map/hash_map.h" + /** * Parse a variable from a string starting with '$'. * @param str The input string starting with '$'. It must start with '$'. @@ -12,4 +14,13 @@ */ size_t parse_var_name(char *str, char **res); +/** + * Expand variables in an AST command using the provided variable map. + * @param command The AST command to expand. + * @param vars The hash map containing variables. + * @return A new AST command with variables expanded, or NULL on error. + */ +struct ast_command *expand(struct ast_command *command, + const struct hash_map *vars); + #endif /* ! EXPANSION_H */ diff --git a/tests/unit/expansion/expand.c b/tests/unit/expansion/expand.c new file mode 100644 index 0000000..1e25d7c --- /dev/null +++ b/tests/unit/expansion/expand.c @@ -0,0 +1,220 @@ +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include + +#include "../../../src/expansion/expansion.h" +#include "../../../src/utils/ast/ast.h" +#include "../../../src/utils/hash_map/hash_map.h" +#include "../../../src/utils/vars/vars.h" + +TestSuite(expand); + +Test(expand, no_expansion) +{ + char str[] = "echo something"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list); + struct ast_command *ast_command = ast_get_command(ast); + + struct ast_command *command2 = expand(ast_command, NULL); + cr_assert_not_null(command2, "Expansion returned NULL"); + cr_assert_str_eq((char *)command2->command->data, "echo something", + "String without variables should remain unchanged"); + ast_free(&ast); +} + +Test(expand, single_quotes_no_expansion) +{ + char str[] = "echo '$VAR'"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR", "expanded"); + + struct ast_command *command2 = expand(ast_command, vars); + cr_assert_not_null(command2, "Expansion returned NULL"); + cr_assert_str_eq((char *)command2->command->data, "echo $VAR", + "Variable should not expand inside single quotes"); + ast_free(&ast); + hash_map_free(vars); +} + +Test(expand, single_dollar) +{ + char str[] = "echo $ sign"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR", "expanded"); + + struct ast_command *command2 = expand(ast_command, vars); + cr_assert_not_null(command2, "Expansion returned NULL"); + cr_assert_str_eq((char *)command2->command->data, "echo $ sign", + "Variable should not expand inside single quotes"); + ast_free(&ast); + hash_map_free(vars); +} + +Test(expand, empty_braces_no_expansion) +{ + char str[] = "echo ${}"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR", "expanded"); + + struct ast_command *command2 = expand(ast_command, vars); + cr_assert_null(command2, "Expansion should fail on empty braces"); + ast_free(&ast); + hash_map_free(vars); +} + +Test(expand, basic_expansion) +{ + char str[] = "echo $VAR"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR", "expanded"); + + struct ast_command *command2 = expand(ast_command, vars); + cr_assert_not_null(command2, "Expansion returned NULL"); + cr_assert_str_eq((char *)command2->command->data, "echo expanded", + "Variable should expand correctly"); + ast_free(&ast); + hash_map_free(vars); +} + +Test(expand, multiple_expansion) +{ + char str[] = "echo $VAR1 $VAR2 ${VAR3}"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR1", "expanded"); + set_var_copy(vars, "VAR2", "values"); + set_var_copy(vars, "VAR3", "here"); + + struct ast_command *command2 = expand(ast_command, vars); + cr_assert_not_null(command2, "Expansion returned NULL"); + cr_assert_str_eq((char *)command2->command->data, + "echo expanded values here", + "Multiple variables should expand correctly"); + ast_free(&ast); + hash_map_free(vars); +} + +Test(expand, env_variable) +{ + char str[] = "echo $MY_ENV_VAR"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list); + struct ast_command *ast_command = ast_get_command(ast); + + setenv("MY_ENV_VAR", "environment", 0); + + struct ast_command *command2 = expand(ast_command, NULL); + cr_assert_not_null(command2, "Expansion returned NULL"); + cr_assert_str_eq((char *)command2->command->data, "echo environment", + "Environment variable should expand correctly"); + ast_free(&ast); +} + +Test(expand, undefined_variable) +{ + char str[] = "echo $UNDEFINED"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + + struct ast_command *command2 = expand(ast_command, vars); + cr_assert_not_null(command2, "Expansion returned NULL"); + cr_assert_str_eq((char *)command2->command->data, "echo ", + "Undefined variable should expand to empty string"); + ast_free(&ast); + hash_map_free(vars); +} + +Test(expand, nested_expansion) +{ + char str[] = "echo $B"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "A", "expanded"); + set_var_copy(vars, "B", "$A"); + + struct ast_command *command2 = expand(ast_command, vars); + cr_assert_not_null(command2, "Expansion returned NULL"); + cr_assert_str_eq((char *)command2->command->data, "echo expanded", + "Nested variable should expand correctly"); + ast_free(&ast); + hash_map_free(vars); +} + +Test(expand, mixed_quotes_expansion) +{ + char str[] = "echo \"$VAR1 and '$VAR2'\""; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR1", "expanded"); + set_var_copy(vars, "VAR2", "not_expanded"); + + struct ast_command *command2 = expand(ast_command, vars); + cr_assert_not_null(command2, "Expansion returned NULL"); + cr_assert_str_eq((char *)command2->command->data, + "echo \"expanded and $VAR2\"", + "Variable in double quotes should expand, while variable " + "in single quotes should not"); + ast_free(&ast); + hash_map_free(vars); +} + +Test(expand, adjacent_variables) +{ + char str[] = "echo $VAR1$VAR2"; + char *str_heap = strdup(str); + struct list *list = list_append(NULL, str_heap); + struct ast *ast = ast_create_command(list); + struct ast_command *ast_command = ast_get_command(ast); + + struct hash_map *vars = vars_init(); + set_var_copy(vars, "VAR1", "hello"); + set_var_copy(vars, "VAR2", "world"); + + struct ast_command *command2 = expand(ast_command, vars); + cr_assert_not_null(command2, "Expansion returned NULL"); + cr_assert_str_eq((char *)command2->command->data, "echo helloworld", + "Adjacent variables should expand correctly"); + ast_free(&ast); + hash_map_free(vars); +} diff --git a/tests/unit/expansion/parse_var.c b/tests/unit/expansion/parse_var.c index ce4c11a..27a4b94 100644 --- a/tests/unit/expansion/parse_var.c +++ b/tests/unit/expansion/parse_var.c @@ -6,11 +6,6 @@ TestSuite(parse_var_name); -// char *input = "$MY$VAR"; -// char *input = "$MY$VAR$"; -// char *input = "$MY$VAR${}"; -// char *input = "$MY$VAR${1}"; - Test(parse_var_name, basic_variable) { char *input = "$MY_VAR";