diff --git a/crates/deno_task_shell/src/grammar.pest b/crates/deno_task_shell/src/grammar.pest index abc6947..e5ae2c7 100644 --- a/crates/deno_task_shell/src/grammar.pest +++ b/crates/deno_task_shell/src/grammar.pest @@ -15,7 +15,7 @@ UNQUOTED_PENDING_WORD = ${ UNQUOTED_ESCAPE_CHAR | "$" ~ ARITHMETIC_EXPRESSION | SUB_COMMAND | - ("$" ~ "{" ~ VARIABLE ~ "}" | "$" ~ VARIABLE) | + VARIABLE_EXPANSION | UNQUOTED_CHAR | QUOTED_WORD ))*) @@ -25,55 +25,98 @@ UNQUOTED_PENDING_WORD = ${ UNQUOTED_ESCAPE_CHAR | "$" ~ ARITHMETIC_EXPRESSION | SUB_COMMAND | - ("$" ~ "{" ~ VARIABLE ~ "}" | "$" ~ VARIABLE) | + VARIABLE_EXPANSION | UNQUOTED_CHAR | QUOTED_WORD ))+ } -TILDE_PREFIX = ${ - "~" ~ (!(OPERATOR | WHITESPACE | NEWLINE | "/") ~ ( - (!("\"" | "'" | "$" | "\\" | "/") ~ ANY) - ))* -} +QUOTED_PENDING_WORD = ${ ( + EXIT_STATUS | + QUOTED_ESCAPE_CHAR | + "$" ~ ARITHMETIC_EXPRESSION | + SUB_COMMAND | + VARIABLE_EXPANSION | + QUOTED_CHAR +)* } -ASSIGNMENT_TILDE_PREFIX = ${ - "~" ~ (!(OPERATOR | WHITESPACE | NEWLINE | "/" | ":") ~ - (!("\"" | "'" | "$" | "\\" | "/") ~ ANY) - )* -} +PARAMETER_PENDING_WORD = ${ + TILDE_PREFIX ~ ( !"}" ~ !":" ~ ( + EXIT_STATUS | + PARAMETER_ESCAPE_CHAR | + "$" ~ ARITHMETIC_EXPRESSION | + SUB_COMMAND | + VARIABLE_EXPANSION | + QUOTED_WORD | + QUOTED_CHAR + ))* | + ( !"}" ~ !":" ~ ( + EXIT_STATUS | + PARAMETER_ESCAPE_CHAR | + "$" ~ ARITHMETIC_EXPRESSION | + SUB_COMMAND | + VARIABLE_EXPANSION | + QUOTED_WORD | + QUOTED_CHAR + ))+ + } FILE_NAME_PENDING_WORD = ${ (TILDE_PREFIX ~ (!(WHITESPACE | OPERATOR | NEWLINE) ~ ( UNQUOTED_ESCAPE_CHAR | - ("$" ~ VARIABLE) | + VARIABLE_EXPANSION | UNQUOTED_CHAR | QUOTED_WORD ))*) | (!(WHITESPACE | OPERATOR | NEWLINE) ~ ( UNQUOTED_ESCAPE_CHAR | - ("$" ~ VARIABLE) | + VARIABLE_EXPANSION | UNQUOTED_CHAR | QUOTED_WORD ))+ } -QUOTED_PENDING_WORD = ${ ( - EXIT_STATUS | - QUOTED_ESCAPE_CHAR | - SUB_COMMAND | - ("$" ~ "{" ~ VARIABLE ~ "}" | "$" ~ VARIABLE) | - QUOTED_CHAR -)* } - -UNQUOTED_ESCAPE_CHAR = ${ ("\\" ~ "$" | "$" ~ !"(" ~ !"{" ~ !VARIABLE) | "\\" ~ (" " | "`" | "\"" | "(" | ")")* } -QUOTED_ESCAPE_CHAR = ${ "\\" ~ "$" | "$" ~ !"(" ~ !"{" ~ !VARIABLE | "\\" ~ ("`" | "\"" | "(" | ")" | "'")* } +UNQUOTED_ESCAPE_CHAR = ${ ("\\" ~ "$" | "$" ~ !"(" ~ !"{" ~ !VARIABLE) | "\\" ~ (" " | "`" | "\"" | "(" | ")") } +QUOTED_ESCAPE_CHAR = ${ "\\" ~ "$" | "$" ~ !"(" ~ !"{" ~ !VARIABLE | "\\" ~ ("`" | "\"" | "(" | ")" | "'") } +PARAMETER_ESCAPE_CHAR = ${ "\\" ~ "$" | "$" ~ !"(" ~ !"{" ~ !VARIABLE | "\\" ~ "}" } UNQUOTED_CHAR = ${ ("\\" ~ " ") | !("]]" | "[[" | "(" | ")" | "<" | ">" | "|" | "&" | ";" | "\"" | "'" | "$") ~ ANY } QUOTED_CHAR = ${ !"\"" ~ ANY } +VARIABLE_EXPANSION = ${ + "$" ~ ( + "{" ~ VARIABLE ~ VARIABLE_MODIFIER? ~ "}" | + VARIABLE + ) +} + VARIABLE = ${ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* } + +VARIABLE_MODIFIER = _{ + VAR_DEFAULT_VALUE | + VAR_ASSIGN_DEFAULT | + VAR_ALTERNATE_VALUE | + VAR_SUBSTRING +} + +VAR_DEFAULT_VALUE = !{ ":-" ~ PARAMETER_PENDING_WORD? } +VAR_ASSIGN_DEFAULT = !{ ":=" ~ PARAMETER_PENDING_WORD } +VAR_ALTERNATE_VALUE = !{ ":+" ~ PARAMETER_PENDING_WORD } +VAR_SUBSTRING = !{ ":" ~ PARAMETER_PENDING_WORD ~ (":" ~ PARAMETER_PENDING_WORD)? } + +TILDE_PREFIX = ${ + "~" ~ (!(OPERATOR | WHITESPACE | NEWLINE | "/") ~ ( + (!("\"" | "'" | "$" | "\\" | "/") ~ ANY) + ))* +} + +ASSIGNMENT_TILDE_PREFIX = ${ + "~" ~ (!(OPERATOR | WHITESPACE | NEWLINE | "/" | ":") ~ + (!("\"" | "'" | "$" | "\\" | "/") ~ ANY) + )* +} + SUB_COMMAND = { "$(" ~ complete_command ~ ")"} DOUBLE_QUOTED = @{ "\"" ~ QUOTED_PENDING_WORD ~ "\"" } diff --git a/crates/deno_task_shell/src/parser.rs b/crates/deno_task_shell/src/parser.rs index 2745ca0..8cf22cb 100644 --- a/crates/deno_task_shell/src/parser.rs +++ b/crates/deno_task_shell/src/parser.rs @@ -365,6 +365,24 @@ impl Word { } } +#[cfg_attr(feature = "serialization", derive(serde::Serialize))] +#[cfg_attr( + feature = "serialization", + serde(rename_all = "camelCase", tag = "kind", content = "value") +)] +#[derive(Debug, PartialEq, Eq, Clone, Error)] +#[error("Invalid variable modifier")] +pub enum VariableModifier { + #[error("Invalid substring")] + Substring { + begin: Word, + length: Option, + }, + DefaultValue(Word), + AssignDefault(Word), + AlternateValue(Word), +} + #[cfg_attr(feature = "serialization", derive(serde::Serialize))] #[cfg_attr( feature = "serialization", @@ -375,7 +393,7 @@ pub enum WordPart { #[error("Invalid text")] Text(String), #[error("Invalid variable")] - Variable(String), + Variable(String, Option>), #[error("Invalid command")] Command(SequentialList), #[error("Invalid quoted string")] @@ -1203,16 +1221,12 @@ fn parse_word(pair: Pair) -> Result { Rule::UNQUOTED_ESCAPE_CHAR => { let mut chars = part.as_str().chars(); let mut escaped_char = String::new(); - while let Some(c) = chars.next() { + if let Some(c) = chars.next() { match c { '\\' => { let next_char = chars.next().unwrap_or('\0'); escaped_char.push(next_char); } - '$' => { - escaped_char.push(c); - break; - } _ => { escaped_char.push(c); break; @@ -1230,8 +1244,9 @@ fn parse_word(pair: Pair) -> Result { parse_complete_command(part.into_inner().next().unwrap())?; parts.push(WordPart::Command(command)); } - Rule::VARIABLE => { - parts.push(WordPart::Variable(part.as_str().to_string())) + Rule::VARIABLE_EXPANSION => { + let variable_expansion = parse_variable_expansion(part)?; + parts.push(variable_expansion); } Rule::QUOTED_WORD => { let quoted = parse_quoted_word(part)?; @@ -1273,7 +1288,7 @@ fn parse_word(pair: Pair) -> Result { } } Rule::VARIABLE => { - parts.push(WordPart::Variable(part.as_str().to_string())) + parts.push(WordPart::Variable(part.as_str().to_string(), None)) } Rule::UNQUOTED_CHAR => { if let Some(WordPart::Text(ref mut text)) = parts.last_mut() { @@ -1303,6 +1318,62 @@ fn parse_word(pair: Pair) -> Result { } } } + Rule::PARAMETER_PENDING_WORD => { + for part in pair.into_inner() { + match part.as_rule() { + Rule::PARAMETER_ESCAPE_CHAR => { + let mut chars = part.as_str().chars(); + let mut escaped_char = String::new(); + if let Some(c) = chars.next() { + match c { + '\\' => { + let next_char = chars.next().unwrap_or('\0'); + escaped_char.push(next_char); + } + _ => { + escaped_char.push(c); + break; + } + } + } + if let Some(WordPart::Text(ref mut text)) = parts.last_mut() { + text.push_str(&escaped_char); + } else { + parts.push(WordPart::Text(escaped_char)); + } + } + Rule::VARIABLE_EXPANSION => { + let variable_expansion = parse_variable_expansion(part)?; + parts.push(variable_expansion); + } + Rule::QUOTED_WORD => { + let quoted = parse_quoted_word(part)?; + parts.push(quoted); + } + Rule::TILDE_PREFIX => { + let tilde_prefix = parse_tilde_prefix(part)?; + parts.push(tilde_prefix); + } + Rule::ARITHMETIC_EXPRESSION => { + let arithmetic_expression = parse_arithmetic_expression(part)?; + parts.push(WordPart::Arithmetic(arithmetic_expression)); + } + Rule::QUOTED_CHAR => { + if let Some(WordPart::Text(ref mut s)) = parts.last_mut() { + s.push_str(part.as_str()); + } else { + parts.push(WordPart::Text(part.as_str().to_string())); + } + } + _ => { + return Err(miette!( + "Unexpected rule in PARAMETER_PENDING_WORD: {:?}", + part.as_rule() + )); + } + } + } + } _ => { return Err(miette!("Unexpected rule in word: {:?}", pair.as_rule())); } @@ -1473,6 +1544,64 @@ fn parse_post_arithmetic_op(pair: Pair) -> Result { } } +fn parse_variable_expansion(part: Pair) -> Result { + let mut inner = part.into_inner(); + let variable = inner + .next() + .ok_or_else(|| miette!("Expected variable name"))?; + let variable_name = variable.as_str().to_string(); + + let modifier = inner.next(); + let parsed_modifier = if let Some(modifier) = modifier { + match modifier.as_rule() { + Rule::VAR_SUBSTRING => { + let mut numbers = modifier.into_inner(); + let begin: Word = if let Some(n) = numbers.next() { + parse_word(n)? + } else { + return Err(miette!("Expected a number for substring begin")); + }; + + let length = if let Some(len_word) = numbers.next() { + Some(parse_word(len_word)?) + } else { + None + }; + Some(Box::new(VariableModifier::Substring { begin, length })) + } + Rule::VAR_DEFAULT_VALUE => { + let value = if let Some(val) = modifier.into_inner().next() { + parse_word(val)? + } else { + Word::new_empty() + }; + Some(Box::new(VariableModifier::DefaultValue(value))) + } + Rule::VAR_ASSIGN_DEFAULT => { + let value = modifier.into_inner().next().unwrap(); + Some(Box::new(VariableModifier::AssignDefault(parse_word( + value, + )?))) + } + Rule::VAR_ALTERNATE_VALUE => { + let value = modifier.into_inner().next().unwrap(); + Some(Box::new(VariableModifier::AlternateValue(parse_word( + value, + )?))) + } + _ => { + return Err(miette!( + "Unexpected rule in variable expansion modifier: {:?}", + modifier.as_rule() + )); + } + } + } else { + None + }; + Ok(WordPart::Variable(variable_name, parsed_modifier)) +} + fn parse_tilde_prefix(pair: Pair) -> Result { let tilde_prefix_str = pair.as_str(); let user = if tilde_prefix_str.len() > 1 { @@ -1506,8 +1635,9 @@ fn parse_quoted_word(pair: Pair) -> Result { parse_complete_command(part.into_inner().next().unwrap())?; parts.push(WordPart::Command(command)); } - Rule::VARIABLE => { - parts.push(WordPart::Variable(part.as_str().to_string())) + Rule::VARIABLE_EXPANSION => { + let variable_expansion = parse_variable_expansion(part)?; + parts.push(variable_expansion); } Rule::QUOTED_CHAR => { if let Some(WordPart::Text(ref mut s)) = parts.last_mut() { @@ -1944,7 +2074,7 @@ mod test { env_vars: vec![], args: vec![ Word::new_word("echo"), - Word(vec![WordPart::Variable("MY_ENV".to_string())]), + Word(vec![WordPart::Variable("MY_ENV".to_string(), None)]), ], } .into(), diff --git a/crates/deno_task_shell/src/shell/command.rs b/crates/deno_task_shell/src/shell/command.rs index b2a319c..1e62a89 100644 --- a/crates/deno_task_shell/src/shell/command.rs +++ b/crates/deno_task_shell/src/shell/command.rs @@ -28,8 +28,9 @@ pub fn execute_unresolved_command_name( mut context: ShellCommandContext, ) -> FutureExecuteResult { async move { + let args = context.args.clone(); let command = - match resolve_command(&command_name, &context, &context.args).await { + match resolve_command(&command_name, &mut context, &args).await { Ok(command_path) => command_path, Err(ResolveCommandError::CommandPath(err)) => { let _ = context.stderr.write_line(&format!("{}", err)); @@ -108,7 +109,7 @@ impl FailedShebangError { async fn resolve_command<'a>( command_name: &UnresolvedCommandName, - context: &ShellCommandContext, + context: &mut ShellCommandContext, original_args: &'a Vec, ) -> Result, ResolveCommandError> { let command_path = match resolve_command_path( @@ -160,10 +161,10 @@ async fn resolve_command<'a>( async fn parse_shebang_args( text: &str, - context: &ShellCommandContext, + context: &mut ShellCommandContext, ) -> Result> { fn err_unsupported(text: &str) -> Result> { - miette::bail!("unsupported shebang. Please report this as a bug (https://github.com/denoland/deno).\n\nShebang: {}", text) + miette::bail!("unsupported shebang. Please report this as a bug (https://github.com/prefix.dev/shell).\n\nShebang: {}", text) } let mut args = crate::parser::parse(text)?; @@ -204,7 +205,7 @@ async fn parse_shebang_args( let result = super::execute::evaluate_args( cmd.args, - &context.state, + &mut context.state, context.stdin.clone(), context.stderr.clone(), ) diff --git a/crates/deno_task_shell/src/shell/execute.rs b/crates/deno_task_shell/src/shell/execute.rs index c17c39e..930a746 100644 --- a/crates/deno_task_shell/src/shell/execute.rs +++ b/crates/deno_task_shell/src/shell/execute.rs @@ -7,7 +7,9 @@ use std::rc::Rc; use futures::future; use futures::future::LocalBoxFuture; use futures::FutureExt; +use miette::miette; use miette::Error; +use miette::IntoDiagnostic; use thiserror::Error; use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; @@ -21,6 +23,7 @@ use crate::parser::IoFile; use crate::parser::RedirectOpInput; use crate::parser::RedirectOpOutput; use crate::parser::UnaryOp; +use crate::parser::VariableModifier; use crate::shell::commands::ShellCommand; use crate::shell::commands::ShellCommandContext; use crate::shell::types::pipe; @@ -52,10 +55,14 @@ use crate::parser::SimpleCommand; use crate::parser::UnaryArithmeticOp; use crate::parser::Word; use crate::parser::WordPart; -use crate::shell::types::WordEvalResult; +use crate::shell::types::Text; +use crate::shell::types::TextPart; +use crate::shell::types::WordPartsResult; +use crate::shell::types::WordResult; use super::command::execute_unresolved_command_name; use super::command::UnresolvedCommandName; +use super::types::ConditionalResult; use super::types::CANCELLATION_EXIT_CODE; /// Executes a `SequentialList` of commands in a deno_task_shell environment. @@ -246,8 +253,10 @@ fn execute_sequence( 0, vec![EnvChange::SetShellVar( var.name, - match evaluate_word(var.value, &state, stdin, stderr.clone()).await { - Ok(value) => value, + match evaluate_word(var.value, &mut state, stdin, stderr.clone()) + .await + { + Ok(value) => value.into(), Err(err) => { return err.into_exit_code(&mut stderr); } @@ -268,13 +277,13 @@ fn execute_sequence( let (exit_code, mut async_handles) = match first_result { ExecuteResult::Exit(_, _) => return first_result, ExecuteResult::Continue(exit_code, sub_changes, async_handles) => { - state.apply_env_var("?", &exit_code.to_string()); - state.apply_changes(&sub_changes); changes.extend(sub_changes); (exit_code, async_handles) } }; + state.apply_changes(&changes); + let next = if list.op.moves_next_for_exit_code(exit_code) { Some(list.next) } else { @@ -359,8 +368,8 @@ async fn execute_pipeline_inner( #[derive(Debug)] enum RedirectPipe { - Input(ShellPipeReader), - Output(ShellPipeWriter), + Input(ShellPipeReader, Option>), + Output(ShellPipeWriter, Option>), } async fn resolve_redirect_pipe( @@ -377,16 +386,16 @@ async fn resolve_redirect_pipe( IoFile::Fd(fd) => match &redirect.op { RedirectOp::Input(RedirectOpInput::Redirect) => { let _ = stderr.write_line( - "deno_task_shell: input redirecting file descriptors is not implemented", - ); + "shell: input redirecting file descriptors is not implemented", + ); Err(ExecuteResult::from_exit_code(1)) } RedirectOp::Output(_op) => match fd { - 1 => Ok(RedirectPipe::Output(stdout.clone())), - 2 => Ok(RedirectPipe::Output(stderr.clone())), + 1 => Ok(RedirectPipe::Output(stdout.clone(), None)), + 2 => Ok(RedirectPipe::Output(stderr.clone(), None)), _ => { let _ = stderr.write_line( - "deno_task_shell: output redirecting file descriptors beyond stdout and stderr is not implemented", + &format!("{:?}", miette!("shell: output redirecting file descriptors beyond stdout and stderr is not implemented")), ); Err(ExecuteResult::from_exit_code(1)) } @@ -422,7 +431,7 @@ async fn resolve_redirect_word_pipe( let words = evaluate_word_parts( word.into_parts(), - state, + &mut state.clone(), stdin.clone(), stderr.clone(), ) @@ -456,13 +465,19 @@ async fn resolve_redirect_word_pipe( let std_file_result = std::fs::OpenOptions::new().read(true).open(&output_path); handle_std_result(&output_path, std_file_result, stderr).map(|std_file| { - RedirectPipe::Input(ShellPipeReader::from_std(std_file)) + RedirectPipe::Input( + ShellPipeReader::from_std(std_file), + Some(words.changes), + ) }) } RedirectOp::Output(op) => { // cross platform suppress output if output_path == "/dev/null" { - return Ok(RedirectPipe::Output(ShellPipeWriter::null())); + return Ok(RedirectPipe::Output( + ShellPipeWriter::null(), + Some(words.changes), + )); } let output_path = state.cwd().join(output_path); let is_append = *op == RedirectOpOutput::Append; @@ -473,7 +488,10 @@ async fn resolve_redirect_word_pipe( .truncate(!is_append) .open(&output_path); handle_std_result(&output_path, std_file_result, stderr).map(|std_file| { - RedirectPipe::Output(ShellPipeWriter::from_std(std_file)) + RedirectPipe::Output( + ShellPipeWriter::from_std(std_file), + Some(words.changes), + ) }) } } @@ -481,12 +499,14 @@ async fn resolve_redirect_word_pipe( async fn execute_command( command: Command, - state: ShellState, + mut state: ShellState, stdin: ShellPipeReader, stdout: ShellPipeWriter, mut stderr: ShellPipeWriter, ) -> ExecuteResult { - let (stdin, stdout, mut stderr) = if let Some(redirect) = &command.redirect { + let (stdin, stdout, mut stderr, changes) = if let Some(redirect) = + &command.redirect + { let pipe = match resolve_redirect_pipe( redirect, &state, @@ -500,46 +520,66 @@ async fn execute_command( Err(value) => return value, }; match pipe { - RedirectPipe::Input(pipe) => match redirect.maybe_fd { + RedirectPipe::Input(pipe, changes) => match redirect.maybe_fd { Some(_) => { let _ = stderr.write_line( "input redirects with file descriptors are not supported", ); return ExecuteResult::from_exit_code(1); } - None => (pipe, stdout, stderr), + None => (pipe, stdout, stderr, changes), }, - RedirectPipe::Output(pipe) => match redirect.maybe_fd { - Some(RedirectFd::Fd(2)) => (stdin, stdout, pipe), - Some(RedirectFd::Fd(1)) | None => (stdin, pipe, stderr), + RedirectPipe::Output(pipe, changes) => match redirect.maybe_fd { + Some(RedirectFd::Fd(2)) => (stdin, stdout, pipe, changes), + Some(RedirectFd::Fd(1)) | None => (stdin, pipe, stderr, changes), Some(RedirectFd::Fd(_)) => { let _ = stderr.write_line( "only redirecting to stdout (1) and stderr (2) is supported", ); return ExecuteResult::from_exit_code(1); } - Some(RedirectFd::StdoutStderr) => (stdin, pipe.clone(), pipe), + Some(RedirectFd::StdoutStderr) => (stdin, pipe.clone(), pipe, changes), }, } } else { - (stdin, stdout, stderr) + (stdin, stdout, stderr, None) + }; + let mut changes = if let Some(changes) = changes { + state.apply_changes(&changes); + changes + } else { + Vec::new() }; match command.inner { CommandInner::Simple(command) => { - execute_simple_command(command, state, stdin, stdout, stderr).await + // This can change the state, so we need to pass it by mutable reference + execute_simple_command(command, &mut state, stdin, stdout, stderr).await } CommandInner::Subshell(list) => { - execute_subshell(list, state, stdin, stdout, stderr).await + // Here the state can be changed but we can not pass by reference + match execute_subshell(list, state, stdin, stdout, stderr).await { + ExecuteResult::Exit(code, handles) => { + ExecuteResult::Exit(code, handles) + } + ExecuteResult::Continue(code, _, handles) => { + ExecuteResult::Continue(code, changes, handles) + } + } } CommandInner::If(if_clause) => { - execute_if_clause(if_clause, state, stdin, stdout, stderr).await + // The state can be changed + execute_if_clause(if_clause, &mut state, stdin, stdout, stderr).await } CommandInner::ArithmeticExpression(arithmetic) => { - match execute_arithmetic_expression(arithmetic, state).await { - Ok(result) => ExecuteResult::Continue(0, result.changes, Vec::new()), + // The state can be changed + match execute_arithmetic_expression(arithmetic, &mut state).await { + Ok(result) => { + changes.extend(result.changes); + ExecuteResult::Continue(0, changes, Vec::new()) + } Err(e) => { let _ = stderr.write_line(&e.to_string()); - ExecuteResult::Continue(2, Vec::new(), Vec::new()) + ExecuteResult::Continue(2, changes, Vec::new()) } } } @@ -548,9 +588,9 @@ async fn execute_command( async fn execute_arithmetic_expression( arithmetic: Arithmetic, - mut state: ShellState, + state: &mut ShellState, ) -> Result { - evaluate_arithmetic(&arithmetic, &mut state).await + evaluate_arithmetic(&arithmetic, state).await } async fn evaluate_arithmetic( @@ -559,7 +599,9 @@ async fn evaluate_arithmetic( ) -> Result { let mut result = ArithmeticResult::new(ArithmeticValue::Integer(0)); for part in &arithmetic.parts { - result = Box::pin(evaluate_arithmetic_part(part, state)).await?; + let part_result = Box::pin(evaluate_arithmetic_part(part, state)).await?; + result.set_value(part_result.value); + result.with_changes(part_result.changes); } Ok(result) } @@ -574,7 +616,7 @@ async fn evaluate_arithmetic_part( } ArithmeticPart::VariableAssignment { name, op, value } => { let val = Box::pin(evaluate_arithmetic_part(value, state)).await?; - let applied_value = match op { + let mut applied_value = match op { AssignmentOp::Assign => val.clone(), _ => { let var = state @@ -599,14 +641,11 @@ async fn evaluate_arithmetic_part( } }; state.apply_env_var(name, &applied_value.to_string()); - Ok( - applied_value - .clone() - .with_changes(vec![EnvChange::SetShellVar( - name.clone(), - applied_value.to_string(), - )]), - ) + applied_value.with_changes(vec![EnvChange::SetShellVar( + name.clone(), + applied_value.to_string(), + )]); + Ok(applied_value) } ArithmeticPart::TripleConditionalExpr { condition, @@ -783,15 +822,24 @@ async fn execute_pipe_sequence( let mut results = futures::future::join_all(wait_tasks).await; output_handle.await.unwrap(); let last_result = results.pop().unwrap(); - let all_handles = results.into_iter().flat_map(|r| r.into_handles()); + + let (all_handles, changes): (Vec<_>, Vec<_>) = results + .into_iter() + .map(|r| (r.into_handles_and_changes())) + .unzip(); + let all_handles: Vec> = + all_handles.into_iter().flatten().collect(); + let mut changes: Vec = changes.into_iter().flatten().collect(); + match last_result { ExecuteResult::Exit(code, mut handles) => { handles.extend(all_handles); - ExecuteResult::Continue(code, Vec::new(), handles) + ExecuteResult::Continue(code, changes, handles) } - ExecuteResult::Continue(code, _, mut handles) => { + ExecuteResult::Continue(code, env_changes, mut handles) => { handles.extend(all_handles); - ExecuteResult::Continue(code, Vec::new(), handles) + changes.extend(env_changes); + ExecuteResult::Continue(code, changes, handles) } } } @@ -819,16 +867,16 @@ async fn execute_subshell( // sub shells do not cause an exit ExecuteResult::Continue(code, Vec::new(), handles) } - ExecuteResult::Continue(code, _env_changes, handles) => { + ExecuteResult::Continue(code, env_changes, handles) => { // env changes are not propagated - ExecuteResult::Continue(code, Vec::new(), handles) + ExecuteResult::Continue(code, env_changes, handles) } } } async fn execute_if_clause( if_clause: IfClause, - state: ShellState, + state: &mut ShellState, stdin: ShellPipeReader, stdout: ShellPipeWriter, mut stderr: ShellPipeWriter, @@ -836,48 +884,77 @@ async fn execute_if_clause( let mut current_condition = if_clause.condition; let mut current_body = if_clause.then_body; let mut current_else = if_clause.else_part; + let mut changes = Vec::new(); loop { let condition_result = evaluate_condition( current_condition, - &state, + state, stdin.clone(), stderr.clone(), ) .await; match condition_result { - Ok(true) => { - return execute_sequential_list( + Ok(ConditionalResult { + value: true, + changes: env_changes, + }) => { + changes.extend(env_changes); + let exec_result = execute_sequential_list( current_body, - state, + state.clone(), 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; + match exec_result { + ExecuteResult::Exit(code, handles) => { + return ExecuteResult::Exit(code, handles); + } + ExecuteResult::Continue(code, env_changes, handles) => { + changes.extend(env_changes); + return ExecuteResult::Continue(code, changes, handles); + } } - None => { - return ExecuteResult::Continue(0, Vec::new(), Vec::new()); + } + Ok(ConditionalResult { + value: false, + changes: env_changes, + }) => { + changes.extend(env_changes); + 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)) => { + let exec_result = execute_sequential_list( + else_body, + state.clone(), + stdin, + stdout, + stderr, + AsyncCommandBehavior::Yield, + ) + .await; + match exec_result { + ExecuteResult::Exit(code, handles) => { + return ExecuteResult::Exit(code, handles); + } + ExecuteResult::Continue(code, env_changes, handles) => { + changes.extend(env_changes); + return ExecuteResult::Continue(code, changes, handles); + } + } + } + None => { + return ExecuteResult::Continue(0, changes, Vec::new()); + } } - }, + } Err(err) => { return err.into_exit_code(&mut stderr); } @@ -887,39 +964,51 @@ async fn execute_if_clause( async fn evaluate_condition( condition: Condition, - state: &ShellState, + state: &mut ShellState, stdin: ShellPipeReader, stderr: ShellPipeWriter, -) -> Result { +) -> Result { + let mut changes = Vec::new(); match condition.condition_inner { ConditionInner::Binary { left, op, right } => { let left = evaluate_word(left, state, stdin.clone(), stderr.clone()).await?; + state.apply_changes(&left.changes); + changes.extend(left.clone().changes); + let right = evaluate_word(right, state, stdin.clone(), stderr.clone()).await?; + state.apply_changes(&right.changes); + changes.extend(right.clone().changes); // 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, - }); + if let Ok(left) = Into::::into(left.clone()).parse::() { + if let Ok(right) = Into::::into(right.clone()).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, + } + .into(), + ); } } - 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), - } + 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, + } + .into(), + ) } ConditionInner::Unary { op, right } => { let _right = @@ -956,14 +1045,14 @@ async fn evaluate_condition( async fn execute_simple_command( command: SimpleCommand, - state: ShellState, + state: &mut ShellState, stdin: ShellPipeReader, stdout: ShellPipeWriter, mut stderr: ShellPipeWriter, ) -> ExecuteResult { let args = - evaluate_args(command.args, &state, stdin.clone(), stderr.clone()).await; - let (args, changes) = match args { + evaluate_args(command.args, state, stdin.clone(), stderr.clone()).await; + let (args, mut changes) = match args { Ok(args) => (args.value, args.changes), Err(err) => { return err.into_exit_code(&mut stderr); @@ -971,23 +1060,24 @@ async fn execute_simple_command( }; let mut state = state.clone(); for env_var in command.env_vars { - let value = - evaluate_word(env_var.value, &state, stdin.clone(), stderr.clone()).await; - let value = match value { - Ok(value) => value, + let word_result = + evaluate_word(env_var.value, &mut state, stdin.clone(), stderr.clone()) + .await; + let word_result = match word_result { + Ok(word_result) => word_result, Err(err) => { return err.into_exit_code(&mut stderr); } }; - state.apply_env_var(&env_var.name, &value); + state.apply_env_var(&env_var.name, &word_result.value); + changes.extend(word_result.changes); } let result = execute_command_args(args, state, stdin, stdout, stderr).await; match result { ExecuteResult::Exit(code, handles) => ExecuteResult::Exit(code, handles), ExecuteResult::Continue(code, env_changes, handles) => { - let mut combined_changes = env_changes.clone(); - combined_changes.extend(changes); - ExecuteResult::Continue(code, combined_changes, handles) + changes.extend(env_changes); + ExecuteResult::Continue(code, changes, handles) } } } @@ -1060,11 +1150,11 @@ fn execute_command_args( pub async fn evaluate_args( args: Vec, - state: &ShellState, + state: &mut ShellState, stdin: ShellPipeReader, stderr: ShellPipeWriter, -) -> Result { - let mut result = WordEvalResult::new(Vec::new(), Vec::new()); +) -> Result { + let mut result = WordPartsResult::new(Vec::new(), Vec::new()); for arg in args { let parts = evaluate_word_parts( arg.into_parts(), @@ -1080,14 +1170,14 @@ pub async fn evaluate_args( async fn evaluate_word( word: Word, - state: &ShellState, + state: &mut ShellState, stdin: ShellPipeReader, stderr: ShellPipeWriter, -) -> Result { +) -> Result { Ok( evaluate_word_parts(word.into_parts(), state, stdin, stderr) .await? - .join(" "), + .into(), ) } @@ -1117,27 +1207,123 @@ impl From for EvaluateWordTextError { } } -fn evaluate_word_parts( - parts: Vec, - state: &ShellState, - stdin: ShellPipeReader, - stderr: ShellPipeWriter, -) -> LocalBoxFuture> { - #[derive(Debug)] - enum TextPart { - Quoted(String), - Text(String), - } +impl VariableModifier { + pub async fn apply( + &self, + name: &str, + state: &mut ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, + ) -> Result<(Text, Option>), miette::Report> { + match self { + VariableModifier::DefaultValue(default_value) => { + match state.get_var(name) { + Some(v) => Ok((v.clone().into(), None)), + None => { + let v = evaluate_word(default_value.clone(), state, stdin, stderr) + .await + .into_diagnostic()?; + Ok((v.value.into(), Some(v.changes))) + } + } + } + VariableModifier::AssignDefault(default_value) => { + match state.get_var(name) { + Some(v) => Ok((v.clone().into(), None)), + None => { + let v = evaluate_word(default_value.clone(), state, stdin, stderr) + .await + .into_diagnostic()?; + state.apply_env_var(name, &v.value); + let mut changes = v.changes; + changes + .push(EnvChange::SetShellVar(name.to_string(), v.value.clone())); + Ok((v.value.into(), Some(changes))) + } + } + } + VariableModifier::Substring { begin, length } => { + if let Some(val) = state.get_var(name) { + let chars: Vec = val.chars().collect(); + + let mut changes = Vec::new(); + + // TODO figure out a way to get rid of cloning stdin and stderr + let begin = + evaluate_word(begin.clone(), state, stdin.clone(), stderr.clone()) + .await + .into_diagnostic() + .and_then(|v| { + changes.extend(v.clone().changes); // TODO figure out a way to get rid of cloning here + v.to_integer().map_err(|e| { + miette::miette!("Failed to parse start index: {:?}", e) + }) + })?; - impl TextPart { - pub fn as_str(&self) -> &str { - match self { - TextPart::Quoted(text) => text, - TextPart::Text(text) => text, + let start = if begin < 0 { + chars + .len() + .saturating_sub(usize::try_from(-begin).into_diagnostic()?) + } else { + usize::try_from(begin).into_diagnostic()? + }; + let end = match length { + Some(len) => { + let len = evaluate_word(len.clone(), state, stdin, stderr) + .await + .into_diagnostic() + .and_then(|v| { + changes.extend(v.clone().changes); // TODO figure out a way to get rid of cloning here + v.to_integer().map_err(|e| { + miette::miette!("Failed to parse start index: {:?}", e) + }) + })?; + + if len < 0 { + chars + .len() + .saturating_sub(usize::try_from(-len).into_diagnostic()?) + } else { + let len = usize::try_from(len).into_diagnostic()?; + start.saturating_add(len).min(chars.len()) + } + } + None => chars.len(), + }; + if start > end { + Err(miette::miette!( + "Invalid substring range: {}..{}", + start, + end + )) + } else { + Ok((chars[start..end].iter().collect(), Some(changes))) + } + } else { + Err(miette::miette!("Undefined variable: {}", name)) + } + } + VariableModifier::AlternateValue(default_value) => { + let val = state.get_var(name); + if val.is_none() || val.unwrap().is_empty() { + Ok(("".to_string().into(), None)) + } else { + let v = evaluate_word(default_value.clone(), state, stdin, stderr) + .await + .into_diagnostic()?; + Ok((v.value.into(), Some(v.changes))) + } } } } +} +fn evaluate_word_parts( + parts: Vec, + state: &mut ShellState, + stdin: ShellPipeReader, + stderr: ShellPipeWriter, +) -> LocalBoxFuture> { fn text_parts_to_string(parts: Vec) -> String { let mut result = String::with_capacity(parts.iter().map(|p| p.as_str().len()).sum()); @@ -1151,7 +1337,7 @@ fn evaluate_word_parts( state: &ShellState, text_parts: Vec, is_quoted: bool, - ) -> Result { + ) -> Result { if !is_quoted && text_parts .iter() @@ -1221,13 +1407,13 @@ fn evaluate_word_parts( }) .collect::>() }; - Ok(WordEvalResult::new(paths, Vec::new())) + Ok(WordPartsResult::new(paths, Vec::new())) } } Err(err) => Err(EvaluateWordTextError::InvalidPattern { pattern, err }), } } else { - Ok(WordEvalResult { + Ok(WordPartsResult { value: vec![text_parts_to_string(text_parts)], changes: Vec::new(), }) @@ -1237,47 +1423,64 @@ fn evaluate_word_parts( fn evaluate_word_parts_inner( parts: Vec, is_quoted: bool, - state: &ShellState, + state: &mut ShellState, stdin: ShellPipeReader, stderr: ShellPipeWriter, - ) -> LocalBoxFuture> { + ) -> LocalBoxFuture> { // recursive async, so requires boxing - let mut changes: Vec = Vec::new(); - async move { - let mut result = WordEvalResult::new(Vec::new(), Vec::new()); + let mut result = WordPartsResult::new(Vec::new(), Vec::new()); let mut current_text = Vec::new(); for part in parts { - let evaluation_result_text = match part { + let evaluation_result_text: Result, Error> = match part { WordPart::Text(text) => { current_text.push(TextPart::Text(text)); - None + continue; } - WordPart::Variable(name) => { - state.get_var(&name).map(|v| v.to_string()) + WordPart::Variable(name, modifier) => { + if let Some(modifier) = modifier { + let (text, env_changes) = modifier + .apply(&name, state, stdin.clone(), stderr.clone()) + .await?; + if let Some(env_changes) = env_changes { + result.with_changes(env_changes); + } + Ok(Some(text)) + } else if let Some(val) = + state.get_var(&name).map(|v| v.to_string()) + { + Ok(Some(val.into())) + } else { + Err(miette::miette!("Undefined variable: {}", name)) + } } - WordPart::Command(list) => Some( - evaluate_command_substitution( + WordPart::Command(list) => { + let cmd = evaluate_command_substitution( list, // contain cancellation to the command substitution &state.with_child_token(), stdin.clone(), stderr.clone(), ) - .await, - ), + .await; + Ok(Some(cmd.into())) + } WordPart::Quoted(parts) => { - let text = evaluate_word_parts_inner( + let res = evaluate_word_parts_inner( parts, true, state, stdin.clone(), stderr.clone(), ) - .await? - .join(" "); - - current_text.push(TextPart::Quoted(text)); + .await?; + + let WordPartsResult { + value, + changes: env_changes, + } = res; + result.with_changes(env_changes); + current_text.push(TextPart::Quoted(value.join(" "))); continue; } WordPart::Tilde(tilde_prefix) => { @@ -1287,16 +1490,18 @@ fn evaluate_word_parts( .display() .to_string(); current_text.push(TextPart::Text(home_str)); + continue; } else { - todo!("tilde expansion with user name is not supported"); + Err(miette::miette!( + "Tilde expansion with username is not supported." + )) } - continue; } WordPart::Arithmetic(arithmetic) => { let arithmetic_result = - execute_arithmetic_expression(arithmetic, state.clone()).await?; + execute_arithmetic_expression(arithmetic, state).await?; current_text.push(TextPart::Text(arithmetic_result.to_string())); - changes.extend(arithmetic_result.changes); + result.with_changes(arithmetic_result.changes); continue; } WordPart::ExitStatus => { @@ -1306,16 +1511,8 @@ fn evaluate_word_parts( } }; - // This text needs to be turned into a vector of strings. - // For now we do a very basic string split on whitespace, but in the future - // we should continue to improve this functionality. - if let Some(text) = evaluation_result_text { - let mut parts = text - .split(' ') - .map(|p| p.trim()) - .filter(|p| !p.is_empty()) - .map(|p| TextPart::Text(p.to_string())) - .collect::>(); + if let Ok(Some(text)) = evaluation_result_text { + let mut parts = text.into_parts(); if !parts.is_empty() { // append the first part to the current text diff --git a/crates/deno_task_shell/src/shell/types.rs b/crates/deno_task_shell/src/shell/types.rs index 3c87caa..61ed704 100644 --- a/crates/deno_task_shell/src/shell/types.rs +++ b/crates/deno_task_shell/src/shell/types.rs @@ -1,6 +1,7 @@ // Copyright 2018-2024 the Deno authors. MIT license. use std::borrow::Cow; +use std::cmp::Ordering; use std::collections::HashMap; use std::fmt; use std::fmt::Display; @@ -115,15 +116,18 @@ impl ShellState { } pub fn get_var(&self, name: &str) -> Option<&String> { - let name = if cfg!(windows) { - Cow::Owned(name.to_uppercase()) + let (original_name, updated_name) = if cfg!(windows) { + ( + Cow::Owned(name.to_string()), + Cow::Owned(name.to_uppercase()), + ) } else { - Cow::Borrowed(name) + (Cow::Borrowed(name), Cow::Borrowed(name)) }; self .env_vars - .get(name.as_ref()) - .or_else(|| self.shell_vars.get(name.as_ref())) + .get(updated_name.as_ref()) + .or_else(|| self.shell_vars.get(original_name.as_ref())) } // Update self.git_branch using self.git_root @@ -212,7 +216,12 @@ impl ShellState { } EnvChange::UnsetVar(name) => { self.shell_vars.remove(name); - self.env_vars.remove(name); + if cfg!(windows) { + // environment variables are case insensitive on windows + self.env_vars.remove(&name.to_uppercase()); + } else { + self.env_vars.remove(name); + } } EnvChange::Cd(new_dir) => { self.set_cwd(new_dir); @@ -332,6 +341,22 @@ impl ExecuteResult { pub fn into_handles(self) -> Vec> { self.into_exit_code_and_handles().1 } + + pub fn into_changes(self) -> Vec { + match self { + ExecuteResult::Exit(_, _) => Vec::new(), + ExecuteResult::Continue(_, changes, _) => changes, + } + } + + pub fn into_handles_and_changes( + self, + ) -> (Vec>, Vec) { + match self { + ExecuteResult::Exit(_, handles) => (handles, Vec::new()), + ExecuteResult::Continue(_, changes, handles) => (handles, changes), + } + } } /// Reader side of a pipe. @@ -579,6 +604,14 @@ impl ArithmeticResult { } } + pub fn with_changes(&mut self, changes: Vec) { + self.changes.extend(changes); + } + + pub fn set_value(&mut self, value: ArithmeticValue) { + self.value = value; + } + pub fn checked_add( &self, other: &ArithmeticResult, @@ -595,7 +628,7 @@ impl ArithmeticResult { if sum.is_finite() { ArithmeticValue::Float(sum) } else { - return Err(miette::miette!("Float overflow: {} + {}", lhs, rhs)); + miette::bail!("Float overflow: {} + {}", lhs, rhs); } } (ArithmeticValue::Integer(lhs), ArithmeticValue::Float(rhs)) @@ -604,7 +637,7 @@ impl ArithmeticResult { if sum.is_finite() { ArithmeticValue::Float(sum) } else { - return Err(miette::miette!("Float overflow: {} + {}", lhs, rhs)); + miette::bail!("Float overflow: {} + {}", lhs, rhs); } } }; @@ -634,7 +667,7 @@ impl ArithmeticResult { if diff.is_finite() { ArithmeticValue::Float(diff) } else { - return Err(miette::miette!("Float overflow: {} - {}", lhs, rhs)); + miette::bail!("Float overflow: {} - {}", lhs, rhs); } } (ArithmeticValue::Integer(lhs), ArithmeticValue::Float(rhs)) => { @@ -642,7 +675,7 @@ impl ArithmeticResult { if diff.is_finite() { ArithmeticValue::Float(diff) } else { - return Err(miette::miette!("Float overflow: {} - {}", lhs, rhs)); + miette::bail!("Float overflow: {} - {}", lhs, rhs); } } (ArithmeticValue::Float(lhs), ArithmeticValue::Integer(rhs)) => { @@ -650,7 +683,7 @@ impl ArithmeticResult { if diff.is_finite() { ArithmeticValue::Float(diff) } else { - return Err(miette::miette!("Float overflow: {} - {}", lhs, rhs)); + miette::bail!("Float overflow: {} - {}", lhs, rhs); } } }; @@ -680,7 +713,7 @@ impl ArithmeticResult { if product.is_finite() { ArithmeticValue::Float(product) } else { - return Err(miette::miette!("Float overflow: {} * {}", lhs, rhs)); + miette::bail!("Float overflow: {} * {}", lhs, rhs); } } (ArithmeticValue::Integer(lhs), ArithmeticValue::Float(rhs)) @@ -689,7 +722,7 @@ impl ArithmeticResult { if product.is_finite() { ArithmeticValue::Float(product) } else { - return Err(miette::miette!("Float overflow: {} * {}", lhs, rhs)); + miette::bail!("Float overflow: {} * {}", lhs, rhs); } } }; @@ -710,7 +743,7 @@ impl ArithmeticResult { let result = match (&self.value, &other.value) { (ArithmeticValue::Integer(lhs), ArithmeticValue::Integer(rhs)) => { if *rhs == 0 { - return Err(miette::miette!("Division by zero: {} / {}", lhs, rhs)); + miette::bail!("Division by zero: {} / {}", lhs, rhs); } lhs .checked_div(*rhs) @@ -721,35 +754,35 @@ impl ArithmeticResult { } (ArithmeticValue::Float(lhs), ArithmeticValue::Float(rhs)) => { if *rhs == 0.0 { - return Err(miette::miette!("Division by zero: {} / {}", lhs, rhs)); + miette::bail!("Division by zero: {} / {}", lhs, rhs); } let quotient = lhs / rhs; if quotient.is_finite() { ArithmeticValue::Float(quotient) } else { - return Err(miette::miette!("Float overflow: {} / {}", lhs, rhs)); + miette::bail!("Float overflow: {} / {}", lhs, rhs); } } (ArithmeticValue::Integer(lhs), ArithmeticValue::Float(rhs)) => { if *rhs == 0.0 { - return Err(miette::miette!("Division by zero: {} / {}", lhs, rhs)); + miette::bail!("Division by zero: {} / {}", lhs, rhs); } let quotient = *lhs as f64 / rhs; if quotient.is_finite() { ArithmeticValue::Float(quotient) } else { - return Err(miette::miette!("Float overflow: {} / {}", lhs, rhs)); + miette::bail!("Float overflow: {} / {}", lhs, rhs); } } (ArithmeticValue::Float(lhs), ArithmeticValue::Integer(rhs)) => { if *rhs == 0 { - return Err(miette::miette!("Division by zero: {} / {}", lhs, rhs)); + miette::bail!("Division by zero: {} / {}", lhs, rhs); } let quotient = lhs / *rhs as f64; if quotient.is_finite() { ArithmeticValue::Float(quotient) } else { - return Err(miette::miette!("Float overflow: {} / {}", lhs, rhs)); + miette::bail!("Float overflow: {} / {}", lhs, rhs); } } }; @@ -770,7 +803,7 @@ impl ArithmeticResult { let result = match (&self.value, &other.value) { (ArithmeticValue::Integer(lhs), ArithmeticValue::Integer(rhs)) => { if *rhs == 0 { - return Err(miette::miette!("Modulo by zero: {} % {}", lhs, rhs)); + miette::bail!("Modulo by zero: {} % {}", lhs, rhs); } lhs .checked_rem(*rhs) @@ -781,35 +814,35 @@ impl ArithmeticResult { } (ArithmeticValue::Float(lhs), ArithmeticValue::Float(rhs)) => { if *rhs == 0.0 { - return Err(miette::miette!("Modulo by zero: {} % {}", lhs, rhs)); + miette::bail!("Modulo by zero: {} % {}", lhs, rhs); } let remainder = lhs % rhs; if remainder.is_finite() { ArithmeticValue::Float(remainder) } else { - return Err(miette::miette!("Float overflow: {} % {}", lhs, rhs)); + miette::bail!("Float overflow: {} % {}", lhs, rhs); } } (ArithmeticValue::Integer(lhs), ArithmeticValue::Float(rhs)) => { if *rhs == 0.0 { - return Err(miette::miette!("Modulo by zero: {} % {}", lhs, rhs)); + miette::bail!("Modulo by zero: {} % {}", lhs, rhs); } let remainder = *lhs as f64 % rhs; if remainder.is_finite() { ArithmeticValue::Float(remainder) } else { - return Err(miette::miette!("Float overflow: {} % {}", lhs, rhs)); + miette::bail!("Float overflow: {} % {}", lhs, rhs); } } (ArithmeticValue::Float(lhs), ArithmeticValue::Integer(rhs)) => { if *rhs == 0 { - return Err(miette::miette!("Modulo by zero: {} % {}", lhs, rhs)); + miette::bail!("Modulo by zero: {} % {}", lhs, rhs); } let remainder = lhs % *rhs as f64; if remainder.is_finite() { ArithmeticValue::Float(remainder) } else { - return Err(miette::miette!("Float overflow: {} % {}", lhs, rhs)); + miette::bail!("Float overflow: {} % {}", lhs, rhs); } } }; @@ -834,7 +867,7 @@ impl ArithmeticResult { if result.is_finite() { ArithmeticValue::Float(result) } else { - return Err(miette::miette!("Float overflow: {} ** {}", lhs, rhs)); + miette::bail!("Float overflow: {} ** {}", lhs, rhs); } } else { lhs @@ -850,7 +883,7 @@ impl ArithmeticResult { if result.is_finite() { ArithmeticValue::Float(result) } else { - return Err(miette::miette!("Float overflow: {} ** {}", lhs, rhs)); + miette::bail!("Float overflow: {} ** {}", lhs, rhs); } } (ArithmeticValue::Integer(lhs), ArithmeticValue::Float(rhs)) => { @@ -858,7 +891,7 @@ impl ArithmeticResult { if result.is_finite() { ArithmeticValue::Float(result) } else { - return Err(miette::miette!("Float overflow: {} ** {}", lhs, rhs)); + miette::bail!("Float overflow: {} ** {}", lhs, rhs); } } (ArithmeticValue::Float(lhs), ArithmeticValue::Integer(rhs)) => { @@ -866,7 +899,7 @@ impl ArithmeticResult { if result.is_finite() { ArithmeticValue::Float(result) } else { - return Err(miette::miette!("Float overflow: {} ** {}", lhs, rhs)); + miette::bail!("Float overflow: {} ** {}", lhs, rhs); } } }; @@ -891,7 +924,7 @@ impl ArithmeticResult { if result.is_finite() { ArithmeticValue::Float(result) } else { - return Err(miette::miette!("Float overflow: -{}", val)); + miette::bail!("Float overflow: -{}", val); } } }; @@ -1072,11 +1105,6 @@ impl ArithmeticResult { changes, }) } - - pub fn with_changes(mut self, changes: Vec) -> Self { - self.changes = changes; - self - } } impl From for ArithmeticResult { @@ -1099,17 +1127,18 @@ impl FromStr for ArithmeticResult { } } -pub struct WordEvalResult { +#[derive(Debug, Clone)] +pub struct WordPartsResult { pub value: Vec, pub changes: Vec, } -impl WordEvalResult { +impl WordPartsResult { pub fn new(value: Vec, changes: Vec) -> Self { - WordEvalResult { value, changes } + WordPartsResult { value, changes } } - pub fn extend(&mut self, other: WordEvalResult) { + pub fn extend(&mut self, other: WordPartsResult) { self.value.extend(other.value); self.changes.extend(other.changes); } @@ -1117,4 +1146,150 @@ impl WordEvalResult { pub fn join(&self, sep: &str) -> String { self.value.join(sep) } + + pub fn with_changes(&mut self, changes: Vec) { + self.changes.extend(changes); + } +} + +impl From for String { + fn from(parts: WordPartsResult) -> Self { + parts.join(" ") + } +} + +#[derive(Debug, Clone)] +pub struct WordResult { + pub value: String, + pub changes: Vec, +} + +impl WordResult { + pub fn new(value: String, changes: Vec) -> Self { + WordResult { value, changes } + } + + pub fn extend(&mut self, other: WordResult) { + self.value.push_str(&other.value); + self.changes.extend(other.changes); + } + + pub fn to_integer(&self) -> Result { + self + .value + .parse::() + .map_err(|_| miette::miette!("Invalid integer: {}", self.value)) + } +} + +impl PartialEq for WordResult { + fn eq(&self, other: &Self) -> bool { + self.value == other.value + } +} + +impl Ord for WordResult { + fn cmp(&self, other: &Self) -> Ordering { + self.value.cmp(&other.value) + } +} + +impl PartialOrd for WordResult { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Eq for WordResult {} + +impl From for WordResult { + fn from(value: String) -> Self { + WordResult::new(value, Vec::new()) + } +} + +impl From for String { + fn from(result: WordResult) -> Self { + result.value + } +} + +impl From for WordResult { + fn from(parts: WordPartsResult) -> Self { + WordResult::new(parts.join(" "), parts.changes) + } +} + +impl From for WordPartsResult { + fn from(word: WordResult) -> Self { + WordPartsResult::new(vec![word.value], word.changes) + } +} + +#[derive(Debug, Clone)] +pub struct ConditionalResult { + pub value: bool, + pub changes: Vec, +} + +impl ConditionalResult { + pub fn new(value: bool, changes: Vec) -> Self { + ConditionalResult { value, changes } + } +} + +impl From for ConditionalResult { + fn from(value: bool) -> Self { + ConditionalResult::new(value, Vec::new()) + } +} + +#[derive(Debug, Clone)] +pub enum TextPart { + Quoted(String), + Text(String), +} + +impl TextPart { + pub fn as_str(&self) -> &str { + match self { + TextPart::Quoted(text) => text, + TextPart::Text(text) => text, + } + } +} + +#[derive(Debug, Clone)] +pub struct Text { + parts: Vec, +} + +impl Text { + pub fn new(parts: Vec) -> Self { + Text { parts } + } + + pub fn into_parts(self) -> Vec { + self.parts + } +} + +impl From for Text { + fn from(parts: String) -> Self { + Text::new( + parts + .split(' ') + .map(|p| p.trim()) + .filter(|p| !p.is_empty()) + .map(|p| TextPart::Text(p.to_string())) + .collect::>(), + ) + } +} + +impl<'a> FromIterator<&'a char> for Text { + fn from_iter>(iter: I) -> Self { + let parts = iter.into_iter().collect::(); + Text::new(vec![TextPart::Text(parts)]) + } } diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 45ab92e..acae14c 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -2,6 +2,7 @@ #[cfg(test)] mod test_builder; + #[cfg(test)] use deno_task_shell::ExecuteResult; #[cfg(test)] @@ -107,6 +108,18 @@ async fn commands() { .await; TestBuilder::new().command("unset").run().await; + + TestBuilder::new() + .command("a=1 && echo $((a=2, a + 1)) && echo $a") + .assert_stdout("3\n2\n") + .run() + .await; + + TestBuilder::new() + .command("a=1 && echo $a") + .assert_stdout("1\n") + .run() + .await; } #[tokio::test] @@ -266,7 +279,7 @@ async fn redirects_input() { TestBuilder::new() .command(r#"cat - <&0"#) - .assert_stderr("deno_task_shell: input redirecting file descriptors is not implemented\n") + .assert_stderr("shell: input redirecting file descriptors is not implemented\n") .assert_exit_code(1) .run() .await; @@ -1058,6 +1071,241 @@ async fn touch() { .await; } +#[tokio::test] +async fn variable_expansion() { + // DEFAULT VALUE EXPANSION + TestBuilder::new() + .command("echo ${FOO:-5}") + .assert_stdout("5\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo "${FOO:-5}""#) + .assert_stdout("5\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=1 && echo ${FOO:-5}"#) + .assert_stdout("1\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=1 && echo "${FOO:-5}""#) + .assert_stdout("1\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo ${FOO:-${BAR:-5}}"#) + .assert_stdout("5\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo "${FOO:-${BAR:-5}}""#) + .assert_stdout("5\n") + .run() + .await; + + TestBuilder::new() + .command("BAR=2 && echo ${FOO:-${BAR:-5}}") + .assert_stdout("2\n") + .run() + .await; + + TestBuilder::new() + .command(r#"BAR=2 && echo "${FOO:-${BAR:-5}}""#) + .assert_stdout("2\n") + .run() + .await; + + TestBuilder::new() + .command("echo ${BAR:-THE VALUE CAN CONTAIN SPACES}") + .assert_stdout("THE VALUE CAN CONTAIN SPACES\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo "${BAR:-THE VALUE CAN CONTAIN SPACES}""#) + .assert_stdout("THE VALUE CAN CONTAIN SPACES\n") + .run() + .await; + + // ASSIGN DEFAULT EXPANSION + TestBuilder::new() + .command("echo ${FOO:=5} && echo $FOO") + .assert_stdout("5\n5\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo "${FOO:=5}" && echo "$FOO""#) + .assert_stdout("5\n5\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=1 && echo ${FOO:=5} && echo $FOO"#) + .assert_stdout("1\n1\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=1 && echo "${FOO:=5}" && echo "$FOO""#) + .assert_stdout("1\n1\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo ${FOO:=${BAR:=5}} && echo $FOO && echo $BAR"#) + .assert_stdout("5\n5\n5\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo "${FOO:=${BAR:=5}}" && echo "$FOO" && echo "$BAR""#) + .assert_stdout("5\n5\n5\n") + .run() + .await; + + // SUBSTRING VARIABLE EXPANSION + TestBuilder::new() + .command(r#"FOO=12345 && echo ${FOO:1:3}"#) + .assert_stdout("234\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=12345 && echo "${FOO:1:3}""#) + .assert_stdout("234\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=12345 && echo ${FOO:1}"#) + .assert_stdout("2345\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=12345 && echo "${FOO:1}""#) + .assert_stdout("2345\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=12345 && echo ${FOO:1:-1}"#) + .assert_stdout("234\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=12345 && echo "${FOO:1:-1}""#) + .assert_stdout("234\n") + .run() + .await; + + // ALTERNATE VALUE EXPANSION + TestBuilder::new() + .command(r#"FOO=1 && echo ${FOO:+5}"#) + .assert_stdout("5\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=1 && echo "${FOO:+5}""#) + .assert_stdout("5\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo ${FOO:+5}"#) + .assert_stdout("\n") + .run() + .await; + + TestBuilder::new() + .command(r#"echo "${FOO:+5}""#) + .assert_stdout("\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=1 && echo ${FOO:+${BAR:+5}}"#) + .assert_stdout("\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=1 && echo "${FOO:+${BAR:+5}}""#) + .assert_stdout("\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=1 && BAR=2 && echo ${FOO:+${BAR:+5}}"#) + .assert_stdout("5\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=1 && BAR=2 && echo "${FOO:+${BAR:+5}}""#) + .assert_stdout("5\n") + .run() + .await; + + TestBuilder::new() + .command("FOO=12345 && echo ${FOO:2:$((2+2))}") + .assert_stdout("345\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=12345 && echo "${FOO:2:$((2+2))}""#) + .assert_stdout("345\n") + .run() + .await; + + TestBuilder::new() + .command("FOO=12345 && echo ${FOO: -2:-1}") + .assert_stdout("4\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=12345 && echo "${FOO: -2:-1}""#) + .assert_stdout("4\n") + .run() + .await; + + TestBuilder::new() + .command("FOO=12345 && echo ${FOO: -2}") + .assert_stdout("45\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=12345 && echo "${FOO: -2}""#) + .assert_stdout("45\n") + .run() + .await; + + TestBuilder::new() + .command("FOO=12345 && echo ${FOO: -4: 2}") + .assert_stdout("23\n") + .run() + .await; + + TestBuilder::new() + .command(r#"FOO=12345 && echo "${FOO: -4: 2}""#) + .assert_stdout("23\n") + .run() + .await; +} + #[cfg(test)] fn no_such_file_error_text() -> &'static str { if cfg!(windows) {