diff --git a/crates/deno_task_shell/src/grammar.pest b/crates/deno_task_shell/src/grammar.pest index 8a98850..59efe11 100644 --- a/crates/deno_task_shell/src/grammar.pest +++ b/crates/deno_task_shell/src/grammar.pest @@ -11,6 +11,7 @@ UNQUOTED_PENDING_WORD = ${ (TILDE_PREFIX ~ (!(OPERATOR | WHITESPACE | NEWLINE) ~ ( EXIT_STATUS | UNQUOTED_ESCAPE_CHAR | + ARITHMETIC_EXPRESSION | SUB_COMMAND | ("$" ~ "{" ~ VARIABLE ~ "}" | "$" ~ VARIABLE) | UNQUOTED_CHAR | @@ -20,6 +21,7 @@ UNQUOTED_PENDING_WORD = ${ (!(OPERATOR | WHITESPACE | NEWLINE) ~ ( EXIT_STATUS | UNQUOTED_ESCAPE_CHAR | + ARITHMETIC_EXPRESSION | SUB_COMMAND | ("$" ~ "{" ~ VARIABLE ~ "}" | "$" ~ VARIABLE) | UNQUOTED_CHAR | @@ -148,7 +150,7 @@ command = !{ compound_command = { brace_group | - subshell | + // subshell | for_clause | case_clause | if_clause | @@ -156,7 +158,16 @@ compound_command = { until_clause } -subshell = !{ "(" ~ compound_list ~ ")" } +ARITHMETIC_EXPRESSION = { "$(" ~ "(" ~ arithmetic_expr ~ ")" ~ ")" } + +arithmetic_expr = { arithmetic_term ~ (add_op ~ arithmetic_term)* } +arithmetic_term = { arithmetic_factor ~ (mul_op ~ arithmetic_factor)* } +arithmetic_factor = { number | "(" ~ arithmetic_expr ~ ")" } +number = @{ ASCII_DIGIT+ } +add_op = { "+" | "-" } +mul_op = { "*" | "/" } + +// subshell = !{ "(" ~ compound_list ~ ")" } compound_list = !{ (newline_list? ~ term ~ separator?)+ } term = !{ and_or ~ (separator ~ and_or)* } diff --git a/crates/deno_task_shell/src/parser.rs b/crates/deno_task_shell/src/parser.rs index 16f3c31..7d542a9 100644 --- a/crates/deno_task_shell/src/parser.rs +++ b/crates/deno_task_shell/src/parser.rs @@ -111,7 +111,7 @@ pub struct BooleanList { #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Error)] +#[derive(Debug, Clone, PartialEq, Eq, Error)] pub enum PipeSequenceOperator { #[error("Stdout pipe operator")] Stdout, @@ -374,6 +374,8 @@ pub enum WordPart { Variable(String), #[error("Invalid command")] Command(SequentialList), + #[error("Invalid arithmetic expression")] + Arithmetic(ArithmeticExpr), #[error("Invalid quoted string")] Quoted(Vec), #[error("Invalid tilde prefix")] @@ -774,7 +776,7 @@ fn parse_compound_command(pair: Pair) -> Result { Rule::brace_group => { Err(miette!("Unsupported compound command brace_group")) } - Rule::subshell => parse_subshell(inner), + // Rule::subshell => parse_subshell(inner), Rule::for_clause => Err(miette!("Unsupported compound command for_clause")), Rule::case_clause => { Err(miette!("Unsupported compound command case_clause")) @@ -1085,6 +1087,11 @@ fn parse_word(pair: Pair) -> Result { let tilde_prefix = parse_tilde_prefix(part)?; parts.push(tilde_prefix); } + Rule::ARITHMETIC_EXPRESSION => { + let expr = part.into_inner().next().unwrap(); + let result = parse_arithmetic_expression(expr).unwrap(); + parts.push(WordPart::Arithmetic(result)); + } _ => { return Err(miette!( "Unexpected rule in UNQUOTED_PENDING_WORD: {:?}", @@ -1173,6 +1180,7 @@ fn parse_quoted_word(pair: Pair) -> Result { match part.as_rule() { Rule::EXIT_STATUS => parts.push(WordPart::Text("$?".to_string())), Rule::QUOTED_ESCAPE_CHAR => { + println!("QUOTED_ESCAPE_CHAR: {:?}", part.as_str()); if let Some(WordPart::Text(ref mut s)) = parts.last_mut() { s.push_str(part.as_str()); } else { @@ -1353,6 +1361,68 @@ fn parse_io_file(pair: Pair) -> Result<(RedirectOp, IoFile)> { Ok((redirect_op, io_file)) } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ArithmeticExpr { + Number(i64), + Variable(String), + Add(Box, Box), + Subtract(Box, Box), + Multiply(Box, Box), + Divide(Box, Box), +} + +fn parse_arithmetic_expression(expr: Pair) -> Result { + println!("parse_arithmetic_expression: {:?}", expr.as_rule()); + match expr.as_rule() { + Rule::arithmetic_expr => parse_add_sub(expr.into_inner()), + // Rule::arithmetic_term => parse_mul_div(expr.into_inner()), + // Rule::arithmetic_factor => parse_factor(expr.into_inner()), + _ => Err(format!("Unexpected rule: {:?}", expr.as_rule())), + } +} + +fn parse_add_sub(mut pairs: pest::iterators::Pairs) -> Result { + let mut expr = parse_mul_div(pairs.next().unwrap())?; + while let Some(op) = pairs.next() { + let rhs = parse_mul_div(pairs.next().unwrap())?; + expr = match op.as_str() { + "+" => ArithmeticExpr::Add(Box::new(expr), Box::new(rhs)), + "-" => ArithmeticExpr::Subtract(Box::new(expr), Box::new(rhs)), + _ => return Err(format!("Invalid operator: {}", op.as_str())), + }; + } + Ok(expr) +} + +fn parse_mul_div(pair: Pair) -> Result { + match pair.as_rule() { + Rule::arithmetic_term => { + let mut pairs = pair.into_inner(); + let mut expr = parse_factor(pairs.next().unwrap())?; + while let Some(op) = pairs.next() { + let rhs = parse_factor(pairs.next().unwrap())?; + expr = match op.as_str() { + "*" => ArithmeticExpr::Multiply(Box::new(expr), Box::new(rhs)), + "/" => ArithmeticExpr::Divide(Box::new(expr), Box::new(rhs)), + _ => return Err(format!("Invalid operator: {}", op.as_str())), + }; + } + Ok(expr) + } + _ => Err(format!("Unexpected rule: {:?}", pair.as_rule())), + } +} + +fn parse_factor(pair: Pair) -> Result { + println!("parse_factor: {:?}", pair.as_rule()); + match pair.as_rule() { + Rule::number => pair.as_str().parse::().map(ArithmeticExpr::Number).map_err(|e| e.to_string()), + Rule::arithmetic_expr => parse_add_sub(pair.into_inner()), + Rule::arithmetic_factor => parse_factor(pair.into_inner()), + _ => Err(format!("Unexpected rule: {:?}", pair.as_rule())), + } +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/deno_task_shell/src/shell/execute.rs b/crates/deno_task_shell/src/shell/execute.rs index 13de4a4..6e175a7 100644 --- a/crates/deno_task_shell/src/shell/execute.rs +++ b/crates/deno_task_shell/src/shell/execute.rs @@ -12,6 +12,7 @@ use thiserror::Error; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; +use crate::parser::ArithmeticExpr; use crate::parser::BinaryOp; use crate::parser::Condition; use crate::parser::ConditionInner; @@ -882,6 +883,8 @@ pub enum EvaluateWordTextError { NoFilesMatched { pattern: String }, #[error("Failed to get home directory")] FailedToGetHomeDirectory(anyhow::Error), + #[error("Arithmetic error: {0}")] + ArithmeticError(String), } impl EvaluateWordTextError { @@ -1041,6 +1044,11 @@ fn evaluate_word_parts( ) .await, ), + WordPart::Arithmetic(expr) => { + let text = evaluate_arithmetic_expression(expr, &state).map_err(|e| EvaluateWordTextError::ArithmeticError(e.to_string()))?; + current_text.push(TextPart::Text(text.to_string())); + continue; + } WordPart::Quoted(parts) => { let text = evaluate_word_parts_inner( parts, @@ -1125,6 +1133,42 @@ fn evaluate_word_parts( evaluate_word_parts_inner(parts, false, state, stdin, stderr) } +fn evaluate_arithmetic_expression(expr: ArithmeticExpr, state: &ShellState) -> Result { + match expr { + ArithmeticExpr::Number(n) => Ok(n), + ArithmeticExpr::Variable(var) => { + state.get_var(&var) + .and_then(|v| v.parse::().ok()) + .ok_or_else(|| format!("Variable '{}' not found or not a valid integer", var)) + }, + ArithmeticExpr::Add(left, right) => { + let l = evaluate_arithmetic_expression(*left, state)?; + let r = evaluate_arithmetic_expression(*right, state)?; + Ok(l + r) + }, + ArithmeticExpr::Subtract(left, right) => { + let l = evaluate_arithmetic_expression(*left, state)?; + let r = evaluate_arithmetic_expression(*right, state)?; + Ok(l - r) + }, + ArithmeticExpr::Multiply(left, right) => { + let l = evaluate_arithmetic_expression(*left, state)?; + let r = evaluate_arithmetic_expression(*right, state)?; + Ok(l * r) + }, + ArithmeticExpr::Divide(left, right) => { + let l = evaluate_arithmetic_expression(*left, state)?; + let r = evaluate_arithmetic_expression(*right, state)?; + if r == 0 { + Err("Division by zero".to_string()) + } else { + Ok(l / r) + } + }, + } + +} + async fn evaluate_command_substitution( list: SequentialList, state: &ShellState,