merge(expansion)

This commit is contained in:
william.valenduc 2026-01-18 01:32:11 +00:00
commit 953b05fba0
10 changed files with 650 additions and 40 deletions

View file

@ -1,3 +1,4 @@
#define _POSIX_C_SOURCE 200809L
#include <ctype.h> #include <ctype.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdio.h> #include <stdio.h>
@ -5,23 +6,113 @@
#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 size_t var_len(char *start) static bool is_var_start_char(char c)
// { {
// char *iter = start; return isalpha(c) || c == '_';
// while (*iter != ' ' && *iter != 0) }
// *iter++;
// return iter - start;
// }
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) 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)
@ -30,31 +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] == '\'')
{
// do nothing
}
else if (str[i] == '\'')
{ {
// 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--;
}
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]))
{ {
// size_t len = var_len(str + i + 1); // variable expansion
// char *end = str + i + len + 1; bool r = expand_var(&str, i, vars);
// char c = *end; if (r == false || str == NULL)
// *end = 0; return NULL;
// printf("var: %s\n", str + i + 1);
// *end = c; 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))
@ -63,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;
} }
@ -71,17 +167,3 @@ struct ast_command *expand(struct ast_command *command)
} }
return 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;
// }

View file

@ -1,4 +1,26 @@
#ifndef EXPANSION_H #ifndef EXPANSION_H
#define EXPANSION_H #define EXPANSION_H
#include <stddef.h>
#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 */ #endif /* ! EXPANSION_H */

View file

@ -28,6 +28,14 @@ static size_t hash(const char *key)
return hash; 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 *hash_map_init(size_t size)
{ {
struct hash_map *p = malloc(sizeof(struct hash_map)); struct hash_map *p = malloc(sizeof(struct hash_map));
@ -102,7 +110,7 @@ void hash_map_free(struct hash_map *hash_map)
{ {
prev = l; prev = l;
l = l->next; l = l->next;
free(prev); destroy_pair_list(&prev);
} }
} }
free(hash_map->data); free(hash_map->data);
@ -163,7 +171,7 @@ bool hash_map_remove(struct hash_map *hash_map, const char *key)
p->next = l->next; p->next = l->next;
else else
hash_map->data[i] = l->next; hash_map->data[i] = l->next;
free(l); destroy_pair_list(&l);
return true; return true;
} }
p = l; p = l;

View file

@ -1,7 +1,8 @@
#include "string_utils.h" #include "string_utils.h"
#include <ctype.h> #include <ctype.h>
#include <stddef.h> #include <stdlib.h>
#include <string.h>
char *trim_blank_left(char *str) char *trim_blank_left(char *str)
{ {
@ -13,3 +14,31 @@ char *trim_blank_left(char *str)
return 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;
}

View file

@ -12,4 +12,10 @@
*/ */
char *trim_blank_left(char *str); 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 */ #endif /* STRING_UTILS_H */

View file

@ -2,7 +2,7 @@
#include "vars.h" #include "vars.h"
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include "../hash_map/hash_map.h" #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); return (char *)hash_map_get(vars, key);
} }
char *get_var_or_env(const struct hash_map *vars, const char *key)
{
char *value = (char *)hash_map_get(vars, key);
if (value == NULL)
value = getenv(key);
return value;
}
bool set_var(struct hash_map *vars, const char *key, const char *value) bool set_var(struct hash_map *vars, const char *key, const char *value)
{ {
if (key == NULL || value == NULL) if (key == NULL || value == NULL)

View file

@ -15,6 +15,12 @@ struct hash_map *vars_init(void);
*/ */
char *get_var(const struct hash_map *vars, const char *key); 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 * 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 * the hash_map and need to be on the heap. Returns true on success, false on

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

@ -0,0 +1,144 @@
#include <criterion/criterion.h>
#include <criterion/new/assert.h>
#include <criterion/redirect.h>
#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);
}

View file

@ -0,0 +1,85 @@
#define _POSIX_C_SOURCE 200809L
#include <criterion/criterion.h>
#include <criterion/new/assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../../../src/utils/string_utils/string_utils.h"
TestSuite(insert_into);
Test(insert_into, basic)
{
char *dest = strdup("The <WORD> 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);
}