Merge branch 'main' into builtins_adds

This commit is contained in:
savalet
2025-04-29 15:42:56 +02:00
committed by GitHub
27 changed files with 384 additions and 102 deletions

View File

@@ -43,9 +43,6 @@ jobs:
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v4
- name: Run the Magic Nix Cache
uses: DeterminateSystems/magic-nix-cache-action@v2
- name: Run coding style checker
run: |
nix run github:Sigmapitech/cs \
@@ -55,6 +52,8 @@ jobs:
- name: Build project
run: make
- name: Run tester
run: ./validator.py
sync_repository:
needs: [ check_the_repository_state ]

2
.gitignore vendored
View File

@@ -46,3 +46,5 @@ unit_tests
# AFL
afl/generated
out.txt

View File

@@ -35,7 +35,7 @@ CFLAGS += -Wduplicated-cond -Wformat=2 -Wshadow -fno-builtin
CFLAGS += -Wstrict-aliasing=0 -Wstrict-prototypes -Wunreachable-code
CFLAGS += -Wwrite-strings -Werror=declaration-after-statement
CFLAGS += -Werror=format-nonliteral -Werror=int-conversion -Werror=return-type
CFLAGS += -Wno-discarded-qualifiers
CFLAGS += -Wno-discarded-qualifiers --std=gnu2x
LDFLAGS += -L .
LDLIBS := -lu

View File

@@ -21,7 +21,7 @@
- [ ] `time`
- [ ] `trap`
- [ ] `wc`
- [ ] `which`/`where`
- [x] `which`/`where`
- [x] `yes`
- [x] pipes
@@ -41,8 +41,7 @@
- [ ] `-n` (dry run mode)
- [ ] `-h` help (open man?)
- [ ] autocompletion of commands
- [x] globbing (*)
- [ ] globbing
- [x] globbing
- [ ] var interpreter
- [ ] inhibitor
- [ ] magic quotes

View File

@@ -1 +1,2 @@
[reports]
merge = "multiplier"

5
iftest.sh Normal file
View File

@@ -0,0 +1,5 @@
if ls
echo YES
else
echo NO
endif

3
out.txt Normal file
View File

@@ -0,0 +1,3 @@
plop
again
again

View File

@@ -4,29 +4,27 @@
** File description:
** parse_alias
*/
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <unistd.h>
#include "utils.h"
#include "common.h"
#include "env.h"
#include "exec.h"
#include "u_mem.h"
#include "u_str.h"
#include "history.h"
#include "alias.h"
#include "common.h"
#include "history.h"
#include "utils.h"
static int skip_blank(char *buffer, int i)
static
int skip_blank(char *buffer, int i)
{
for (; buffer[i] != 0 && isblank(buffer[i]); i++);
return i;
}
static int skip_to_next_token(char *buffer, int i)
static
int skip_to_next_token(char *buffer, int i)
{
for (; buffer[i] != 0 && is_a_token(buffer, i) == false; i++);
return i;
@@ -97,3 +95,18 @@ int parse_alias(char **buffer, size_t *buffer_len, alias_t *alias)
need_to_replace = replace_alias(buffer, alias);
return RETURN_SUCCESS;
}
alias_t init_alias(void)
{
alias_t alias;
alias.size = 1;
alias.alias_array = malloc(sizeof(char *) * alias.size);
alias.alias_to_replace = malloc(sizeof(char *) * alias.size);
if (!alias.alias_array || !alias.alias_to_replace)
return alias;
alias.alias_array[0] = NULL;
alias.alias_to_replace[0] = NULL;
alias.size = 0;
return alias;
}

View File

