diff --git a/crates/deno_task_shell/src/grammar.pest b/crates/deno_task_shell/src/grammar.pest index d2a992a..8a98850 100644 --- a/crates/deno_task_shell/src/grammar.pest +++ b/crates/deno_task_shell/src/grammar.pest @@ -107,11 +107,11 @@ OPERATOR = _{ } // Reserved words -If = { "if" } -Then = { "then" } +If = _{ "if" } +Then = _{ "then" } Else = { "else" } Elif = { "elif" } -Fi = { "fi" } +Fi = _{ "fi" } Do = { "do" } Done = { "done" } Case = { "case" } @@ -147,7 +147,6 @@ command = !{ } compound_command = { - double_square_bracket | brace_group | subshell | for_clause | @@ -167,8 +166,6 @@ for_clause = { do_group } -double_square_bracket = !{ "[[" ~ compound_list ~ "]]" } - case_clause = !{ Case ~ UNQUOTED_PENDING_WORD ~ linebreak ~ linebreak ~ In ~ linebreak ~ @@ -197,14 +194,55 @@ pattern = !{ } if_clause = !{ - If ~ compound_list ~ - linebreak ~ Then ~ linebreak ~ compound_list ~ linebreak ~ + If ~ conditional_expression ~ + linebreak ~ Then ~ linebreak ~ complete_command ~ linebreak ~ else_part? ~ linebreak ~ Fi } else_part = !{ - Elif ~ compound_list ~ Then ~ linebreak ~ else_part | - Else ~ linebreak ~ compound_list + Elif ~ conditional_expression ~ Then ~ complete_command ~ linebreak ~ else_part? | + Else ~ linebreak ~ complete_command +} + +conditional_expression = !{ + ("[[" ~ (unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD) ~ "]]") | + ("[" ~ (unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD) ~ "]") | + ("test" ~ (unary_conditional_expression | binary_conditional_expression | UNQUOTED_PENDING_WORD)) +} + +unary_conditional_expression = !{ + file_conditional_op ~ FILE_NAME_PENDING_WORD | + variable_conditional_op ~ VARIABLE | + string_conditional_op ~ UNQUOTED_PENDING_WORD +} + +file_conditional_op = !{ + "-a" | "-b" | "-c" | "-d" | "-e" | "-f" | "-g" | "-h" | "-k" | + "-p" | "-r" | "-s" | "-u" | "-w" | "-x" | "-G" | "-L" | + "-N" | "-O" | "-S" +} + +variable_conditional_op = !{ + "-v" | "-R" +} + +string_conditional_op = !{ + "-n" | "-z" +} + +binary_conditional_expression = !{ + UNQUOTED_PENDING_WORD ~ ( + binary_string_conditional_op | + binary_arithmetic_conditional_op + ) ~ UNQUOTED_PENDING_WORD +} + +binary_string_conditional_op = !{ + "==" | "=" | "!=" | "<" | ">" +} + +binary_arithmetic_conditional_op = !{ + "-eq" | "-ne" | "-lt" | "-le" | "-gt" | "-ge" } while_clause = !{ While ~ compound_list ~ do_group } diff --git a/crates/deno_task_shell/src/parser.rs b/crates/deno_task_shell/src/parser.rs index 6f8afab..16f3c31 100644 --- a/crates/deno_task_shell/src/parser.rs +++ b/crates/deno_task_shell/src/parser.rs @@ -158,6 +158,8 @@ pub enum CommandInner { Simple(SimpleCommand), #[error("Invalid subshell")] Subshell(Box), + #[error("Invalid if command")] + If(IfClause), } impl From for Sequence { @@ -207,6 +209,92 @@ impl From for Sequence { } } +#[cfg_attr(feature = "serialization", derive(serde::Serialize))] +#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] +#[derive(Debug, PartialEq, Eq, Clone, Error)] +#[error("Invalid if clause")] +pub struct IfClause { + pub condition: Condition, + pub then_body: SequentialList, + pub else_part: Option, +} + +#[cfg_attr(feature = "serialization", derive(serde::Serialize))] +#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] +#[derive(Debug, PartialEq, Eq, Clone, Error)] +#[error("Invalid else part")] +pub enum ElsePart { + Elif(Box), + Else(SequentialList), +} + +#[cfg_attr(feature = "serialization", derive(serde::Serialize))] +#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] +#[derive(Debug, PartialEq, Eq, Clone, Error)] +#[error("Invalid condition")] +pub struct Condition { + pub condition_inner: ConditionInner, +} + +#[cfg_attr(feature = "serialization", derive(serde::Serialize))] +#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] +#[derive(Debug, PartialEq, Eq, Clone, Error)] +#[error("Invalid condition inner")] +pub enum ConditionInner { + Binary { + left: Word, + op: BinaryOp, + right: Word, + }, + Unary { + op: Option, + right: Word, + }, +} + +#[cfg_attr(feature = "serialization", derive(serde::Serialize))] +#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] +#[derive(Debug, PartialEq, Eq, Clone, Error)] +#[error("Invalid binary operator")] +pub enum BinaryOp { + Equal, + NotEqual, + LessThan, + LessThanOrEqual, + GreaterThan, + GreaterThanOrEqual, +} + +#[cfg_attr(feature = "serialization", derive(serde::Serialize))] +#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] +#[derive(Debug, PartialEq, Eq, Clone, Error)] +#[error("Invalid unary operator")] +pub enum UnaryOp { + FileExists, + BlockSpecial, + CharSpecial, + Directory, + RegularFile, + SetGroupId, + SymbolicLink, + StickyBit, + NamedPipe, + Readable, + SizeNonZero, + TerminalFd, + SetUserId, + Writable, + Executable, + OwnedByEffectiveGroupId, + ModifiedSinceLastRead, + OwnedByEffectiveUserId, + Socket, + NonEmptyString, + EmptyString, + VariableSet, + VariableNameReference, +} + #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] #[derive(Debug, PartialEq, Eq, Clone, Error)] @@ -691,7 +779,13 @@ fn parse_compound_command(pair: Pair) -> Result { Rule::case_clause => { Err(miette!("Unsupported compound command case_clause")) } - Rule::if_clause => Err(miette!("Unsupported compound command if_clause")), + Rule::if_clause => { + let if_clause = parse_if_clause(inner)?; + Ok(Command { + inner: CommandInner::If(if_clause), + redirect: None, + }) + } Rule::while_clause => { Err(miette!("Unsupported compound command while_clause")) } @@ -718,6 +812,223 @@ fn parse_subshell(pair: Pair) -> Result { } } +fn parse_if_clause(pair: Pair) -> Result { + let mut inner = pair.into_inner(); + let condition = inner + .next() + .ok_or_else(|| miette!("Expected condition after If"))?; + let condition = parse_conditional_expression(condition)?; + + let then_body_pair = inner + .next() + .ok_or_else(|| miette!("Expected then body after If"))?; + let then_body = parse_complete_command(then_body_pair)?; + + let else_part = match inner.next() { + Some(else_pair) => Some(parse_else_part(else_pair)?), + None => None, + }; + + Ok(IfClause { + condition, + then_body, + else_part, + }) +} + +fn parse_else_part(pair: Pair) -> Result { + let mut inner = pair.into_inner(); + + let keyword = inner + .next() + .ok_or_else(|| miette!("Expected ELSE or ELIF keyword"))?; + + match keyword.as_rule() { + Rule::Elif => { + let condition = inner + .next() + .ok_or_else(|| miette!("Expected condition after Elif"))?; + let condition = parse_conditional_expression(condition)?; + + let then_body_pair = inner + .next() + .ok_or_else(|| miette!("Expected then body after Elif"))?; + let then_body = parse_complete_command(then_body_pair)?; + + let else_part = match inner.next() { + Some(else_pair) => Some(parse_else_part(else_pair)?), + None => None, + }; + + Ok(ElsePart::Elif(Box::new(IfClause { + condition, + then_body, + else_part, + }))) + } + Rule::Else => { + let body_pair = inner + .next() + .ok_or_else(|| miette!("Expected body after Else"))?; + let body = parse_complete_command(body_pair)?; + Ok(ElsePart::Else(body)) + } + _ => Err(miette!( + "Unexpected rule in else_part: {:?}", + keyword.as_rule() + )), + } +} + +fn parse_conditional_expression(pair: Pair) -> Result { + let inner = pair + .into_inner() + .next() + .ok_or_else(|| miette!("Expected conditional expression content"))?; + + match inner.as_rule() { + Rule::unary_conditional_expression => { + parse_unary_conditional_expression(inner) + } + Rule::binary_conditional_expression => { + parse_binary_conditional_expression(inner) + } + _ => Err(miette!( + "Unexpected rule in conditional expression: {:?}", + inner.as_rule() + )), + } +} + +fn parse_unary_conditional_expression(pair: Pair) -> Result { + let mut inner = pair.into_inner(); + let operator = inner.next().ok_or_else(|| miette!("Expected operator"))?; + let operand = inner.next().ok_or_else(|| miette!("Expected operand"))?; + + let op = match operator.as_rule() { + Rule::string_conditional_op => match operator.as_str() { + "-n" => UnaryOp::NonEmptyString, + "-z" => UnaryOp::EmptyString, + _ => { + return Err(miette!( + "Unexpected string conditional operator: {}", + operator.as_str() + )) + } + }, + Rule::file_conditional_op => match operator.as_str() { + "-a" => UnaryOp::FileExists, + "-b" => UnaryOp::BlockSpecial, + "-c" => UnaryOp::CharSpecial, + "-d" => UnaryOp::Directory, + "-f" => UnaryOp::RegularFile, + "-g" => UnaryOp::SetGroupId, + "-h" => UnaryOp::SymbolicLink, + "-k" => UnaryOp::StickyBit, + "-p" => UnaryOp::NamedPipe, + "-r" => UnaryOp::Readable, + "-s" => UnaryOp::SizeNonZero, + "-u" => UnaryOp::SetUserId, + "-w" => UnaryOp::Writable, + "-x" => UnaryOp::Executable, + "-G" => UnaryOp::OwnedByEffectiveGroupId, + "-L" => UnaryOp::SymbolicLink, + "-N" => UnaryOp::ModifiedSinceLastRead, + "-O" => UnaryOp::OwnedByEffectiveUserId, + "-S" => UnaryOp::Socket, + _ => { + return Err(miette!( + "Unexpected file conditional operator: {}", + operator.as_str() + )) + } + }, + Rule::variable_conditional_op => match operator.as_str() { + "-v" => UnaryOp::VariableSet, + "-R" => UnaryOp::VariableNameReference, + _ => { + return Err(miette!( + "Unexpected variable conditional operator: {}", + operator.as_str() + )) + } + }, + _ => { + return Err(miette!( + "Unexpected unary conditional operator rule: {:?}", + operator.as_rule() + )) + } + }; + + let right = parse_word(operand)?; + + Ok(Condition { + condition_inner: ConditionInner::Unary { + op: Some(op), + right, + }, + }) +} + +fn parse_binary_conditional_expression(pair: Pair) -> Result { + let mut inner = pair.into_inner(); + let left = inner + .next() + .ok_or_else(|| miette!("Expected left operand"))?; + let operator = inner.next().ok_or_else(|| miette!("Expected operator"))?; + let right = inner + .next() + .ok_or_else(|| miette!("Expected right operand"))?; + + let left_word = parse_word(left)?; + let right_word = parse_word(right)?; + + let op = match operator.as_rule() { + Rule::binary_string_conditional_op => match operator.as_str() { + "==" => BinaryOp::Equal, + "=" => BinaryOp::Equal, + "!=" => BinaryOp::NotEqual, + "<" => BinaryOp::LessThan, + ">" => BinaryOp::GreaterThan, + _ => { + return Err(miette!( + "Unexpected string conditional operator: {}", + operator.as_str() + )) + } + }, + Rule::binary_arithmetic_conditional_op => match operator.as_str() { + "-eq" => BinaryOp::Equal, + "-ne" => BinaryOp::NotEqual, + "-lt" => BinaryOp::LessThan, + "-le" => BinaryOp::LessThanOrEqual, + "-gt" => BinaryOp::GreaterThan, + "-ge" => BinaryOp::GreaterThanOrEqual, + _ => { + return Err(miette!( + "Unexpected arithmetic conditional operator: {}", + operator.as_str() + )) + } + }, + _ => { + return Err(miette!( + "Unexpected operator rule: {:?}", + operator.as_rule() + )) + } + }; + + Ok(Condition { + condition_inner: ConditionInner::Binary { + left: left_word, + op, + right: right_word, + }, + }) +} + fn parse_word(pair: Pair) -> Result { let mut parts = Vec::new(); diff --git a/crates/deno_task_shell/src/shell/command.rs b/crates/deno_task_shell/src/shell/command.rs index c5c0577..462852d 100644 --- a/crates/deno_task_shell/src/shell/command.rs +++ b/crates/deno_task_shell/src/shell/command.rs @@ -193,6 +193,7 @@ async fn parse_shebang_args( let cmd = match cmd.inner { crate::parser::CommandInner::Simple(cmd) => cmd, crate::parser::CommandInner::Subshell(_) => return err_unsupported(text), + crate::parser::CommandInner::If(_) => return err_unsupported(text), }; if !cmd.env_vars.is_empty() { return err_unsupported(text); diff --git a/crates/deno_task_shell/src/shell/execute.rs b/crates/deno_task_shell/src/shell/execute.rs index df4327f..13de4a4 100644 --- a/crates/deno_task_shell/src/shell/execute.rs +++ b/crates/deno_task_shell/src/shell/execute.rs @@ -12,9 +12,14 @@ use thiserror::Error; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; +use crate::parser::BinaryOp; +use crate::parser::Condition; +use crate::parser::ConditionInner; +use crate::parser::ElsePart; use crate::parser::IoFile; use crate::parser::RedirectOpInput; use crate::parser::RedirectOpOutput; +use crate::parser::UnaryOp; use crate::shell::commands::ShellCommand; use crate::shell::commands::ShellCommandContext; use crate::shell::types::pipe; @@ -27,6 +32,7 @@ use crate::shell::types::ShellState; use crate::parser::Command; use crate::parser::CommandInner; +use crate::parser::IfClause; use crate::parser::PipeSequence; use crate::parser::PipeSequenceOperator; use crate::parser::Pipeline; @@ -39,6 +45,8 @@ use crate::parser::SequentialList; use crate::parser::SimpleCommand; use crate::parser::Word; use crate::parser::WordPart; +// use crate::parser::ElsePart; +// use crate::parser::ElifClause; use super::command::execute_unresolved_command_name; use super::command::UnresolvedCommandName; @@ -517,6 +525,9 @@ async fn execute_command( CommandInner::Subshell(list) => { execute_subshell(list, state, stdin, stdout, stderr).await } + CommandInner::If(if_clause) => { + execute_if_clause(if_clause, state, stdin, stdout, stderr).await + } } } @@ -603,6 +614,134 @@ async fn execute_subshell( } } +async fn execute_if_clause( + if_clause: IfClause, + state: ShellState, + stdin: ShellPipeReader, + stdout: ShellPipeWriter, + mut stderr: ShellPipeWriter, +) -> ExecuteResult { + let mut current_condition = if_clause.condition; + let mut current_body = if_clause.then_body; + let mut current_else = if_clause.else_part; + + loop { + let condition_result = evaluate_condition( + current_condition, + &state, + stdin.clone(), + stderr.clone(), + ) + .await; + match condition_result { + Ok(true) => { + return execute_sequential_list( + current_body, + state, + stdin, + stdout, + stderr, + AsyncCommandBehavior::Yield, + ) + .await; + } + Ok(false) => match current_else { + Some(ElsePart::Elif(elif_clause)) => { + current_condition = elif_clause.condition; + current_body = elif_clause.then_body; + current_else = elif_clause.else_part; + } + Some(ElsePart::Else(else_body)) => { + return execute_sequential_list( + else_body, + state, + stdin, + stdout, + stderr, + AsyncCommandBehavior::Yield, + ) + .await; + } + None => { + return ExecuteResult::Continue(0, Vec::new(), Vec::new()); + } + }, + Err(err) => { + return err.into_exit_code(&mut stderr); + } + } + } +} + +async fn evaluate_condition( + condition: Condition, + state: &ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, +) -> Result { + match condition.condition_inner { + ConditionInner::Binary { left, op, right } => { + let left = + evaluate_word(left, state, stdin.clone(), stderr.clone()).await?; + let right = + evaluate_word(right, state, stdin.clone(), stderr.clone()).await?; + + // transform the string comparison to a numeric comparison if possible + if let Ok(left) = left.parse::() { + if let Ok(right) = right.parse::() { + return Ok(match op { + BinaryOp::Equal => left == right, + BinaryOp::NotEqual => left != right, + BinaryOp::LessThan => left < right, + BinaryOp::LessThanOrEqual => left <= right, + BinaryOp::GreaterThan => left > right, + BinaryOp::GreaterThanOrEqual => left >= right, + }); + } + } + + match op { + BinaryOp::Equal => Ok(left == right), + BinaryOp::NotEqual => Ok(left != right), + BinaryOp::LessThan => Ok(left < right), + BinaryOp::LessThanOrEqual => Ok(left <= right), + BinaryOp::GreaterThan => Ok(left > right), + BinaryOp::GreaterThanOrEqual => Ok(left >= right), + } + } + ConditionInner::Unary { op, right } => { + let _right = + evaluate_word(right, state, stdin.clone(), stderr.clone()).await?; + match op { + Some(UnaryOp::FileExists) => todo!(), + Some(UnaryOp::BlockSpecial) => todo!(), + Some(UnaryOp::CharSpecial) => todo!(), + Some(UnaryOp::Directory) => todo!(), + Some(UnaryOp::RegularFile) => todo!(), + Some(UnaryOp::SetGroupId) => todo!(), + Some(UnaryOp::SymbolicLink) => todo!(), + Some(UnaryOp::StickyBit) => todo!(), + Some(UnaryOp::NamedPipe) => todo!(), + Some(UnaryOp::Readable) => todo!(), + Some(UnaryOp::SizeNonZero) => todo!(), + Some(UnaryOp::TerminalFd) => todo!(), + Some(UnaryOp::SetUserId) => todo!(), + Some(UnaryOp::Writable) => todo!(), + Some(UnaryOp::Executable) => todo!(), + Some(UnaryOp::OwnedByEffectiveGroupId) => todo!(), + Some(UnaryOp::ModifiedSinceLastRead) => todo!(), + Some(UnaryOp::OwnedByEffectiveUserId) => todo!(), + Some(UnaryOp::Socket) => todo!(), + Some(UnaryOp::NonEmptyString) => todo!(), + Some(UnaryOp::EmptyString) => todo!(), + Some(UnaryOp::VariableSet) => todo!(), + Some(UnaryOp::VariableNameReference) => todo!(), + None => todo!(), + } + } + } +} + async fn execute_simple_command( command: SimpleCommand, state: ShellState, diff --git a/scripts/if_else.sh b/scripts/if_else.sh index eaa9fa9..f3fd04f 100644 --- a/scripts/if_else.sh +++ b/scripts/if_else.sh @@ -1,5 +1,8 @@ -if [[ $FOO == "bar" ]] then - echo "FOO is bar" +FOO=2 +if [[ $FOO -eq 1 ]] then + echo "FOO is 1" +elif [[ $FOO -eq 2 ]] then + echo "FOO is 2" else - echo "FOO is not bar" + echo "FOO is not 1 or 2" fi \ No newline at end of file