feat(expansion): expand_var and expand

This commit is contained in:
william.valenduc 2026-01-18 01:27:31 +00:00
parent a16712e802
commit 34c741d86e
4 changed files with 276 additions and 14 deletions

View file

@ -6,6 +6,9 @@
#include <string.h> #include <string.h>
#include "../utils/ast/ast.h" #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) static bool is_var_start_char(char c)
{ {
@ -77,14 +80,39 @@ size_t parse_var_name(char *str, char **res)
return i; 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) if (command == NULL)
return NULL; return NULL;
bool in_quotes = false;
char *str; char *str;
size_t len; size_t len;
bool in_quotes;
struct list *l = command->command; struct list *l = command->command;
while (l != NULL) while (l != NULL)
@ -93,28 +121,35 @@ struct ast_command *expand(struct ast_command *command)
str = (char *)l->data; str = (char *)l->data;
len = strlen(str); len = strlen(str);
for (size_t i = 0; str[i] != '\0'; i++) for (size_t i = 0; str[i] != 0; i++)
{ {
if (in_quotes) if (str[i] == '\'')
{
continue; // do nothing
}
else if (str[i] == '\'')
{ {
// remove quote // remove quote
in_quotes = !in_quotes; 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--; i--;
} }
else if (in_quotes)
{
continue; // do nothing
}
else if (str[i] == '$' && str[i + 1] != 0 && !isspace(str[i + 1])) else if (str[i] == '$' && str[i + 1] != 0 && !isspace(str[i + 1]))
{ {
// variable expansion // 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) if (in_quotes)
{ {
// error: quote not closed // error: quote not closed
fprintf(stderr, "Error: quote not closed in string: %s\n", str);
return NULL;
} }
if (len != strlen(str)) if (len != strlen(str))
@ -123,6 +158,7 @@ struct ast_command *expand(struct ast_command *command)
if (new_str == NULL) if (new_str == NULL)
{ {
// error: realloc fail // error: realloc fail
return NULL;
} }
l->data = new_str; l->data = new_str;
} }

View file

@ -3,6 +3,8 @@
#include <stddef.h> #include <stddef.h>
#include "../utils/hash_map/hash_map.h"
/** /**
* Parse a variable from a string starting with '$'. * Parse a variable from a string starting with '$'.
* @param str The input string starting with '$'. It must start 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); 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 */ #endif /* ! EXPANSION_H */

View file

@ -0,0 +1,220 @@
#define _POSIX_C_SOURCE 200809L
#include <criterion/criterion.h>
#include <criterion/new/assert.h>
#include <criterion/redirect.h>
#include <stdlib.h>
#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);
}

View file

@ -6,11 +6,6 @@
TestSuite(parse_var_name); 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) Test(parse_var_name, basic_variable)
{ {
char *input = "$MY_VAR"; char *input = "$MY_VAR";