From 778e0c048115adedd2b99786f6512e78890e1b88 Mon Sep 17 00:00:00 2001 From: marcnava-42cursus Date: Mon, 9 Feb 2026 22:08:45 +0100 Subject: [PATCH] Builtins fixed The builtins wasnt protected, now all data received is protected, the hashmap addition is protected and added functionality of env, export and unset (not implemented in this version). Added fixed details documentation in docs/builtins_fixes.md generated by codex and created tests/builtins_edge_cases.sh to test all the builtins to work correctly --- docs/builtins_fixes.md | 151 ++++++++++++++++++++++ include/builtins.h | 26 +++- include/core.h | 2 + src/builtins/builtins.c | 52 ++++++-- src/builtins/cd/cd.c | 113 +++++++++++++---- src/builtins/echo/echo.c | 2 +- src/builtins/echo/echo_def.c | 4 +- src/builtins/env/env.c | 58 +++++++++ src/builtins/exit/exit.c | 106 ++++++++++++---- src/builtins/export/export.c | 79 ++++++++++++ src/builtins/pwd/pwd.c | 14 +- src/builtins/unset/unset.c | 61 +++++++++ src/variables/environment.c | 4 +- src/variables/environment_unset.c | 41 ++++++ tests/builtins_edge_cases.sh | 204 ++++++++++++++++++++++++++++++ 15 files changed, 843 insertions(+), 74 deletions(-) create mode 100644 docs/builtins_fixes.md create mode 100644 src/builtins/env/env.c create mode 100644 src/builtins/export/export.c create mode 100644 src/builtins/unset/unset.c create mode 100644 src/variables/environment_unset.c create mode 100755 tests/builtins_edge_cases.sh diff --git a/docs/builtins_fixes.md b/docs/builtins_fixes.md new file mode 100644 index 0000000..759d7dc --- /dev/null +++ b/docs/builtins_fixes.md @@ -0,0 +1,151 @@ +# Correcciones en Builtins + +Este documento resume los fallos detectados en los builtins actuales y la +solucion aplicada en codigo. + +## 1) Infraestructura de builtins (`include/builtins.h`, `src/builtins/builtins.c`) + +### Fallo +- Uso inconsistente de tipos (`u_int8_t`/`unsigned char` frente a `uint8_t`). +- `set_builtins` no comprobaba fallos de registro por builtin (duplicado de + clave o insercion en hashmap), pudiendo dejar estado parcial. +- `is_builtin` podia dereferenciar punteros nulos. + +### Por que fallaba +- `u_int8_t` no es el tipo estandar C99 y depende de plataforma/headers. +- Si falla una insercion, la tabla quedaba inicializada parcialmente sin + rollback. +- En errores de inicializacion, consultar `is_builtin` podia romper. + +### Solucion +- Unificacion de firmas a `uint8_t`. +- Nuevo helper `register_builtin()` con validacion tras `ft_hashmap_put`. +- Si falla cualquier alta: limpieza de `minishell->builtins` y retorno de + error. +- Guardas nulas en `is_builtin`. + +## 2) `cd` (`src/builtins/cd/cd.c`) + +### Fallo +- `cd` con demasiados argumentos devolvia `2` (bash devuelve `1`). +- `cd` sin `HOME` acababa llamando a `chdir(NULL)`. +- El manejo de error usaba comprobaciones invertidas (`access`) y codigos + incorrectos. +- No se actualizaban `PWD` y `OLDPWD` tras `chdir` exitoso. +- `cd -` (usar `OLDPWD`) no estaba soportado. + +### Por que fallaba +- Codigos de salida incompatibles con el comportamiento esperado del shell. +- `HOME` no definido no se controlaba antes del `chdir`. +- La logica de `access` estaba al reves y mezclaba condiciones. +- Variables de entorno del directorio quedaban desincronizadas. +- Faltaba resolver el caso especial de `-` hacia `OLDPWD`. + +### Solucion +- Refactor en `resolve_cd_path()` para validar argumentos y `HOME`. +- Retorno `EXIT_FAILURE` en `too many arguments` y `HOME not set`. +- Error de `chdir` simplificado a `perror("minishell: cd")` + retorno `1`. +- Actualizacion de `OLDPWD` y `PWD` mediante `getcwd(NULL, 0)` + `set_env()`. +- Soporte de `cd -`: usa `OLDPWD`, valida `OLDPWD not set` e imprime el nuevo + directorio tras el cambio. + +## 3) `exit` (`src/builtins/exit/exit.c`) + +### Fallo +- Habia un `printf` de debug en ejecucion real. +- `exit ` devolvia `2` pero no cerraba el shell. +- `exit n m` devolvia `2`; en bash es `1` y no sale del shell. +- Validacion numerica basada en `ft_strisnum` sin control de overflow. +- Se mostraba `exit` incluso en contexto no interactivo. + +### Por que fallaba +- Debug residual contamina salida. +- Semantica de `exit` incompleta respecto a bash. +- Valores fuera de rango podian tratarse como validos por conversion directa. +- Mensaje `exit` debe mostrarse solo en shell interactivo. + +### Solucion +- Eliminado debug print. +- Nuevo flujo `resolve_exit_status()`: + - Sin argumentos: usa `msh->exit_status`. + - Argumento no numerico o fuera de `long`: mensaje + `numeric argument required`, `msh->exit = true`, estado `2`. + - Demasiados argumentos: mensaje de error y estado `1`, sin salir. +- Parser numerico propio (`get_uint8_from_num` + `has_overflow`) con soporte + de signo y control de overflow. +- `ft_eputendl("exit")` solo si `isatty(STDIN_FILENO)`. + +## 4) `pwd` (`src/builtins/pwd/pwd.c`) + +### Fallo +- Si `getcwd` fallaba, el builtin devolvia `EXIT_SUCCESS`. +- Uso de buffer fijo (`PATH_MAX`) menos robusto para rutas largas. + +### Por que fallaba +- El shell reportaba exito aunque no pudiera obtener el directorio. +- Un buffer fijo puede truncar o fallar en escenarios de rutas profundas. + +### Solucion +- Cambio a `getcwd(NULL, 0)` con memoria dinamica. +- Si falla, `perror("minishell: pwd")` y retorno `EXIT_FAILURE`. +- `free()` del buffer dinamico tras imprimir. + +## 5) `echo` (`src/builtins/echo/echo.c`, `src/builtins/echo/echo_def.c`) + +### Cambio aplicado +- Ajuste de tipos de retorno auxiliares a `uint8_t` para mantener consistencia + con `builtins.h`. + +### Nota +- No se detectaron fallos funcionales criticos adicionales en la logica actual + de `echo` durante esta revision. + +## 6) Builtins faltantes: `env`, `export`, `unset` + +### Fallo +- Los builtins `env`, `export` y `unset` no estaban implementados ni + registrados en `set_builtins`. + +### Por que fallaba +- Comandos basicos de shell no existian en la tabla de builtins. +- Cualquier prueba/flujo que dependiera de gestion de variables exportadas + fallaba (listar, crear y eliminar variables de entorno). + +### Solucion +- Nuevos builtins implementados: + - `src/builtins/env/env.c` + - `src/builtins/export/export.c` + - `src/builtins/unset/unset.c` +- Registro en `src/builtins/builtins.c` (tabla ampliada de 4 a 7 entradas). +- Nuevos prototipos en `include/builtins.h`. +- Soporte de borrado real de entorno mediante `unset_env`: + - Declaracion en `include/core.h` + - Implementacion en `src/variables/environment_unset.c` + +## 7) Comportamiento aplicado en los nuevos builtins + +### `env` +- Si recibe argumentos, devuelve error (`minishell: env: too many arguments`) + y estado `1`. +- Sin argumentos, lista `KEY=VALUE` de las variables de entorno actuales. + +### `export` +- `export NAME=VALUE`: crea/actualiza la variable. +- `export NAME`: crea/actualiza con valor vacio (`NAME=`) para que aparezca + en `env`. +- Identificadores invalidos (`1A=2`, etc.) devuelven error + `not a valid identifier` y estado `1`. +- Sin argumentos, reutiliza el listado de `env`. + +### `unset` +- Elimina variables validas del entorno en memoria. +- Identificadores invalidos devuelven error + `not a valid identifier` y estado `1`. +- Si el identificador es valido y no existe, no falla (comportamiento shell). + +## Validacion realizada + +- `norminette` ejecutado sobre los archivos modificados: `OK`. +- Build completa no ejecutable en este entorno por falta de acceso de red al + clonado de librerias (`github.com`), por lo que no se pudo validar runtime + del binario en esta sesion. diff --git a/include/builtins.h b/include/builtins.h index c8dac83..01ae629 100644 --- a/include/builtins.h +++ b/include/builtins.h @@ -17,7 +17,7 @@ # include "minishell.h" # include "core.h" -typedef unsigned char (*t_builtin_func)(t_command cmd, t_minishell *minishell); +typedef uint8_t (*t_builtin_func)(t_command cmd, t_minishell *minishell); /******************************************************************************/ /* Functions */ @@ -25,24 +25,36 @@ typedef unsigned char (*t_builtin_func)(t_command cmd, t_minishell *minishell) /* builtins.c */ -extern u_int8_t set_builtins(t_minishell *minishell); +extern uint8_t set_builtins(t_minishell *minishell); -extern u_int8_t is_builtin(const char *command_name, t_minishell *minishell); +extern uint8_t is_builtin(const char *command_name, t_minishell *minishell); /* cd.c */ -extern u_int8_t builtin_cd(t_command cmd, t_minishell *minishell); +extern uint8_t builtin_cd(t_command cmd, t_minishell *minishell); /* echo.c */ -extern u_int8_t builtin_echo(t_command cmd, t_minishell *minishell); +extern uint8_t builtin_echo(t_command cmd, t_minishell *minishell); /* exit.c */ -extern u_int8_t builtin_exit(t_command cmd, t_minishell *minishell); +extern uint8_t builtin_exit(t_command cmd, t_minishell *minishell); /* pwd.c */ -extern u_int8_t builtin_pwd(t_command cmd, t_minishell *minishell); +extern uint8_t builtin_pwd(t_command cmd, t_minishell *minishell); + +/* env.c */ + +extern uint8_t builtin_env(t_command cmd, t_minishell *minishell); + +/* export.c */ + +extern uint8_t builtin_export(t_command cmd, t_minishell *minishell); + +/* unset.c */ + +extern uint8_t builtin_unset(t_command cmd, t_minishell *minishell); #endif /* BUILTINS_H */ diff --git a/include/core.h b/include/core.h index 3e568ae..14cb4fd 100644 --- a/include/core.h +++ b/include/core.h @@ -107,4 +107,6 @@ extern void free_envp(char **envp); extern char *get_env(const char *env_name, t_minishell *msh); +extern void unset_env(const char *env_name, t_minishell *msh); + #endif /* CORE_H */ diff --git a/src/builtins/builtins.c b/src/builtins/builtins.c index 0d8c89b..3dd536b 100644 --- a/src/builtins/builtins.c +++ b/src/builtins/builtins.c @@ -12,23 +12,53 @@ #include "builtins.h" -u_int8_t set_builtins( - t_minishell *minishell -) { - minishell->builtins - = ft_hashmap_new(4, ft_hashmap_hashstr, ft_hashmap_strcmp); - if (minishell->builtins == NULL) +static uint8_t register_builtin( + t_minishell *minishell, + const char *name, + t_builtin_func builtin +) +{ + char *key; + + key = ft_strdup(name); + if (key == NULL) return (0); - ft_hashmap_put(minishell->builtins, ft_strdup("cd"), builtin_cd); - ft_hashmap_put(minishell->builtins, ft_strdup("echo"), builtin_echo); - ft_hashmap_put(minishell->builtins, ft_strdup("exit"), builtin_exit); - ft_hashmap_put(minishell->builtins, ft_strdup("pwd"), builtin_pwd); + ft_hashmap_put(minishell->builtins, key, builtin); + if (!ft_hashmap_contains_key(minishell->builtins, name)) + { + free(key); + return (0); + } return (1); } -u_int8_t is_builtin( +uint8_t set_builtins( + t_minishell *minishell +) { + minishell->builtins + = ft_hashmap_new(7, ft_hashmap_hashstr, ft_hashmap_strcmp); + if (minishell->builtins == NULL) + return (0); + if (!register_builtin(minishell, "cd", builtin_cd) + || !register_builtin(minishell, "echo", builtin_echo) + || !register_builtin(minishell, "env", builtin_env) + || !register_builtin(minishell, "exit", builtin_exit) + || !register_builtin(minishell, "export", builtin_export) + || !register_builtin(minishell, "pwd", builtin_pwd) + || !register_builtin(minishell, "unset", builtin_unset)) + { + ft_hashmap_clear_keys(&minishell->builtins); + return (0); + } + return (1); +} + +uint8_t is_builtin( const char *command_name, t_minishell *minishell ) { + if (command_name == NULL || minishell == NULL + || minishell->builtins == NULL) + return (0); return (ft_hashmap_contains_key(minishell->builtins, command_name)); } diff --git a/src/builtins/cd/cd.c b/src/builtins/cd/cd.c index 2f508de..04c1ebd 100644 --- a/src/builtins/cd/cd.c +++ b/src/builtins/cd/cd.c @@ -12,43 +12,104 @@ #include "builtins.h" -static u_int8_t handle_error(t_command cmd, t_minishell *msh, char *path); +static uint8_t handle_error(void); +static void update_pwd_vars(t_minishell *msh, char *oldpwd); +static char *get_path_from_env( + t_minishell *msh, + const char *env_name, + const char *error, + uint8_t *status + ); +static char *resolve_cd_path( + t_command cmd, + t_minishell *msh, + uint8_t *status + ); -u_int8_t builtin_cd( +uint8_t builtin_cd( t_command cmd, t_minishell *msh ){ char *path; + char *oldpwd; + uint8_t status; + bool show_pwd; - if (cmd.argc > 2) - { - ft_eputendl("minishell: cd: too many arguments"); - return (2); - } - else if (cmd.argc == 1) - path = get_env("HOME", msh); - else - path = cmd.argv[1]; + path = resolve_cd_path(cmd, msh, &status); + if (status != EXIT_SUCCESS) + return (status); + show_pwd = (cmd.argc == 2 && ft_strcmp(cmd.argv[1], "-") == 0); + oldpwd = getcwd(NULL, 0); if (chdir(path) == -1) - return (handle_error(cmd, msh, path)); + { + free(oldpwd); + return (handle_error()); + } + update_pwd_vars(msh, oldpwd); + if (show_pwd && get_env("PWD", msh) != NULL) + ft_putendl(get_env("PWD", msh)); + free(oldpwd); return (EXIT_SUCCESS); } -static u_int8_t handle_error( +static uint8_t handle_error(void) +{ + perror("minishell: cd"); + return (EXIT_FAILURE); +} + +static void update_pwd_vars( + t_minishell *msh, + char *oldpwd +){ + char *newpwd; + + if (oldpwd != NULL) + set_env("OLDPWD", oldpwd, msh); + newpwd = getcwd(NULL, 0); + if (newpwd != NULL) + { + set_env("PWD", newpwd, msh); + free(newpwd); + } +} + +static char *resolve_cd_path( t_command cmd, t_minishell *msh, - char *path + uint8_t *status ){ - u_int8_t exit_code; - - (void)msh; - exit_code = 0; - if (access(path, F_OK) != -1) - // No such file or directory - exit_code = 1; - else if (access(path, X_OK) == -1) - // Permission denied - exit_code = 2; - perror(cmd.argv[0]); - return (exit_code); + if (cmd.argc > 2) + { + ft_eputendl("minishell: cd: too many arguments"); + *status = EXIT_FAILURE; + return (NULL); + } + if (cmd.argc == 2 && ft_strcmp(cmd.argv[1], "-") == 0) + return (get_path_from_env(msh, "OLDPWD", + "minishell: cd: OLDPWD not set", status)); + if (cmd.argc == 1) + return (get_path_from_env(msh, "HOME", + "minishell: cd: HOME not set", status)); + *status = EXIT_SUCCESS; + return (cmd.argv[1]); +} + +static char *get_path_from_env( + t_minishell *msh, + const char *env_name, + const char *error, + uint8_t *status +){ + char *path; + + path = get_env(env_name, msh); + if (path == NULL) + { + ft_eputendl(error); + *status = EXIT_FAILURE; + return (NULL); + } + *status = EXIT_SUCCESS; + return (path); } diff --git a/src/builtins/echo/echo.c b/src/builtins/echo/echo.c index 5a8a729..567235f 100644 --- a/src/builtins/echo/echo.c +++ b/src/builtins/echo/echo.c @@ -13,7 +13,7 @@ #include "builtins.h" #include "echo_def.h" -u_int8_t builtin_echo( +uint8_t builtin_echo( t_command cmd, t_minishell *msh ){ diff --git a/src/builtins/echo/echo_def.c b/src/builtins/echo/echo_def.c index fd60d0d..f5e5f65 100644 --- a/src/builtins/echo/echo_def.c +++ b/src/builtins/echo/echo_def.c @@ -13,7 +13,7 @@ #include "echo_def.h" static void assign_flags(t_args *args, t_command cmd, size_t *i); -static u_int8_t is_valid_flag(t_args *args, char *flag); +static uint8_t is_valid_flag(t_args *args, char *flag); static void assign_default_flags( t_args *args @@ -51,7 +51,7 @@ static void assign_flags( } } -static u_int8_t is_valid_flag( +static uint8_t is_valid_flag( t_args *args, char *flag ){ diff --git a/src/builtins/env/env.c b/src/builtins/env/env.c new file mode 100644 index 0000000..daae306 --- /dev/null +++ b/src/builtins/env/env.c @@ -0,0 +1,58 @@ +/* ************************************************************************** */ +/* */ +/* ::: :::::::: */ +/* env.c :+: :+: :+: */ +/* +:+ +:+ +:+ */ +/* By: sede-san 1) + { + ft_eputendl("minishell: env: too many arguments"); + return (EXIT_FAILURE); + } + print_env(msh); + return (EXIT_SUCCESS); +} + +static void print_entry( + t_map_entry *entry +) +{ + ft_putstr(entry->key); + ft_putchar('='); + if (entry->value != NULL) + ft_putstr(entry->value); + ft_putchar('\n'); +} + +static void print_env( + t_minishell *msh +) +{ + t_list *entries; + t_list *current; + + entries = ft_hashmap_entries(msh->variables.environment); + current = entries; + while (current != NULL) + { + print_entry((t_map_entry *)current->content); + current = current->next; + } + ft_lstclear_nodes(&entries); +} diff --git a/src/builtins/exit/exit.c b/src/builtins/exit/exit.c index 9d32e0d..a2aff19 100644 --- a/src/builtins/exit/exit.c +++ b/src/builtins/exit/exit.c @@ -11,7 +11,19 @@ /* ************************************************************************** */ #include "builtins.h" -#include +#include + +static uint8_t get_uint8_from_num(const char *arg, uint8_t *status); +static uint8_t has_overflow( + uint64_t n, + char digit, + uint64_t limit + ); +static uint8_t resolve_exit_status( + t_command cmd, + t_minishell *msh, + uint8_t *exit_status + ); uint8_t builtin_exit( t_command cmd, @@ -19,28 +31,78 @@ uint8_t builtin_exit( ) { uint8_t exit_status; - - ft_eputendl("exit"); - if (cmd.argc == 1) - exit_status = msh->exit_status; - else if (!ft_strisnum(cmd.argv[1])) - { - ft_eprintf( - "minishell: exit: %s: numeric argument required\n", - cmd.argv[1]); - return (2); - } - else if (cmd.argc > 2) - { - ft_eputendl("exit: too many arguments"); - return (2); - } - else - exit_status = (uint8_t)ft_atol(cmd.argv[1]); - printf("builtin_exit: exit_status=%d\n", exit_status); // Debug print - - msh->exit = 1; + if (isatty(STDIN_FILENO)) + ft_eputendl("exit"); + if (!resolve_exit_status(cmd, msh, &exit_status)) + return (msh->exit_status); + msh->exit = true; msh->exit_status = exit_status; return (exit_status); } + +static uint8_t resolve_exit_status( + t_command cmd, + t_minishell *msh, + uint8_t *exit_status +){ + if (cmd.argc == 1) + *exit_status = msh->exit_status; + else if (!get_uint8_from_num(cmd.argv[1], exit_status)) + { + ft_eprintf("minishell: exit: %s: numeric argument required\n", + cmd.argv[1]); + msh->exit = true; + msh->exit_status = 2; + return (0); + } + else if (cmd.argc > 2) + { + ft_eputendl("minishell: exit: too many arguments"); + msh->exit_status = EXIT_FAILURE; + return (0); + } + return (1); +} + +static uint8_t get_uint8_from_num( + const char *arg, + uint8_t *status +){ + uint64_t n; + uint64_t limit; + int sign; + + if (arg == NULL || *arg == '\0') + return (0); + n = 0; + sign = 1; + if (*arg == '+' || *arg == '-') + if (*arg++ == '-') + sign = -1; + if (*arg == '\0') + return (0); + limit = LONG_MAX; + if (sign == -1) + limit = (uint64_t)LONG_MAX + 1; + while (*arg != '\0') + { + if (!ft_isdigit(*arg) || has_overflow(n, *arg, limit)) + return (0); + n = (n * 10) + (*arg++ - '0'); + } + *status = (uint8_t)(n * sign); + return (1); +} + +static uint8_t has_overflow( + uint64_t n, + char digit, + uint64_t limit +){ + if (n > (limit / 10)) + return (1); + if (n == (limit / 10) && (uint64_t)(digit - '0') > (limit % 10)) + return (1); + return (0); +} diff --git a/src/builtins/export/export.c b/src/builtins/export/export.c new file mode 100644 index 0000000..e082ea7 --- /dev/null +++ b/src/builtins/export/export.c @@ -0,0 +1,79 @@ +/* ************************************************************************** */ +/* */ +/* ::: :::::::: */ +/* export.c :+: :+: :+: */ +/* +:+ +:+ +:+ */ +/* By: sede-san variables.environment); - envp = (char **)malloc((msh->variables.environment->size + 1) * sizeof(char *)); + envp = (char **)malloc( + (msh->variables.environment->size + 1) * sizeof(char *) + ); if (envp != NULL) { i = 0; diff --git a/src/variables/environment_unset.c b/src/variables/environment_unset.c new file mode 100644 index 0000000..feae147 --- /dev/null +++ b/src/variables/environment_unset.c @@ -0,0 +1,41 @@ +/* ************************************************************************** */ +/* */ +/* ::: :::::::: */ +/* environment_unset.c :+: :+: :+: */ +/* +:+ +:+ +:+ */ +/* By: sede-san variables.environment); + current = entries; + while (current != NULL) + { + entry = current->content; + if (ft_strcmp(entry->key, env_name) != 0) + ft_hashmap_put(new_env, + ft_strdup(entry->key), ft_strdup(entry->value)); + current = current->next; + } + ft_lstclear_nodes(&entries); + ft_hashmap_clear(&msh->variables.environment, free); + msh->variables.environment = new_env; +} diff --git a/tests/builtins_edge_cases.sh b/tests/builtins_edge_cases.sh new file mode 100755 index 0000000..f0e6477 --- /dev/null +++ b/tests/builtins_edge_cases.sh @@ -0,0 +1,204 @@ +#!/usr/bin/env bash + +set -u + +ROOT_DIR="$(cd -- "$(dirname -- "$0")/.." && pwd)" +MSH_BIN="$ROOT_DIR/minishell" +TMP_DIR="$(mktemp -d /tmp/minishell-builtins-tests.XXXXXX)" + +PASS_COUNT=0 +FAIL_COUNT=0 +CASE_OUT="" +CASE_ERR="" +CASE_EC=0 +CASE_NAME="" +CASE_FAILED=0 + +cleanup() { + rm -rf "$TMP_DIR" +} + +pass() { + printf "PASS: %s\n" "$1" + PASS_COUNT=$((PASS_COUNT + 1)) +} + +fail() { + printf "FAIL: %s\n" "$1" + FAIL_COUNT=$((FAIL_COUNT + 1)) + CASE_FAILED=1 +} + +assert_exit_code() { + local expected="$1" + if [ "$CASE_EC" -ne "$expected" ]; then + fail "$CASE_NAME -> exit code esperado $expected, obtenido $CASE_EC" + fi +} + +assert_err_contains() { + local text="$1" + if ! grep -Fq -- "$text" "$CASE_ERR"; then + fail "$CASE_NAME -> stderr no contiene: $text" + fi +} + +assert_out_contains() { + local text="$1" + if ! grep -Fq -- "$text" "$CASE_OUT"; then + fail "$CASE_NAME -> stdout no contiene: $text" + fi +} + +assert_out_regex_count() { + local regex="$1" + local expected="$2" + local count + + count="$(grep -Ec -- "$regex" "$CASE_OUT" || true)" + if [ "$count" -ne "$expected" ]; then + fail "$CASE_NAME -> regex [$regex] esperado $expected, obtenido $count" + fi +} + +run_case() { + local name="$1" + local input="$2" + local expected_exit="$3" + local clean_env="${4:-0}" + + CASE_NAME="$name" + CASE_OUT="$TMP_DIR/$name.out" + CASE_ERR="$TMP_DIR/$name.err" + CASE_EC=0 + CASE_FAILED=0 + + if [ "$clean_env" -eq 1 ]; then + printf "%b" "$input" | env -i PATH="$PATH" "$MSH_BIN" >"$CASE_OUT" 2>"$CASE_ERR" + else + printf "%b" "$input" | "$MSH_BIN" >"$CASE_OUT" 2>"$CASE_ERR" + fi + CASE_EC=$? + assert_exit_code "$expected_exit" + if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME" + fi +} + +run_pwd_deleted_dir_case() { + local tmpd + + tmpd="$TMP_DIR/pwd_deleted_dir" + mkdir -p "$tmpd" + run_case "pwd_deleted_dir" \ + "cd $tmpd\n/bin/rmdir $tmpd\npwd\nexit\n" \ + 1 + assert_err_contains "minishell: pwd:" + if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" + fi +} + +trap cleanup EXIT + +if [ ! -x "$MSH_BIN" ]; then + printf "Compilando minishell...\n" + if ! make -C "$ROOT_DIR" >/dev/null; then + printf "No se pudo compilar minishell\n" + exit 1 + fi +fi + +run_case "exit_non_numeric" "exit abc\n" 2 +assert_err_contains "numeric argument required" +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +run_case "exit_sign_only" "exit +\n" 2 +assert_err_contains "numeric argument required" +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +run_case "exit_negative_wrap" "exit -1\n" 255 + +run_case "exit_too_many_args" "exit 7 8\npwd\nexit\n" 0 +assert_err_contains "too many arguments" +assert_out_contains "$ROOT_DIR" +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +run_case "cd_home_not_set" "cd\nexit\n" 1 1 +assert_err_contains "HOME not set" +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +run_case "cd_invalid_path" "cd /definitely/not/here\nexit\n" 1 +assert_err_contains "No such file or directory" +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +run_case "cd_dash_roundtrip" "cd /tmp\ncd -\npwd\nexit\n" 0 +assert_out_contains "$ROOT_DIR" +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +run_pwd_deleted_dir_case + +run_case "echo_flags" "echo -nn hi\necho hi-there\nexit\n" 0 +assert_out_contains "himinishell >" +assert_out_contains "hi-there" +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +run_case "env_builtin_with_arg" "env EXTRA\nexit\n" 1 +assert_err_contains "too many arguments" +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +run_case "export_unset_cycle" \ + "export FOO=bar\n/usr/bin/env\nunset FOO\n/usr/bin/env\nexit\n" \ + 0 +assert_out_regex_count "^FOO=bar$" 1 +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +run_case "env_builtin_lists_exported" \ + "export FOO=bar\nenv\nunset FOO\nenv\nexit\n" \ + 0 +assert_out_regex_count "^FOO=bar$" 1 +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +run_case "export_invalid_identifier" "export 1A=2\nexit\n" 1 +assert_err_contains "not a valid identifier" +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +run_case "unset_invalid_identifier" "unset 1A\nexit\n" 1 +assert_err_contains "not a valid identifier" +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +run_case "export_without_value" "unset ZZZ\nexport ZZZ\n/usr/bin/env\nexit\n" 0 +assert_out_regex_count "^ZZZ=$" 1 +if [ "$CASE_FAILED" -eq 0 ]; then + pass "$CASE_NAME assertions" +fi + +printf "\nResumen tests builtins: PASS=%d FAIL=%d\n" "$PASS_COUNT" "$FAIL_COUNT" +if [ "$FAIL_COUNT" -ne 0 ]; then + exit 1 +fi +exit 0