diff --git a/crates/deno_task_shell/src/grammar.pest b/crates/deno_task_shell/src/grammar.pest index dc82856..523ae06 100644 --- a/crates/deno_task_shell/src/grammar.pest +++ b/crates/deno_task_shell/src/grammar.pest @@ -191,9 +191,9 @@ pipeline = !{ Bang? ~ pipe_sequence } pipe_sequence = !{ command ~ ((StdoutStderr | Stdout) ~ linebreak ~ pipe_sequence)? } command = !{ + function_definition | compound_command ~ redirect_list? | - simple_command | - function_definition + simple_command } compound_command = { @@ -378,10 +378,14 @@ binary_posix_conditional_op = !{ while_clause = !{ While ~ conditional_expression ~ do_group } until_clause = !{ Until ~ conditional_expression ~ do_group } -function_definition = !{ fname ~ "(" ~ ")" ~ linebreak ~ function_body } -function_body = !{ compound_command ~ redirect_list? } +function_definition = { + fname ~ "(" ~ ")" ~ linebreak ~ function_body | + "function" ~ fname ~ ("(" ~ ")")? ~ linebreak ~ function_body +} + +function_body = !{ Lbrace ~ compound_list ~ Rbrace } -fname = @{ RESERVED_WORD | NAME | ASSIGNMENT_WORD | UNQUOTED_PENDING_WORD } +fname = @{ NAME } name = @{ NAME } brace_group = !{ Lbrace ~ compound_list ~ Rbrace } diff --git a/crates/deno_task_shell/src/parser.rs b/crates/deno_task_shell/src/parser.rs index 6c41ab5..d4aa81c 100644 --- a/crates/deno_task_shell/src/parser.rs +++ b/crates/deno_task_shell/src/parser.rs @@ -149,6 +149,15 @@ pub struct Command { pub redirect: Option, } +#[cfg_attr(feature = "serialization", derive(serde::Serialize))] +#[cfg_attr(feature = "serialization", serde(rename_all = "camelCase"))] +#[derive(Debug, Clone, PartialEq, Eq, Error)] +#[error("Invalid function")] +pub struct Function { + pub name: String, + pub body: SequentialList, +} + #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( feature = "serialization", @@ -170,6 +179,8 @@ pub enum CommandInner { Case(CaseClause), #[error("Invalid arithmetic expression")] ArithmeticExpression(Arithmetic), + #[error("Invalid function definition")] + FunctionType(Function), } impl From for Sequence { @@ -910,13 +921,69 @@ fn parse_command(pair: Pair) -> Result { match inner.as_rule() { Rule::simple_command => parse_simple_command(inner), Rule::compound_command => parse_compound_command(inner), - Rule::function_definition => { - Err(miette!("Function definitions are not supported yet")) - } + Rule::function_definition => parse_function_definition(inner), _ => Err(miette!("Unexpected rule in command: {:?}", inner.as_rule())), } } +fn parse_function_definition(pair: Pair) -> Result { + let mut inner = pair.into_inner(); + + // Handle both styles: + // 1. name() { body } + // 2. function name { body } or function name() { body } + let (name, body_pair) = if inner.peek().unwrap().as_rule() == Rule::fname { + // Style 1: name() { body } + let name = inner.next().unwrap().as_str().to_string(); + // Skip the () part + if inner.peek().is_some() { + let next = inner.peek().unwrap(); + if next.as_str() == "(" || next.as_str() == ")" { + inner.next(); // skip ( + inner.next(); // skip ) + } + } + (name, inner.next().unwrap()) + } else { + // Style 2: function name [()] { body } + // Skip "function" keyword + inner.next(); + let name = inner.next().unwrap().as_str().to_string(); + // Skip optional () + if inner.peek().is_some() { + let next = inner.peek().unwrap(); + if next.as_str() == "(" || next.as_str() == ")" { + inner.next(); // skip ( + inner.next(); // skip ) + } + } + (name, inner.next().unwrap()) + }; + + // Parse the function body + let mut body_inner = body_pair.into_inner(); + // Skip Lbrace + if let Some(lbrace) = body_inner.next() { + if lbrace.as_str() != "{" { + return Err(miette!("Expected Lbrace to start function body")); + } + } + // Parse the actual compound_list + let compound_list = body_inner.next() + .ok_or_else(|| miette!("Expected compound list in function body"))?; + let mut body_items = Vec::new(); + + parse_compound_list(compound_list, &mut body_items)?; + + Ok(Command { + inner: CommandInner::FunctionType(Function { + name, + body: SequentialList { items: body_items }, + }), + redirect: None, + }) +} + fn parse_simple_command(pair: Pair) -> Result { let mut env_vars = Vec::new(); let mut args = Vec::new(); diff --git a/crates/deno_task_shell/src/shell/command.rs b/crates/deno_task_shell/src/shell/command.rs index 37f1513..bd0d882 100644 --- a/crates/deno_task_shell/src/shell/command.rs +++ b/crates/deno_task_shell/src/shell/command.rs @@ -205,6 +205,7 @@ async fn parse_shebang_args( CommandInner::While(_) => return err_unsupported(text), CommandInner::ArithmeticExpression(_) => return err_unsupported(text), CommandInner::Case(_) => return err_unsupported(text), + CommandInner::FunctionType(_) => 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 2115328..fd7e8fc 100644 --- a/crates/deno_task_shell/src/shell/execute.rs +++ b/crates/deno_task_shell/src/shell/execute.rs @@ -50,6 +50,7 @@ use crate::parser::IfClause; use crate::parser::PipeSequence; use crate::parser::PipeSequenceOperator; use crate::parser::Pipeline; +use crate::parser::Function; use crate::parser::PipelineInner; use crate::parser::Redirect; use crate::parser::RedirectFd; @@ -667,6 +668,10 @@ async fn execute_command( } } } + CommandInner::FunctionType(function) => { + state.add_function(function.name.clone(), function); + ExecuteResult::Continue(0, changes, Vec::new()) + } } } @@ -1521,6 +1526,46 @@ async fn execute_simple_command( } }; + if !args.is_empty() { + let command_name = &args[0]; + if let Some(body) = state.get_function(command_name).cloned() { + // Set $0 to function name and $1, $2, etc. to arguments + let mut function_changes = vec![ + EnvChange::SetShellVar("0".to_string(), + command_name.clone().to_string()) + ]; + for (i, arg) in args.iter().skip(1).enumerate() { + function_changes.push( + EnvChange::SetShellVar((i + 1).to_string(), arg.clone().to_string()) + ); + } + + state.apply_changes(&function_changes); + changes.extend(function_changes); + + let result = execute_sequential_list( + body.body.clone(), + state.clone(), + stdin, + stdout, + stderr, + AsyncCommandBehavior::Yield, + ) + .await; + + match result { + ExecuteResult::Exit(code, env_changes, handles) => { + changes.extend(env_changes); + return ExecuteResult::Exit(code, changes, handles); + } + ExecuteResult::Continue(code, env_changes, handles) => { + changes.extend(env_changes); + return ExecuteResult::Continue(code, changes, handles); + } + } + } + } + let mut state = state.clone(); for env_var in command.env_vars { let word_result = evaluate_word( diff --git a/crates/deno_task_shell/src/shell/types.rs b/crates/deno_task_shell/src/shell/types.rs index aa4eb95..553ab36 100644 --- a/crates/deno_task_shell/src/shell/types.rs +++ b/crates/deno_task_shell/src/shell/types.rs @@ -25,6 +25,7 @@ use crate::shell::fs_util; use super::commands::builtin_commands; use super::commands::ShellCommand; +use crate::parser::Function; #[derive(Clone)] pub struct ShellState { @@ -52,6 +53,7 @@ pub struct ShellState { last_command_exit_code: i32, // Exit code of the last command // The shell options to be modified using `set` command shell_options: HashMap, + pub functions: HashMap, } #[allow(clippy::print_stdout)] @@ -92,6 +94,7 @@ impl ShellState { map.insert(ShellOptions::ExitOnError, true); map }, + functions: HashMap::new(), }; // ensure the data is normalized for (name, value) in env_vars { @@ -353,6 +356,14 @@ impl ShellState { pub fn reset_cancellation_token(&mut self) { self.token = CancellationToken::default(); } + + pub fn add_function(&mut self, name: String, func: Function) { + self.functions.insert(name, func); + } + + pub fn get_function(&self, name: &str) -> Option<&Function> { + return self.functions.get(name); + } } #[derive(Debug, PartialEq, Eq, Clone, PartialOrd)]