diff --git a/crates/deno_task_shell/src/shell/commands/break_cmd.rs b/crates/deno_task_shell/src/shell/commands/break_cmd.rs new file mode 100644 index 0000000..a7766e6 --- /dev/null +++ b/crates/deno_task_shell/src/shell/commands/break_cmd.rs @@ -0,0 +1,108 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +use futures::future::LocalBoxFuture; +use miette::bail; +use miette::Result; + +use crate::shell::types::ExecuteResult; + +use super::args::parse_arg_kinds; +use super::args::ArgKind; +use super::ShellCommand; +use super::ShellCommandContext; + +pub struct BreakCommand; + +impl ShellCommand for BreakCommand { + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let result = match execute_break(context.args) { + Ok(code) => ExecuteResult::Break(code, Vec::new(), Vec::new()), + Err(err) => { + context.stderr.write_line(&format!("break: {err}")).unwrap(); + ExecuteResult::Continue(1, Vec::new(), Vec::new()) + } + }; + Box::pin(futures::future::ready(result)) + } +} + +fn execute_break(args: Vec) -> Result { + let _n = parse_args(args)?; + // For now, we only support breaking out of the innermost loop + // TODO: Support breaking out of n levels of loops + Ok(0) +} + +fn parse_args(args: Vec) -> Result { + let args = parse_arg_kinds(&args); + let mut paths = Vec::new(); + for arg in args { + match arg { + ArgKind::Arg(arg) => { + paths.push(arg); + } + _ => arg.bail_unsupported()?, + } + } + + match paths.len() { + 0 => Ok(1), + 1 => { + let arg = paths.remove(0).to_string(); + match arg.parse::() { + Ok(value) if value > 0 => Ok(value), + Ok(_) => bail!("loop count out of range"), + Err(_) => bail!("numeric argument required"), + } + } + _ => { + bail!("too many arguments") + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parses_args() { + assert_eq!(parse_args(vec![]).unwrap(), 1); + assert_eq!(parse_args(vec!["1".to_string()]).unwrap(), 1); + assert_eq!(parse_args(vec!["2".to_string()]).unwrap(), 2); + assert_eq!( + parse_args(vec!["0".to_string()]).err().unwrap().to_string(), + "loop count out of range" + ); + assert_eq!( + parse_args(vec!["-1".to_string()]) + .err() + .unwrap() + .to_string(), + "loop count out of range" + ); + assert_eq!( + parse_args(vec!["test".to_string()]) + .err() + .unwrap() + .to_string(), + "numeric argument required" + ); + assert_eq!( + parse_args(vec!["1".to_string(), "2".to_string()]) + .err() + .unwrap() + .to_string(), + "too many arguments" + ); + } + + #[test] + fn executes_break() { + assert_eq!(execute_break(vec![]).unwrap(), 0); + assert_eq!(execute_break(vec!["1".to_string()]).unwrap(), 0); + } +} diff --git a/crates/deno_task_shell/src/shell/commands/continue_cmd.rs b/crates/deno_task_shell/src/shell/commands/continue_cmd.rs new file mode 100644 index 0000000..4ee18db --- /dev/null +++ b/crates/deno_task_shell/src/shell/commands/continue_cmd.rs @@ -0,0 +1,113 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +use futures::future::LocalBoxFuture; +use miette::bail; +use miette::Result; + +use crate::shell::types::ExecuteResult; + +use super::args::parse_arg_kinds; +use super::args::ArgKind; +use super::ShellCommand; +use super::ShellCommandContext; + +pub struct ContinueCommand; + +impl ShellCommand for ContinueCommand { + fn execute( + &self, + mut context: ShellCommandContext, + ) -> LocalBoxFuture<'static, ExecuteResult> { + let result = match execute_continue(context.args) { + Ok(code) => { + ExecuteResult::LoopContinue(code, Vec::new(), Vec::new()) + } + Err(err) => { + context + .stderr + .write_line(&format!("continue: {err}")) + .unwrap(); + ExecuteResult::Continue(1, Vec::new(), Vec::new()) + } + }; + Box::pin(futures::future::ready(result)) + } +} + +fn execute_continue(args: Vec) -> Result { + let _n = parse_args(args)?; + // For now, we only support continuing in the innermost loop + // TODO: Support continuing in n levels of loops + Ok(0) +} + +fn parse_args(args: Vec) -> Result { + let args = parse_arg_kinds(&args); + let mut paths = Vec::new(); + for arg in args { + match arg { + ArgKind::Arg(arg) => { + paths.push(arg); + } + _ => arg.bail_unsupported()?, + } + } + + match paths.len() { + 0 => Ok(1), + 1 => { + let arg = paths.remove(0).to_string(); + match arg.parse::() { + Ok(value) if value > 0 => Ok(value), + Ok(_) => bail!("loop count out of range"), + Err(_) => bail!("numeric argument required"), + } + } + _ => { + bail!("too many arguments") + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn parses_args() { + assert_eq!(parse_args(vec![]).unwrap(), 1); + assert_eq!(parse_args(vec!["1".to_string()]).unwrap(), 1); + assert_eq!(parse_args(vec!["2".to_string()]).unwrap(), 2); + assert_eq!( + parse_args(vec!["0".to_string()]).err().unwrap().to_string(), + "loop count out of range" + ); + assert_eq!( + parse_args(vec!["-1".to_string()]) + .err() + .unwrap() + .to_string(), + "loop count out of range" + ); + assert_eq!( + parse_args(vec!["test".to_string()]) + .err() + .unwrap() + .to_string(), + "numeric argument required" + ); + assert_eq!( + parse_args(vec!["1".to_string(), "2".to_string()]) + .err() + .unwrap() + .to_string(), + "too many arguments" + ); + } + + #[test] + fn executes_continue() { + assert_eq!(execute_continue(vec![]).unwrap(), 0); + assert_eq!(execute_continue(vec!["1".to_string()]).unwrap(), 0); + } +} diff --git a/crates/deno_task_shell/src/shell/commands/mod.rs b/crates/deno_task_shell/src/shell/commands/mod.rs index 6bca36c..247567a 100644 --- a/crates/deno_task_shell/src/shell/commands/mod.rs +++ b/crates/deno_task_shell/src/shell/commands/mod.rs @@ -1,8 +1,10 @@ // Copyright 2018-2024 the Deno authors. MIT license. mod args; +mod break_cmd; mod cat; mod cd; +mod continue_cmd; mod cp_mv; mod echo; mod executable; @@ -34,6 +36,10 @@ use super::types::ShellState; pub fn builtin_commands() -> HashMap> { HashMap::from([ + ( + "break".to_string(), + Rc::new(break_cmd::BreakCommand) as Rc, + ), ( "cat".to_string(), Rc::new(cat::CatCommand) as Rc, @@ -42,6 +48,10 @@ pub fn builtin_commands() -> HashMap> { "cd".to_string(), Rc::new(cd::CdCommand) as Rc, ), + ( + "continue".to_string(), + Rc::new(continue_cmd::ContinueCommand) as Rc, + ), ( "cp".to_string(), Rc::new(cp_mv::CpCommand) as Rc, diff --git a/crates/deno_task_shell/src/shell/execute.rs b/crates/deno_task_shell/src/shell/execute.rs index ed790a0..e0d3a6d 100644 --- a/crates/deno_task_shell/src/shell/execute.rs +++ b/crates/deno_task_shell/src/shell/execute.rs @@ -139,6 +139,8 @@ pub async fn execute_with_pipes( match result { ExecuteResult::Exit(code, _, _) => code, ExecuteResult::Continue(exit_code, _, _) => exit_code, + ExecuteResult::Break(exit_code, _, _) => exit_code, + ExecuteResult::LoopContinue(exit_code, _, _) => exit_code, } } @@ -162,6 +164,8 @@ pub fn execute_sequential_list( let mut final_changes = Vec::new(); let mut async_handles = Vec::new(); let mut was_exit = false; + let mut was_break = false; + let mut was_loop_continue = false; for item in list.items { if item.is_async { let state = state.clone(); @@ -201,6 +205,28 @@ pub fn execute_sequential_list( was_exit = true; break; } + ExecuteResult::Break(exit_code, changes, handles) => { + state.apply_changes(&changes); + state.set_shell_var("?", &exit_code.to_string()); + final_changes.extend(changes); + async_handles.extend(handles); + final_exit_code = exit_code; + was_break = true; + break; + } + ExecuteResult::LoopContinue( + exit_code, + changes, + handles, + ) => { + state.apply_changes(&changes); + state.set_shell_var("?", &exit_code.to_string()); + final_changes.extend(changes); + async_handles.extend(handles); + final_exit_code = exit_code; + was_loop_continue = true; + break; + } ExecuteResult::Continue(exit_code, changes, handles) => { state.apply_changes(&changes); state.set_shell_var("?", &exit_code.to_string()); @@ -228,6 +254,14 @@ pub fn execute_sequential_list( if was_exit { ExecuteResult::Exit(final_exit_code, final_changes, async_handles) + } else if was_break { + ExecuteResult::Break(final_exit_code, final_changes, async_handles) + } else if was_loop_continue { + ExecuteResult::LoopContinue( + final_exit_code, + final_changes, + async_handles, + ) } else { ExecuteResult::Continue( final_exit_code, @@ -310,6 +344,10 @@ fn execute_sequence( .await; let (exit_code, mut async_handles) = match first_result { ExecuteResult::Exit(_, _, _) => return first_result, + ExecuteResult::Break(_, _, _) => return first_result, + ExecuteResult::LoopContinue(_, _, _) => { + return first_result + } ExecuteResult::Continue( exit_code, sub_changes, @@ -349,6 +387,28 @@ fn execute_sequence( async_handles.extend(sub_handles); ExecuteResult::Exit(code, changes, async_handles) } + ExecuteResult::Break( + code, + sub_changes, + sub_handles, + ) => { + changes.extend(sub_changes); + async_handles.extend(sub_handles); + ExecuteResult::Break(code, changes, async_handles) + } + ExecuteResult::LoopContinue( + code, + sub_changes, + sub_handles, + ) => { + changes.extend(sub_changes); + async_handles.extend(sub_handles); + ExecuteResult::LoopContinue( + code, + changes, + async_handles, + ) + } ExecuteResult::Continue( exit_code, sub_changes, @@ -390,6 +450,12 @@ async fn execute_pipeline( ExecuteResult::Exit(code, changes, handles) => { ExecuteResult::Exit(code, changes, handles) } + ExecuteResult::Break(code, changes, handles) => { + ExecuteResult::Break(code, changes, handles) + } + ExecuteResult::LoopContinue(code, changes, handles) => { + ExecuteResult::LoopContinue(code, changes, handles) + } ExecuteResult::Continue(code, changes, handles) => { let new_code = if code == 0 { 1 } else { 0 }; ExecuteResult::Continue(new_code, changes, handles) @@ -625,6 +691,12 @@ async fn execute_command( ExecuteResult::Exit(code, _, handles) => { ExecuteResult::Exit(code, changes, handles) } + ExecuteResult::Break(code, _, handles) => { + ExecuteResult::Break(code, changes, handles) + } + ExecuteResult::LoopContinue(code, _, handles) => { + ExecuteResult::LoopContinue(code, changes, handles) + } ExecuteResult::Continue(code, _, handles) => { ExecuteResult::Continue(code, changes, handles) } @@ -721,6 +793,24 @@ async fn execute_while_clause( last_exit_code = code; break; } + ExecuteResult::Break(code, env_changes, handles) => { + state.apply_changes(&env_changes); + changes.extend(env_changes); + async_handles.extend(handles); + last_exit_code = code; + break; + } + ExecuteResult::LoopContinue( + code, + env_changes, + handles, + ) => { + state.apply_changes(&env_changes); + changes.extend(env_changes); + async_handles.extend(handles); + last_exit_code = code; + continue; + } ExecuteResult::Continue(code, env_changes, handles) => { state.apply_changes(&env_changes); changes.extend(env_changes); @@ -770,6 +860,8 @@ async fn execute_case_clause( let mut changes = Vec::new(); let mut last_exit_code = 0; let mut async_handles = Vec::new(); + let mut was_break = false; + let mut was_loop_continue = false; // Evaluate the word to match against let word_value = match evaluate_word( @@ -821,6 +913,22 @@ async fn execute_case_clause( last_exit_code = code; break; } + ExecuteResult::Break(code, env_changes, handles) => { + state.apply_changes(&env_changes); + changes.extend(env_changes); + async_handles.extend(handles); + last_exit_code = code; + was_break = true; + break; + } + ExecuteResult::LoopContinue(code, env_changes, handles) => { + state.apply_changes(&env_changes); + changes.extend(env_changes); + async_handles.extend(handles); + last_exit_code = code; + was_loop_continue = true; + break; + } ExecuteResult::Continue(code, env_changes, handles) => { state.apply_changes(&env_changes); changes.extend(env_changes); @@ -838,7 +946,11 @@ async fn execute_case_clause( state.apply_changes(&changes); - if state.exit_on_error() && last_exit_code != 0 { + if was_break { + ExecuteResult::Break(last_exit_code, changes, async_handles) + } else if was_loop_continue { + ExecuteResult::LoopContinue(last_exit_code, changes, async_handles) + } else if state.exit_on_error() && last_exit_code != 0 { ExecuteResult::Exit(last_exit_code, changes, async_handles) } else { ExecuteResult::Continue(last_exit_code, changes, async_handles) @@ -891,6 +1003,18 @@ async fn execute_for_clause( last_exit_code = code; break; } + ExecuteResult::Break(code, env_changes, handles) => { + changes.extend(env_changes); + async_handles.extend(handles); + last_exit_code = code; + break; + } + ExecuteResult::LoopContinue(code, env_changes, handles) => { + changes.extend(env_changes); + async_handles.extend(handles); + last_exit_code = code; + continue; + } ExecuteResult::Continue(code, env_changes, handles) => { changes.extend(env_changes); async_handles.extend(handles); @@ -1192,6 +1316,16 @@ async fn execute_pipe_sequence( handles.extend(all_handles); ExecuteResult::Continue(code, changes, handles) } + ExecuteResult::Break(code, env_changes, mut handles) => { + changes.extend(env_changes); + handles.extend(all_handles); + ExecuteResult::Break(code, changes, handles) + } + ExecuteResult::LoopContinue(code, env_changes, mut handles) => { + changes.extend(env_changes); + handles.extend(all_handles); + ExecuteResult::LoopContinue(code, changes, handles) + } ExecuteResult::Continue(code, env_changes, mut handles) => { handles.extend(all_handles); changes.extend(env_changes); @@ -1223,6 +1357,14 @@ async fn execute_subshell( // sub shells do not cause an exit ExecuteResult::Continue(code, env_changes, handles) } + ExecuteResult::Break(code, env_changes, handles) => { + // break propagates out of the subshell to the loop + ExecuteResult::Break(code, env_changes, handles) + } + ExecuteResult::LoopContinue(code, env_changes, handles) => { + // continue propagates out of the subshell to the loop + ExecuteResult::LoopContinue(code, env_changes, handles) + } ExecuteResult::Continue(code, env_changes, handles) => { // env changes are not propagated ExecuteResult::Continue(code, env_changes, handles) @@ -1270,6 +1412,16 @@ async fn execute_if_clause( changes.extend(env_changes); return ExecuteResult::Exit(code, changes, handles); } + ExecuteResult::Break(code, env_changes, handles) => { + changes.extend(env_changes); + return ExecuteResult::Break(code, changes, handles); + } + ExecuteResult::LoopContinue(code, env_changes, handles) => { + changes.extend(env_changes); + return ExecuteResult::LoopContinue( + code, changes, handles, + ); + } ExecuteResult::Continue(code, env_changes, handles) => { changes.extend(env_changes); return ExecuteResult::Continue(code, changes, handles); @@ -1304,6 +1456,26 @@ async fn execute_if_clause( code, changes, handles, ); } + ExecuteResult::Break( + code, + env_changes, + handles, + ) => { + changes.extend(env_changes); + return ExecuteResult::Break( + code, changes, handles, + ); + } + ExecuteResult::LoopContinue( + code, + env_changes, + handles, + ) => { + changes.extend(env_changes); + return ExecuteResult::LoopContinue( + code, changes, handles, + ); + } ExecuteResult::Continue( code, env_changes, @@ -1557,6 +1729,14 @@ async fn execute_simple_command( changes.extend(env_changes); ExecuteResult::Exit(code, changes, handles) } + ExecuteResult::Break(code, env_changes, handles) => { + changes.extend(env_changes); + ExecuteResult::Break(code, changes, handles) + } + ExecuteResult::LoopContinue(code, env_changes, handles) => { + changes.extend(env_changes); + ExecuteResult::LoopContinue(code, changes, handles) + } ExecuteResult::Continue(code, env_changes, handles) => { changes.extend(env_changes); ExecuteResult::Continue(code, changes, handles) diff --git a/crates/deno_task_shell/src/shell/types.rs b/crates/deno_task_shell/src/shell/types.rs index aa4eb95..9eb1970 100644 --- a/crates/deno_task_shell/src/shell/types.rs +++ b/crates/deno_task_shell/src/shell/types.rs @@ -391,6 +391,8 @@ pub const CANCELLATION_EXIT_CODE: i32 = 130; pub enum ExecuteResult { Exit(i32, Vec, Vec>), Continue(i32, Vec, Vec>), + Break(i32, Vec, Vec>), + LoopContinue(i32, Vec, Vec>), } impl ExecuteResult { @@ -406,6 +408,8 @@ impl ExecuteResult { match self { ExecuteResult::Exit(code, _, handles) => (code, handles), ExecuteResult::Continue(code, _, handles) => (code, handles), + ExecuteResult::Break(code, _, handles) => (code, handles), + ExecuteResult::LoopContinue(code, _, handles) => (code, handles), } } @@ -417,6 +421,8 @@ impl ExecuteResult { match self { ExecuteResult::Exit(_, changes, _) => changes, ExecuteResult::Continue(_, changes, _) => changes, + ExecuteResult::Break(_, changes, _) => changes, + ExecuteResult::LoopContinue(_, changes, _) => changes, } } @@ -426,6 +432,10 @@ impl ExecuteResult { match self { ExecuteResult::Exit(_, changes, handles) => (handles, changes), ExecuteResult::Continue(_, changes, handles) => (handles, changes), + ExecuteResult::Break(_, changes, handles) => (handles, changes), + ExecuteResult::LoopContinue(_, changes, handles) => { + (handles, changes) + } } } @@ -433,6 +443,8 @@ impl ExecuteResult { match self { ExecuteResult::Exit(code, _, _) => *code, ExecuteResult::Continue(code, _, _) => *code, + ExecuteResult::Break(code, _, _) => *code, + ExecuteResult::LoopContinue(code, _, _) => *code, } } } diff --git a/crates/shell/src/execute.rs b/crates/shell/src/execute.rs index 5bbda7e..64490c8 100644 --- a/crates/shell/src/execute.rs +++ b/crates/shell/src/execute.rs @@ -47,6 +47,8 @@ pub async fn execute( let changes = match &result { ExecuteResult::Exit(_, changes, _) => changes, ExecuteResult::Continue(_, changes, _) => changes, + ExecuteResult::Break(_, changes, _) => changes, + ExecuteResult::LoopContinue(_, changes, _) => changes, }; // set CWD to the last command's CWD state.apply_changes(changes); diff --git a/crates/tests/test-data/loop.sh b/crates/tests/test-data/loop.sh index df5db4d..0284096 100644 --- a/crates/tests/test-data/loop.sh +++ b/crates/tests/test-data/loop.sh @@ -51,4 +51,46 @@ Number: 5 # 2 # 3 # 4 -# 5 \ No newline at end of file +# 5 + +# Test break in for loop +> for i in 1 2 3 4 5; do +> if [[ $i == 3 ]]; then +> break +> fi +> echo $i +> done +1 +2 + +# Test continue in for loop +> for i in 1 2 3 4 5; do +> if [[ $i == 3 ]]; then +> continue +> fi +> echo $i +> done +1 +2 +4 +5 + +# Test break in nested loop +> for i in 1 2 3; do +> echo "outer: $i" +> for j in a b c; do +> echo " inner: $j" +> if [[ $j == b ]]; then +> break +> fi +> done +> done +outer: 1 + inner: a + inner: b +outer: 2 + inner: a + inner: b +outer: 3 + inner: a + inner: b \ No newline at end of file