feat(expansion): parse_subshell_str and tests

This commit is contained in:
william.valenduc 2026-01-31 17:31:30 +00:00
parent 2e7bcd7fa1
commit bb7d4b772e
4 changed files with 151 additions and 21 deletions

View file

@ -1,4 +1,6 @@
#define _POSIX_C_SOURCE 200809L
#include "expansion.h"
#include <ctype.h>
#include <stdbool.h>
#include <stdio.h>
@ -115,6 +117,40 @@ static bool expand_var(char **str, size_t pos, const struct hash_map *vars)
return false;
}
size_t parse_subshell_str(char *str, char **res)
{
size_t i = 1; // skip the '('
int paren_count = 1;
if (str[i] == ')')
{
// empty subshell
*res = NULL;
return 0;
}
while (str[i] != 0)
{
if (str[i] == '(')
paren_count++;
else if (str[i] == ')')
{
paren_count--;
if (paren_count == 0)
{
*res = strndup(str + 1, i - 1);
return i + 1;
}
}
i++;
}
// error: parenthesis not closed
*res = NULL;
return 0;
}
bool expand(struct ast_command *command, const struct hash_map *vars)
{
if (command == NULL)
@ -122,34 +158,42 @@ bool expand(struct ast_command *command, const struct hash_map *vars)
char *str;
size_t len;
bool in_quotes;
enum quote_state quotes;
struct list *l = command->command;
while (l != NULL)
{
in_quotes = false;
quotes = NO_QUOTE;
str = (char *)l->data;
len = strlen(str);
for (size_t i = 0; str[i] != 0; i++)
{
if (str[i] == '\'')
if (str[i] == '\'' || str[i] == '\"')
{
// remove single quote
in_quotes = !in_quotes;
if (quotes == NO_QUOTE)
{
quotes = (str[i] == '\'') ? SINGLE_QUOTE : DOUBLE_QUOTE;
}
else if ((quotes == SINGLE_QUOTE && str[i] == '\'')
|| (quotes == DOUBLE_QUOTE && str[i] == '\"'))
{
quotes = NO_QUOTE;
}
else
{
// inside the other quote type, do nothing
continue;
}
// remove quote
memmove(str + i, str + i + 1, strlen(str + i + 1) + 1);
i--;
}
else if (in_quotes)
else if (quotes == SINGLE_QUOTE)
{
continue; // do nothing
}
else if (str[i] == '\"')
{
// remove double quote
memmove(str + i, str + i + 1, strlen(str + i + 1) + 1);
i--;
}
else if (str[i] == '$' && str[i + 1] != 0 && !isspace(str[i + 1]))
{
// variable expansion
@ -161,21 +205,20 @@ bool expand(struct ast_command *command, const struct hash_map *vars)
}
}
if (in_quotes)
{
// error: quote not closed
fprintf(stderr, "Error: quote not closed in string: %s\n", str);
return false;
}
// if (quotes != NO_QUOTE)
// {
// // error: quote not closed
// fprintf(stderr, "Error: quote not closed in string: %s\n", str);
// return false;
// }
if (len != strlen(str))
{
char *new_str = realloc(str, strlen(str) + 1);
if (new_str == NULL)
{
// error: realloc fail
return false;
}
l->data = new_str;
}

View file

@ -7,6 +7,13 @@
#include "../utils/ast/ast.h"
#include "../utils/hash_map/hash_map.h"
enum quote_state
{
NO_QUOTE,
SINGLE_QUOTE,
DOUBLE_QUOTE
};
/**
* Parse a variable from a string starting with '$'.
* @param str The input string starting with '$'. It must start with '$'.
@ -16,6 +23,15 @@
*/
size_t parse_var_name(char *str, char **res);
/**
* Parse a subshell string enclosed in parentheses.
* @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
* subshell string.
* @return The number of characters processed in the input string.
*/
size_t parse_subshell_str(char *str, char **res);
/**
* Expand variables in an AST command using the provided variable map.
* @param command The AST command to expand.

View file

@ -0,0 +1,72 @@
#include <criterion/criterion.h>
#include <criterion/new/assert.h>
#include "../../../src/expansion/expansion.h"
TestSuite(parse_subshell_str);
Test(parse_subshell_str, basic_subshell)
{
char *input = "(ls -l)";
char *extracted_var = NULL;
size_t r = parse_subshell_str(input, &extracted_var);
cr_expect(r == 7);
cr_expect_str_eq(extracted_var, "ls -l");
free(extracted_var);
}
Test(parse_subshell_str, multi_basic_subshell)
{
char *input = "(echo hello) and (echo world)";
char *extracted_var = NULL;
size_t r = parse_subshell_str(input, &extracted_var);
cr_expect(r == 12);
cr_expect_str_eq(extracted_var, "echo hello");
free(extracted_var);
input += r + 5; // skip " and "
r = parse_subshell_str(input, &extracted_var);
cr_expect(r == 12);
cr_expect_str_eq(extracted_var, "echo world");
free(extracted_var);
}
Test(parse_subshell_str, incomplete_braces)
{
char *input = "(echo hello";
char *extracted_var = NULL;
size_t r = parse_subshell_str(input, &extracted_var);
cr_expect(r == 0);
cr_expect(extracted_var == NULL);
}
Test(parse_subshell_str, empty_braces)
{
char *input = "()";
char *extracted_var = NULL;
size_t r = parse_subshell_str(input, &extracted_var);
cr_expect(r == 0);
cr_expect(extracted_var == NULL);
}
Test(parse_subshell_str, nested_subshell)
{
char *input = "(echo (nested))";
char *extracted_var = NULL;
size_t r = parse_subshell_str(input, &extracted_var);
cr_expect(r == 15);
cr_expect_str_eq(extracted_var, "echo (nested)");
free(extracted_var);
char *nested = input + 6; // point to the nested subshell
r = parse_subshell_str(nested, &extracted_var);
cr_expect(r == 8);
cr_expect_str_eq(extracted_var, "nested");
free(extracted_var);
}

View file

@ -1,6 +1,5 @@
#include <criterion/criterion.h>
#include <criterion/new/assert.h>
#include <criterion/redirect.h>
#include "../../../src/expansion/expansion.h"