@@ -7,9 +7,7 @@
#ifndef ALIAS_H
#define ALIAS_H
#include "env.h"
#include "history.h"
#include "shell.h"
#include <stddef.h>
typedef struct alias_s {
size_t size;
@@ -19,5 +17,5 @@ typedef struct alias_s {
void free_alias(alias_t *alias);
int parse_alias(char **buffer, size_t *buffer_len, alias_t *alias);
alias_t init_alias(void);
#endif /* ALIAS*/

View File

@@ -19,7 +19,7 @@
#define IF_PROMPT "if? "
#define T_ALL 0xff
typedef enum {
typedef enum : size_t {
T_SEMICOLON = 1 << 0, // ;
T_LEFT_QUOTE = 1 << 1, // "
T_RIGHT_QUOTE = 1 << 2, // "
@@ -37,13 +37,16 @@ typedef enum {
T_IN_REDIRECT = 1 << 14, // <
T_AT = 1 << 15, // <
T_WHILE = 1 << 16, // while
T_IF = 1 << 17, // if
T_THEN = 1 << 18, // then
T_ELSE = 1 << 19, // else
T_ENDIF = 1 << 20, // endif
T_EOF = 1 << 21, // \0
T_ARG = 1 << 22,
T_INVALID = 1 << 23
T_FOREACH = 1 << 17, // foreach
T_IF = 1 << 18, // if
T_THEN = 1 << 19, // then
T_ELSE = 1 << 20, // else
T_ENDIF = 1 << 21, // endif
T_STAR = 1 << 22, // *
T_NEWLINE = 1 << 23, // \n
T_EOF = 1 << 24, // \0
T_ARG = 1 << 25,
T_INVALID = 1 << 26
} token_type_t;
typedef enum {

View File

@@ -20,8 +20,12 @@ ast_t *parse_arg(ast_ctx_t *ctx, ast_t *node)
ctx->act_tok = get_next_token(ctx);
if (ctx->act_tok.type == T_SEMICOLON)
return node;
if (*ctx->act_tok.str == '\\') {
ctx->act_tok = get_next_token(ctx);
ctx->act_tok.type = T_ARG;
}
if (ctx->act_tok.type & (T_ARG | T_REDIRECT | T_APPEND |
T_IN_REDIRECT | T_HEREDOC | T_VAR)) {
T_IN_REDIRECT | T_HEREDOC | T_VAR | T_STAR)) {
if (!ensure_node_cap(node))
return NULL;
node->vector.tokens[node->vector.sz] = ctx->act_tok;
@@ -159,15 +163,15 @@ ast_t *create_semi_node(ast_ctx_t *ctx, ast_t *l_node)
static
ast_t *fill_semi_node(ast_ctx_t *ctx, ast_t *node)
{
while (ctx->act_tok.type == T_SEMICOLON) {
while (ctx->act_tok.type & (T_SEMICOLON | T_NEWLINE)) {
ctx->act_tok = get_next_token(ctx);
if (ctx->act_tok.type == T_SEMICOLON)
if (ctx->act_tok.type & (T_SEMICOLON | T_NEWLINE))
continue;
if (!ensure_list_cap(node))
return false;
return NULL;
node->list.nodes[node->list.sz] = parse_semi(ctx);
if (node->list.nodes[node->list.sz] == NULL)
return false;
return NULL;
node->list.sz++;
}
return node;
@@ -187,7 +191,7 @@ ast_t *parse_expression(ast_ctx_t *ctx)
l_node = parse_semi(ctx);
if (l_node == NULL)
return ctx->ast;
if (ctx->act_tok.type == T_SEMICOLON) {
if (ctx->act_tok.type & (T_SEMICOLON | T_NEWLINE)) {
node = create_semi_node(ctx, l_node);
if (node == NULL)
return NULL;

View File

@@ -32,6 +32,8 @@ const tokens_list_t TOKENS_LIST[] = {
{ T_THEN, "then", 4, "T_THEN"},
{ T_ELSE, "else", 4, "T_ELSE"},
{ T_ENDIF, "endif", 5, "T_ENDIF"},
{ T_NEWLINE, "\n", 1, "T_NEWLINE"},
{ T_STAR, "*", 1, "T_STAR"},
{ T_EOF, "\0", 1, "T_EOF" }
};

View File

@@ -20,10 +20,10 @@
bool check_local_var(char *var, char *func_name)
{
if (!isalpha(var[0]))
return (fprintf(stdout, "%s: Variable name must begin"
return (fprintf(stderr, "%s: Variable name must begin"
" with a letter.\n", func_name), RETURN_FAILURE);
if (!u_str_is_only_alnum(var))
return (fprintf(stdout, "%s: Variable name must contain"
return (fprintf(stderr, "%s: Variable name must contain"
" alphanumeric characters.\n", func_name), RETURN_FAILURE);
return RETURN_SUCCESS;
}

View File

@@ -8,17 +8,21 @@
#ifndef BUILTINS_HANDLER_H
#define BUILTINS_HANDLER_H
#include <termios.h>
#include "alias.h"
#include "env.h"
#include "history.h"
#include "shell.h"
#include "alias.h"
#include "local.h"
#include "shell.h"
typedef struct {
env_t *env;
history_t *history;
his_command_t *history_command;
alias_t *alias;
bool is_running;
struct termios saved_term_settings;
local_t *local;
} exec_ctx_t;

View File

@@ -7,8 +7,12 @@
#ifndef COMMON_H
#define COMMON_H
#include "exec.h"
enum {
RETURN_SUCCESS = 0,
RETURN_FAILURE = 1
};
void free_everything(exec_ctx_t *exec_ctx);
#endif /* COMMON_H */

View File

@@ -23,7 +23,6 @@
#include "path.h"
#include "u_mem.h"
#include "u_str.h"
#include "alias.h"
const builtins_funcs_t BUILTINS[] = {
{ "builtins", &builtins_builtins },

View File

@@ -29,15 +29,17 @@ bool process_globbing(char *pattern, args_t *args)
{
glob_t globs;
int glob_result;
char *vl;
glob_result = glob(pattern, GLOB_ERR, NULL, &globs);
if (!check_glob_result(glob_result, args->args[0]))
return false;
for (size_t i = 0; i < globs.gl_pathc; i++) {
ensure_args_capacity(args);
args->args[args->sz] = strdup(globs.gl_pathv[i]);
if (args->args[args->sz] == NULL)
vl = strdup(globs.gl_pathv[i]);
if (vl == NULL)
return globfree(&globs), false;
args->args[args->sz] = vl;
args->sz++;
}
globfree(&globs);
@@ -48,7 +50,11 @@ bool process_args(ast_t *node, args_t *args, size_t *toks_i, ef_t *ef)
{
token_t tok = node->vector.tokens[*toks_i];
if (strchr(tok.str, '*') != NULL)
if (strchr(tok.str, '\\') != NULL) {
args->args[args->sz] = tok.str;
return true;
}
if (tok.type == T_STAR || strcspn(tok.str, "[]?") != strlen(tok.str))
return (process_globbing(tok.str, args));
if (!ensure_args_capacity(args))
return false;

View File

@@ -6,6 +6,7 @@
*/
#include <stdio.h>
#include <unistd.h>
#include "ast.h"
#include "env.h"
@@ -22,7 +23,7 @@ char *handle_var_case(ast_t *node, exec_ctx_t *ctx, size_t *i)
if (r_char == NULL)
r_char = get_local_value(ctx->local, node->vector.tokens[*i].str);
if (r_char == NULL) {
printf("%s: Undefined variable.\n",
dprintf(STDERR_FILENO, "%s: Undefined variable.\n",
node->vector.tokens[*i].str);
return NULL;
}

118
src/readline.c Normal file
View File

@@ -0,0 +1,118 @@
/*
** EPITECH PROJECT, 2025
** __
** File description:
** _
*/
#include <ctype.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <termios.h>
#include <unistd.h>
#include "common.h"
#include "readline.h"
#include "u_str.h"
static
bool str_printable(char const *str, size_t size)
{
for (size_t i = 0; i < size; i++)
if (!isprint(str[i]))
return false;
return true;
}
static
bool ensure_buff_av_capacity(buff_t *buff, size_t requested)
{
char *new_str;
size_t endsize = BUFF_INIT_SZ;
if ((buff->sz + requested) < buff->cap)
return true;
for (; endsize < buff->sz + requested; endsize <<= 1);
if (endsize > buff->cap) {
new_str = realloc(buff->str, (sizeof *buff->str) * endsize);
if (new_str == NULL)
return false;
buff->str = new_str;
buff->cap = endsize;
}
return true;
}
static
bool ensure_buff_capacity(buff_t *buff)
{
char *new_str;
if (buff->str == NULL) {
new_str = malloc((sizeof *buff->str) * BUFF_INIT_SZ);
if (new_str == NULL)
return false;
buff->str = new_str;
buff->cap = BUFF_INIT_SZ;
}
if (buff->sz == buff->cap) {
new_str = realloc(buff->str, (sizeof *buff->str) * buff->cap << 1);
if (new_str == NULL)
return false;
buff->str = new_str;
buff->cap <<= 1;
}
return true;
}
static
bool append_null_terminator(buff_t *buff)
{
if (!ensure_buff_av_capacity(buff, 1))
return false;
buff->str[buff->sz - 1] = '\0';
buff->sz++;
if (isatty(STDIN_FILENO))
WRITE_CONST(STDOUT_FILENO, "\n");
return true;
}
static
int8_t handle_line_buff(buff_t *buff, char *read_buff, ssize_t read_size)
{
if (*read_buff == CTRL('d')) {
buff->sz = 0;
return RETURN_SUCCESS;
}
if (isatty(STDIN_FILENO) && str_printable(read_buff, read_size))
write(STDOUT_FILENO, read_buff, read_size);
if (!ensure_buff_av_capacity(buff, read_size))
return RETURN_FAILURE;
strncpy(buff->str + buff->sz,
read_buff, read_size);
buff->sz += read_size;
return -1;
}
bool readline(buff_t *buff)
{
char read_buff[2] = "";
ssize_t read_size = 0;
if (!ensure_buff_capacity(buff))
return false;
while (strchr(read_buff, '\n') == NULL && *read_buff != '\r') {
memset(read_buff, '\0', sizeof read_buff);
read_size = read(STDIN_FILENO, &read_buff, sizeof read_buff - 1);
if (read_size < 0)
return false;
if (read_size == 0)
return true;
if (handle_line_buff(buff, read_buff, read_size) > -1)
return true;
}
return append_null_terminator(buff);
}

16
src/readline.h Normal file
View File

@@ -0,0 +1,16 @@
/*
** EPITECH PROJECT, 2025
** __
** File description:
** _
*/
#ifndef READLINE
#define READLINE
#define BUFF_INIT_SZ 16
#include <stdbool.h>
#include "u_str.h"
bool readline(buff_t *buff);
#endif /* READLINE */

View File

@@ -11,15 +11,16 @@
#include <stdlib.h>
#include <unistd.h>
#include "ast.h"
#include "builtins_handler.h"
#include "common.h"
#include "debug.h"
#include "env.h"
#include "history.h"
#include "local.h"
#include "readline.h"
#include "shell.h"
#include "u_str.h"
#include "local.h"
#include "loop.h"
#include "visitor.h"
__attribute__((unused))
static
@@ -58,44 +59,60 @@ void write_prompt(int is_a_tty)
}
static
bool change_shell_command(char **buffer, exec_ctx_t *exec_ctx,
size_t buffer_sz)
bool change_shell_command(buff_t *buff, exec_ctx_t *exec_ctx)
{
size_t buffer_len = 0;
char *tmp_buff = NULL;
size_t buffer_len;
if (getline(buffer, &buffer_sz, stdin) == -1)
return true;
tmp_buff = (*buffer);
buffer_len = update_command(&tmp_buff, &buffer_sz, exec_ctx);
buff->sz = 0;
if (!readline(buff))
return false;
if (!buff->sz)
return false;
tmp_buff = buff->str;
buffer_len = update_command(&tmp_buff, &buff->sz, exec_ctx);
if (buffer_len == 0)
return false;
if (buffer_len < 1 || !u_str_is_alnum(tmp_buff)) {
check_basic_error(tmp_buff);
free(tmp_buff);
return false;
return true;
if (buffer_len < 1 || !u_str_is_alnum(tmp_buff))
return check_basic_error(tmp_buff), true;
U_DEBUG("Buffer [%lu] [%s]\n", buffer_len, tmp_buff);
if (visitor(tmp_buff, exec_ctx) == RETURN_FAILURE
&& !exec_ctx->history->last_exit_code)
exec_ctx->history->last_exit_code = RETURN_FAILURE;
return true;
}
static
void init_shell_repl(exec_ctx_t *exec_ctx)
{
struct termios repl_settings;
exec_ctx->is_running = true;
if (isatty(STDIN_FILENO)) {
tcgetattr(STDIN_FILENO, &repl_settings);
exec_ctx->saved_term_settings = repl_settings;
repl_settings.c_iflag = IXON;
repl_settings.c_lflag = ~(ECHO | ICANON);
tcsetattr(STDIN_FILENO, TCSANOW, &repl_settings);
}
U_DEBUG("Buffer [%lu] [%s]\n", buffer_len, buffer);
visitor(tmp_buff, exec_ctx);
free(tmp_buff);
return false;
}
static
int shell_loop(int is_a_tty, exec_ctx_t *exec_ctx)
{
char *buffer = NULL;
size_t buffer_sz = 0;
buff_t buff = { .str = NULL, 0, .cap = BUFF_INIT_SZ };
init_shell_repl(exec_ctx);
while (true) {
write_prompt(is_a_tty);
if (change_shell_command(&buffer, exec_ctx, buffer_sz) == true)
if (!change_shell_command(&buff, exec_ctx))
return exec_ctx->history->last_exit_code;
}
free(exec_ctx->history_command);
return (free(buffer), exec_ctx->history->last_exit_code);
return free(buff.str), exec_ctx->history->last_exit_code;
}
static
his_command_t *init_cmd_history(void)
{
his_command_t *cmd_history = malloc(sizeof(his_command_t) * 100);
@@ -126,41 +143,26 @@ bool error_in_init(exec_ctx_t *exec_ctx)
return false;
}
alias_t init_alias(void)
{
alias_t alias;
alias.size = 1;
alias.alias_array = malloc(sizeof(char *) * alias.size);
alias.alias_to_replace = malloc(sizeof(char *) * alias.size);
if (!alias.alias_array || !alias.alias_to_replace)
return alias;
alias.alias_array[0] = NULL;
alias.alias_to_replace[0] = NULL;
alias.size = 0;
return alias;
}
int shell(char **env_ptr)
{
alias_t alias = init_alias();
env_t env = parse_env(env_ptr);
history_t history = { .cmd_history = NULL, 0, .last_chdir = NULL};
history_t history = { .cmd_history = NULL, .last_exit_code = 0,
.last_chdir = NULL};
his_command_t *cmd_history = init_cmd_history();
local_t local = create_local();
exec_ctx_t exec_ctx = {.env = &env, .local = &local,
.history = &history, .history_command = cmd_history, .alias = &alias};
int shell_result;
if (error_in_init(&exec_ctx) == true){
if (error_in_init(&exec_ctx) == true)
return RETURN_FAILURE;
}
U_DEBUG_CALL(debug_env_entries, &env);
signal(SIGINT, ignore_sigint);
shell_result = shell_loop(isatty(STDIN_FILENO), &exec_ctx);
if (isatty(STDIN_FILENO))
if (isatty(STDIN_FILENO)) {
WRITE_CONST(STDOUT_FILENO, "exit\n");
free_env(exec_ctx.env);
free_alias(exec_ctx.alias);
return shell_result;
tcsetattr(STDIN_FILENO, TCSANOW, &exec_ctx.saved_term_settings);
}
return free_everything(&exec_ctx), shell_result;
}

View File

@@ -8,6 +8,7 @@
#ifndef SHELL_H
#define SHELL_H
#include "vt100_esc_codes.h"
#define SHELL_PROMPT RED "|> " RESET
typedef struct {
@@ -15,5 +16,6 @@ typedef struct {
int last_exit_code;
char *last_chdir;
} history_t;
int shell(char **env);
#endif /* SHELL_H */

View File

@@ -41,7 +41,6 @@ size_t update_command(char **buffer,
buffer_len = u_strlen(*buffer);
if (buffer_len < 2)
return RETURN_FAILURE;
(*buffer)[buffer_len - 1] = '\0';
if (parse_history(buffer, &buffer_len,
buffer_sz, &exec_ctx->history_command) == 84)
return RETURN_SUCCESS;

17
src/utils/free.c Normal file
View File

@@ -0,0 +1,17 @@
/*
** EPITECH PROJECT, 2025
** __
** File description:
** _
*/
#include <stdlib.h>
#include "exec.h"
void free_everything(exec_ctx_t *exec_ctx)
{
free_env(exec_ctx->env);
free_alias(exec_ctx->alias);
free(exec_ctx->history_command);
}

View File

@@ -146,7 +146,7 @@ int visit_expression(ef_t *ef, ast_t *node)
{
int result = RETURN_FAILURE;
if (node->tok.type == T_SEMICOLON)
if (node->tok.type & (T_SEMICOLON | T_NEWLINE))
result = visit_semi(ef, node);
if (node->tok.type & (T_IF | T_AND | T_OR))
result = visit_condition(ef, node);
@@ -178,6 +178,8 @@ void remove_trailing_semi(char *str)
break;
if (str[len] == ';')
str[len] = '\0';
if (str[len] == '\n')
str[len] = '\0';
}
}

View File

@@ -37,9 +37,9 @@ TESTS = [
key="PATH",
name="path handing",
cmds=[
"/bin/sh --version\n",
"/../bin/sh --version\n",
"~/../../bin/sh --version\n",
"/bin/ls\n",
"/../bin/ls\n",
# "~/../../bin/sh --version\n",
"fixtures/exec.sh\n",
],
depends_on=("ARGS",)
@@ -101,4 +101,75 @@ TESTS = [
],
depends_on=("ARGS",)
),
Test(
key="REDIR",
name="I/O redirections",
cmds=[
"echo plop > out.txt\n",
"cat < out.txt\n",
"echo again >> out.txt\n",
"cat < out.txt\n",
"cat nofile.txt\n",
"echo test > /no/perm/file\n",
],
depends_on=("ARGS",)
),
Test(
key="PIPE",
name="pipes",
cmds=[
"echo plop | cat\n",
"echo kek | grep kek\n",
"ls | grep Makefile\n",
"who | wc -l\n",
"ls | | cat\n", # Syntax error
],
depends_on=("ARGS",)
),
Test(
key="ENV_EXPANSION",
name="environment variable expansion",
cmds=[
"echo $HOME\n",
"echo $PATH\n",
"setenv TESTVAR bonjour;echo $TESTVAR;unsetenv TESTVAR;echo $TESTVAR\n",
],
depends_on=("ENV","SEMICOLON",)
),
Test(
key="EXIT",
name="exit command",
cmds=[
"exit\n",
],
depends_on=("ARGS",)
),
Test(
key="MULTICMD",
name="multiple commands per line",
cmds=[
"echo one ; echo two ; echo three\n",
"ls ; pwd ; whoami\n",
"cd src/ ; pwd ; cd - ; pwd\n",
],
depends_on=("SEMICOLON",)
),
Test(
key="PARSING_ERR",
name="bad parsing cases",
cmds=[
"ls |\n",
"ls >\n",
"; ls\n",
";; ls\n",
"ls ;; ls\n",
],
depends_on=("PIPE", "REDIR", "SEMICOLON",)
),
]

24
validator.py Normal file → Executable file
View File

@@ -1,8 +1,12 @@
#! /usr/bin/env nix-shell
#! nix-shell -i python3 -p python3 tcsh
from __future__ import annotations
from dataclasses import dataclass
import difflib
import subprocess
import sys
@dataclass
@@ -84,21 +88,25 @@ class Test:
print("\033[31m.\033[0m", end='') # ]]
return cmd, result_42sh, result_tcsh
def run(self, test_map):
def run(self, test_map) -> bool:
if self.has_run:
return
return True
self.has_run = True
success = True
for dep_name in self.depends_on:
dep = test_map.get(dep_name)
if dep is None:
print("\033[33mOKWarning\033[0m:" # ]]
print("\033[33mWarning\033[0m:" # ]]
"Missing dependency:", dep_name)
continue
if not dep.has_run:
dep.run(test_map)
success &= dep.run(test_map)
if not success:
return False
print(self.name, end=" ")
failures = []
@@ -108,19 +116,23 @@ class Test:
failures.append(failure)
if not failures:
print(" \033[32mOK\033[0m") # ]]
return True
else:
print()
for fail in failures:
print_diff(*fail)
return False
def main():
from validation_tests import TESTS
test_map = {test.key: test for test in TESTS}
success = True
for test in TESTS:
test.run(test_map=test_map)
success &= test.run(test_map=test_map)
return not success
if __name__ == "__main__":
main()
sys.exit(main())