From 661f6a398132346405e3fcd63be0997874e3edd6 Mon Sep 17 00:00:00 2001
From: Parsa Bahraminejad
Date: Sat, 14 Sep 2024 00:35:06 -0400
Subject: [PATCH] feat: added basic support for if statements (#121)
---
crates/deno_task_shell/src/grammar.pest | 58 +++-
crates/deno_task_shell/src/parser.rs | 313 +++++++++++++++++++-
crates/deno_task_shell/src/shell/command.rs | 1 +
crates/deno_task_shell/src/shell/execute.rs | 139 +++++++++
scripts/if_else.sh | 9 +-
5 files changed, 506 insertions(+), 14 deletions(-)
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