From 36df14e5990e4b6b8b2981bbef956266430694a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kha=C3=AFs=20COLIN?= Date: Fri, 21 Feb 2025 16:04:47 +0100 Subject: [PATCH] refactor pipeline: use a state machine for parsing This allows for easier-to-norm code --- Makefile | 2 + src/parser/pipeline/pipeline.c | 90 ++++++---------- src/parser/pipeline/pipeline.h | 73 ++++++++++++- src/parser/pipeline/pipeline_parse.c | 108 +++++++++++++++++++ src/parser/pipeline/pipeline_parse.h | 21 ++++ src/parser/pipeline/pipeline_parse_baseops.c | 65 +++++++++++ src/parser/pipeline/pipeline_parse_baseops.h | 23 ++++ 7 files changed, 322 insertions(+), 60 deletions(-) create mode 100644 src/parser/pipeline/pipeline_parse.c create mode 100644 src/parser/pipeline/pipeline_parse.h create mode 100644 src/parser/pipeline/pipeline_parse_baseops.c create mode 100644 src/parser/pipeline/pipeline_parse_baseops.h diff --git a/Makefile b/Makefile index b88712e..5d92a94 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,8 @@ srcs = \ src/parser/matchers/pipe.c \ src/parser/matchers/quote.c \ src/parser/pipeline/pipeline.c \ + src/parser/pipeline/pipeline_parse_baseops.c \ + src/parser/pipeline/pipeline_parse.c \ src/parser/simple_cmd/simple_cmd.c \ src/parser/worddesc/worddesc.c \ src/parser/wordlist/wordlist.c \ diff --git a/src/parser/pipeline/pipeline.c b/src/parser/pipeline/pipeline.c index a421b07..2500c44 100644 --- a/src/parser/pipeline/pipeline.c +++ b/src/parser/pipeline/pipeline.c @@ -6,72 +6,23 @@ /* By: khais +#+ +:+ +#+ */ /* +#+#+#+#+#+ +#+ */ /* Created: 2025/02/21 13:23:50 by khais #+# #+# */ -/* Updated: 2025/02/21 15:42:34 by khais ### ########.fr */ +/* Updated: 2025/02/24 15:03:25 by khais ### ########.fr */ /* */ /* ************************************************************************** */ #include "pipeline.h" +#include "pipeline_parse.h" #include "libft.h" -#include "../../ft_errno.h" -#include "../matchers/pipe.h" - -static int pipeline_count_cmds(t_wordlist *words) -{ - int count; - - if (words == NULL) - return (0); - count = 1; - while (words != NULL) - { - if (is_pipe(words->word)) - count++; - words = words->next; - } - return (count); -} - -static t_worddesc *ignore_word(t_worddesc *current_word, t_wordlist **words) -{ - worddesc_destroy(current_word); - return (wordlist_pop(words)); -} - -static t_pipeline *pipeline_parse_wordlist(t_pipeline *pipeline, - t_wordlist *words) -{ - int idx; - t_wordlist *current_wordlist; - t_worddesc *current_word; - - idx = 0; - current_wordlist = NULL; - current_word = wordlist_pop(&words); - while (current_word != NULL) - { - if (is_pipe(current_word)) - { - pipeline->cmds[idx] = simple_cmd_from_wordlist(current_wordlist); - if (pipeline->cmds[idx] == NULL) - return (pipeline_destroy(pipeline), wordlist_destroy(words), NULL); - current_wordlist = NULL; - current_word = ignore_word(current_word, &words); - if (is_pipe(current_word)) - return (pipeline_destroy(pipeline), wordlist_destroy(words), worddesc_destroy(current_word), ft_errno(FT_EUNEXPECTED_PIPE), NULL); - idx++; - } - current_wordlist = wordlist_push(current_wordlist, current_word); - current_word = wordlist_pop(&words); - } - pipeline->cmds[idx] = simple_cmd_from_wordlist(current_wordlist); - if (pipeline->cmds[idx] == NULL) - return (pipeline_destroy(pipeline), NULL); - return (pipeline); -} +/* +** Parse a pipeline from the given wordlist. +** +** Return NULL and set ft_errno in case of error. +*/ t_pipeline *pipeline_from_wordlist(t_wordlist *words) { - t_pipeline *pipeline; + t_pipeline *pipeline; + t_pipeline_builder builder; if (words == NULL) return (NULL); @@ -82,10 +33,16 @@ t_pipeline *pipeline_from_wordlist(t_wordlist *words) pipeline->cmds = ft_calloc(pipeline->num_cmd, sizeof(t_simple_cmd *)); if (pipeline->cmds == NULL) return (NULL); - pipeline = pipeline_parse_wordlist(pipeline, words); + ft_bzero(&builder, sizeof(t_pipeline_builder)); + builder.words = words; + builder.pipeline = pipeline; + pipeline = pipeline_parse_wordlist(&builder); return (pipeline); } +/* +** Destroy the given pipeline, freeing all memory. +*/ void pipeline_destroy(t_pipeline *pipeline) { int i; @@ -101,3 +58,18 @@ void pipeline_destroy(t_pipeline *pipeline) free(pipeline->cmds); free(pipeline); } + +/* +** Free all memory for the given builder, set freed variables to NULL, and set +** error to true +*/ +void builder_destroy(t_pipeline_builder *builder) +{ + pipeline_destroy(builder->pipeline); + builder->pipeline = NULL; + worddesc_destroy(builder->current_word); + builder->current_word = NULL; + wordlist_destroy(builder->words); + builder->words = NULL; + builder->error = true; +} diff --git a/src/parser/pipeline/pipeline.h b/src/parser/pipeline/pipeline.h index ffea8ca..bd57dfe 100644 --- a/src/parser/pipeline/pipeline.h +++ b/src/parser/pipeline/pipeline.h @@ -6,7 +6,7 @@ /* By: khais +#+ +:+ +#+ */ /* +#+#+#+#+#+ +#+ */ /* Created: 2025/02/21 13:19:51 by khais #+# #+# */ -/* Updated: 2025/02/21 13:27:50 by khais ### ########.fr */ +/* Updated: 2025/02/24 15:02:35 by khais ### ########.fr */ /* */ /* ************************************************************************** */ @@ -14,14 +14,85 @@ # define PIPELINE_H # include "../simple_cmd/simple_cmd.h" +# include +/* +** cf. 3.2.3 Pipelines +** +** A pipeline is a sequence of one or more commands separated by the control +** operator '|'. +** +** The output of each command in the pipeline is connected via a pipe to the +** input of the next command. +** +** That is, each command reads the previous command’s output. +** +** This connection is performed before any redirections specified by the first +** command. +** +** The shell waits for all commands in the pipeline to complete before reading +** the next command. +** +** Each command in a multi-command pipeline, where pipes are created, is +** executed in its own _subshell_, which is a separate process. +** +** e.g. +** +** ```shell +** export TT=1 | echo $TT +** ``` +** +** prints an empty string, because TT is unset in the second subshell. +** +** The exit status of a pipeline is the exit status of the last command in the +** pipeline. +** +** The shell waits for all commands in the pipeline to terminate before +** returning a value. +*/ typedef struct s_pipeline { + /* + ** The commands in the pipeline + */ t_simple_cmd **cmds; + /* + ** The number of commands in this pipeline + */ int num_cmd; } t_pipeline; +typedef struct s_pipeline_builder +{ + /* + ** index of the command we are currently working on + */ + int idx; + /* + ** wordlist that is being built up to be the wordlist of the command being + ** parsed + */ + t_wordlist *current_wordlist; + /* + ** word that is currently being considered + */ + t_worddesc *current_word; + /* + ** linked list of words that are being parsed + */ + t_wordlist *words; + /* + ** pipeline that is currently being constructed + */ + t_pipeline *pipeline; + /* + ** true if an error occured, which should interrupt processing + */ + bool error; +} t_pipeline_builder; + t_pipeline *pipeline_from_wordlist(t_wordlist *words); void pipeline_destroy(t_pipeline *pipeline); +void builder_destroy(t_pipeline_builder *builder); #endif diff --git a/src/parser/pipeline/pipeline_parse.c b/src/parser/pipeline/pipeline_parse.c new file mode 100644 index 0000000..85e01a7 --- /dev/null +++ b/src/parser/pipeline/pipeline_parse.c @@ -0,0 +1,108 @@ +/* ************************************************************************** */ +/* */ +/* ::: :::::::: */ +/* pipeline_parse.c :+: :+: :+: */ +/* +:+ +:+ +:+ */ +/* By: khais +#+ +:+ +#+ */ +/* +#+#+#+#+#+ +#+ */ +/* Created: 2025/02/24 14:36:43 by khais #+# #+# */ +/* Updated: 2025/02/24 15:09:35 by khais ### ########.fr */ +/* */ +/* ************************************************************************** */ + +#include "pipeline_parse.h" +#include "pipeline_parse_baseops.h" +#include "../../ft_errno.h" +#include "../matchers/pipe.h" +#include + +/* +** count the number of commands in a pipeline from the given words. +** +** A command is separated from the next by a pipe token. +** +** Note that this does not handle errors such as repeated pipe tokens, or pipe +** tokens at the wrong place. +*/ +int pipeline_count_cmds(t_wordlist *words) +{ + int count; + + if (words == NULL) + return (0); + count = 1; + while (words != NULL) + { + if (is_pipe(words->word)) + count++; + words = words->next; + } + return (count); +} + +/* +** Delimit a command in the pipeline. +** +** - Create a new simple command from the wordlist that has been accumulated +** thus far. +** - Reset the current wordlist. +** - Ignore the current word, which is assumed to be a pipe token. +** - Check if the new current word is a pipe token, and if it is, signal +** FT_EUNEXPECTED_PIPE. +** +** Destroys the builder and sets error in case of error. +*/ +static void pipeline_delimit(t_pipeline_builder *builder) +{ + builder->pipeline->cmds[builder->idx] + = simple_cmd_from_wordlist(builder->current_wordlist); + if (builder->pipeline->cmds[builder->idx] == NULL) + { + builder_destroy(builder); + return ; + } + builder->current_wordlist = NULL; + ignore_word(builder); + if (current_is_pipe(builder)) + { + builder_destroy(builder); + ft_errno(FT_EUNEXPECTED_PIPE); + return ; + } + builder->idx++; +} + +/* +** Parse a pipeline from a wordlist using the given builder. +** +** - Assumes that current word is unset. +** - Load the next word. +** - While there are still words remaining in the input stream +** - If we have a pipe character +** - Delimit the current command +** - If there was an error +** - Return NULL +** - Push the current word to the accumulator for the next command +** - Advance the state to the next word +** - Since there is no pipe token at the end to delimit the last command, +** delimit it now +** - Return the pipeline. Since all input tokens have been consumed, no +** additional memory freeing is required. +** in case of error in pipeline_delimit, it will call destroy, which will set +** builder->pipeline to NULL +*/ +t_pipeline *pipeline_parse_wordlist(t_pipeline_builder *builder) +{ + next_word(builder); + while (!eof(builder)) + { + if (current_is_pipe(builder)) + pipeline_delimit(builder); + if (builder->error) + return (NULL); + push_word(builder); + next_word(builder); + } + pipeline_delimit(builder); + return (builder->pipeline); +} diff --git a/src/parser/pipeline/pipeline_parse.h b/src/parser/pipeline/pipeline_parse.h new file mode 100644 index 0000000..5b8ecdd --- /dev/null +++ b/src/parser/pipeline/pipeline_parse.h @@ -0,0 +1,21 @@ +/* ************************************************************************** */ +/* */ +/* ::: :::::::: */ +/* pipeline_parse.h :+: :+: :+: */ +/* +:+ +:+ +:+ */ +/* By: khais +#+ +:+ +#+ */ +/* +#+#+#+#+#+ +#+ */ +/* Created: 2025/02/24 14:34:55 by khais #+# #+# */ +/* Updated: 2025/02/24 15:01:46 by khais ### ########.fr */ +/* */ +/* ************************************************************************** */ + +#ifndef PIPELINE_PARSE_H +# define PIPELINE_PARSE_H + +# include "pipeline.h" + +int pipeline_count_cmds(t_wordlist *words); +t_pipeline *pipeline_parse_wordlist(t_pipeline_builder *builder); + +#endif // PIPELINE_PARSE_H diff --git a/src/parser/pipeline/pipeline_parse_baseops.c b/src/parser/pipeline/pipeline_parse_baseops.c new file mode 100644 index 0000000..4c1d89e --- /dev/null +++ b/src/parser/pipeline/pipeline_parse_baseops.c @@ -0,0 +1,65 @@ +/* ************************************************************************** */ +/* */ +/* ::: :::::::: */ +/* pipeline_parse_baseops.c :+: :+: :+: */ +/* +:+ +:+ +:+ */ +/* By: khais +#+ +:+ +#+ */ +/* +#+#+#+#+#+ +#+ */ +/* Created: 2025/02/24 15:06:33 by khais #+# #+# */ +/* Updated: 2025/02/24 15:10:09 by khais ### ########.fr */ +/* */ +/* ************************************************************************** */ + +#include "pipeline_parse_baseops.h" +#include "../matchers/pipe.h" +#include + +/* +** Advance the state to the next word +*/ +void next_word(t_pipeline_builder *builder) +{ + builder->current_word = wordlist_pop(&builder->words); +} + +/* +** If the current word is NULL, return true. +** +** Used to detect end of word stream. +** +** Assumes that next_word was previously called. +*/ +bool eof(t_pipeline_builder *builder) +{ + return (builder->current_word == NULL); +} + +/* +** return true if the given current word is a pipe operator "|" +*/ +bool current_is_pipe(t_pipeline_builder *builder) +{ + return (builder->current_word != NULL && is_pipe(builder->current_word)); +} + +/* +** Remove the current word from the wordstream and advance to the next word. +*/ +void ignore_word(t_pipeline_builder *builder) +{ + worddesc_destroy(builder->current_word); + next_word(builder); +} + +/* +** push the current word to the wordlist that will be used for the next simple +** command. +** +** Resets current word to NULL. +*/ +void push_word(t_pipeline_builder *builder) +{ + builder->current_wordlist + = wordlist_push(builder->current_wordlist, builder->current_word); + builder->current_word = NULL; +} diff --git a/src/parser/pipeline/pipeline_parse_baseops.h b/src/parser/pipeline/pipeline_parse_baseops.h new file mode 100644 index 0000000..ded8413 --- /dev/null +++ b/src/parser/pipeline/pipeline_parse_baseops.h @@ -0,0 +1,23 @@ +/* ************************************************************************** */ +/* */ +/* ::: :::::::: */ +/* pipeline_parse_baseops.h :+: :+: :+: */ +/* +:+ +:+ +:+ */ +/* By: khais +#+ +:+ +#+ */ +/* +#+#+#+#+#+ +#+ */ +/* Created: 2025/02/24 15:05:16 by khais #+# #+# */ +/* Updated: 2025/02/24 15:09:01 by khais ### ########.fr */ +/* */ +/* ************************************************************************** */ +#ifndef PIPELINE_PARSE_BASEOPS_H +# define PIPELINE_PARSE_BASEOPS_H + +# include "pipeline.h" + +void ignore_word(t_pipeline_builder *builder); +bool current_is_pipe(t_pipeline_builder *builder); +void next_word(t_pipeline_builder *builder); +bool eof(t_pipeline_builder *builder); +void push_word(t_pipeline_builder *builder); + +#endif // PIPELINE_PARSE_BASEOPS_H