diff --git a/src/expansion/expansion.c b/src/expansion/expansion.c index fe28dd0..dca13ec 100644 --- a/src/expansion/expansion.c +++ b/src/expansion/expansion.c @@ -1,3 +1,4 @@ +#define _POSIX_C_SOURCE 200809L #include #include #include @@ -5,23 +6,113 @@ #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 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 == '_'; +} -struct ast_command *expand(struct ast_command *command) +static bool is_var_char(char c) +{ + return isalnum(c) || c == '_'; +} + +static bool is_special_var_char(char c) +{ + return c == '@' || c == '*' || c == '?' || c == '$' || isdigit(c) + || c == '#'; +} + +size_t parse_var_name(char *str, char **res) +{ + char *brace = NULL; + size_t i = 1; // skip the '$' + + if (str[i] == '{' && str[i + 1] != 0 && str[i + 1] != '}') + { + if (is_special_var_char(str[i + 1]) && str[i + 2] == '}') + { + // Special variable like ${1}, ${?} + *res = strndup(str + i + 1, 1); + return 4; // length of ${X} + } + + brace = str + i; + i++; // skip the '{' + } + else if (is_special_var_char(str[i])) + { + *res = strndup(str + i, 1); + return 2; // length of $X + } + + if (!is_var_start_char(str[i])) + { + // Not a valid variable start + *res = NULL; + return 0; + } + + while (1) + { + if (str[i] == '}' && *brace == '{') + { + *res = strndup(str + 2, i - 2); + return i + 1; + } + else if (!is_var_char(str[i])) + { + if (brace != NULL) + { + // Missing closing '}' + *res = NULL; + return 0; + } + break; + } + i++; + } + + *res = strndup(str + 1, i - 1); + return i; +} + +static bool expand_var(char **str, size_t pos, const struct hash_map *vars) +{ + char *var_name = NULL; + size_t r = parse_var_name(*str + pos, &var_name); + if (r > 0 && var_name != NULL) + { + char *value = 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) @@ -30,31 +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) - { - // 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])) { - // 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 + 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)) @@ -63,6 +158,7 @@ struct ast_command *expand(struct ast_command *command) if (new_str == NULL) { // error: realloc fail + return NULL; } l->data = new_str; } @@ -71,17 +167,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..7211ae3 100644 --- a/src/expansion/expansion.h +++ b/src/expansion/expansion.h @@ -1,4 +1,26 @@ #ifndef EXPANSION_H #define EXPANSION_H +#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 '$'. + * @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); + +/** + * 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/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; 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/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 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 new file mode 100644 index 0000000..27a4b94 --- /dev/null +++ b/tests/unit/expansion/parse_var.c @@ -0,0 +1,144 @@ +#include +#include +#include + +#include "../../../src/expansion/expansion.h" + +TestSuite(parse_var_name); + +Test(parse_var_name, basic_variable) +{ + char *input = "$MY_VAR"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 7); + cr_expect_str_eq(extracted_var, "MY_VAR"); + free(extracted_var); +} + +Test(parse_var_name, multi_basic_variable) +{ + char *input = "$MY$VAR"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 3); + cr_expect_str_eq(extracted_var, "MY"); + free(extracted_var); + + input += r; + r = parse_var_name(input, &extracted_var); + + cr_expect(r == 4); + cr_expect_str_eq(extracted_var, "VAR"); + free(extracted_var); +} + +Test(parse_var_name, variable_with_braces) +{ + char *input = "${MY_VAR}"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 9); + cr_expect_str_eq(extracted_var, "MY_VAR"); + free(extracted_var); +} + +Test(parse_var_name, special_variable) +{ + char *input = "$1"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 2); + cr_expect_str_eq(extracted_var, "1"); + free(extracted_var); +} + +Test(parse_var_name, special_variable_with_braces) +{ + char *input = "${1}"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 4); + cr_expect_str_eq(extracted_var, "1"); + free(extracted_var); +} + +Test(parse_var_name, incomplete_braces) +{ + char *input = "${MY_VAR"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_var_name, empty_braces) +{ + char *input = "${}"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_var_name, dollar_sign_only) +{ + char *input = "$"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_var_name, variable_followed_by_dollar) +{ + char *input = "$MY$VAR$"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 3); + cr_expect_str_eq(extracted_var, "MY"); + free(extracted_var); + + input += r; + r = parse_var_name(input, &extracted_var); + + cr_expect(r == 4); + cr_expect_str_eq(extracted_var, "VAR"); + free(extracted_var); + + input += r; + r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} + +Test(parse_var_name, special_variable_followed_by_text) +{ + char *input = "$1VAR"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 2); + cr_expect_str_eq(extracted_var, "1"); + free(extracted_var); +} + +Test(parse_var_name, bad_variable_with_braces) +{ + char *input = "${1VAR}"; + char *extracted_var = NULL; + size_t r = parse_var_name(input, &extracted_var); + + cr_expect(r == 0); + cr_expect(extracted_var == NULL); +} diff --git a/tests/unit/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); +}