From 62e34b69b34ff2f9f39b8c75a9789d9d35da8fe7 Mon Sep 17 00:00:00 2001 From: JT <547158+jntrnr@users.noreply.github.com> Date: Fri, 25 Nov 2022 09:39:16 +1300 Subject: [PATCH] New commands: `break`, `continue`, `return`, and `loop` (#7230) # Description This adds `break`, `continue`, `return`, and `loop`. * `break` - breaks out a loop * `continue` - continues a loop at the next iteration * `return` - early return from a function call * `loop` - loop forever (until the loop hits a break) Examples: ``` for i in 1..10 { if $i == 5 { continue } print $i } ``` ``` for i in 1..10 { if $i == 5 { break } print $i } ``` ``` def foo [x] { if true { return 2 } $x } foo 100 ``` ``` loop { print "hello, forever" } ``` ``` [1, 2, 3, 4, 5] | each {|x| if $x > 3 { break } $x } ``` # User-Facing Changes Adds the above commands. # Tests + Formatting Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass # After Submitting If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. --- crates/nu-cli/src/repl.rs | 5 +- crates/nu-command/src/core_commands/break_.rs | 49 +++++++++ .../nu-command/src/core_commands/continue_.rs | 49 +++++++++ crates/nu-command/src/core_commands/do_.rs | 4 +- crates/nu-command/src/core_commands/for_.rs | 40 +++++-- crates/nu-command/src/core_commands/loop_.rs | 101 ++++++++++++++++++ crates/nu-command/src/core_commands/mod.rs | 8 ++ .../nu-command/src/core_commands/return_.rs | 61 +++++++++++ crates/nu-command/src/core_commands/while_.rs | 18 +++- crates/nu-command/src/default_context.rs | 4 + crates/nu-command/src/example_test.rs | 4 +- crates/nu-command/src/filters/each.rs | 29 ++--- crates/nu-command/src/filters/each_while.rs | 8 +- crates/nu-command/src/filters/par_each.rs | 12 +-- crates/nu-command/tests/commands/break_.rs | 37 +++++++ crates/nu-command/tests/commands/continue_.rs | 13 +++ crates/nu-command/tests/commands/mod.rs | 3 + crates/nu-command/tests/commands/return_.rs | 25 +++++ crates/nu-engine/src/eval.rs | 23 +++- crates/nu-engine/src/lib.rs | 4 +- crates/nu-protocol/src/shell_error.rs | 14 ++- 21 files changed, 469 insertions(+), 42 deletions(-) create mode 100644 crates/nu-command/src/core_commands/break_.rs create mode 100644 crates/nu-command/src/core_commands/continue_.rs create mode 100644 crates/nu-command/src/core_commands/loop_.rs create mode 100644 crates/nu-command/src/core_commands/return_.rs create mode 100644 crates/nu-command/tests/commands/break_.rs create mode 100644 crates/nu-command/tests/commands/continue_.rs create mode 100644 crates/nu-command/tests/commands/return_.rs diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index dd2e47cd6..2b0838fa5 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -10,7 +10,7 @@ use lazy_static::lazy_static; use log::{info, trace, warn}; use miette::{IntoDiagnostic, Result}; use nu_color_config::get_color_config; -use nu_engine::{convert_env_values, eval_block}; +use nu_engine::{convert_env_values, eval_block, eval_block_with_early_return}; use nu_parser::{lex, parse, trim_quotes_str}; use nu_protocol::{ ast::PathMember, @@ -962,7 +962,8 @@ pub fn run_hook_block( } } - match eval_block(engine_state, &mut callee_stack, block, input, false, false) { + match eval_block_with_early_return(engine_state, &mut callee_stack, block, input, false, false) + { Ok(pipeline_data) => match pipeline_data.into_value(span) { Value::Error { error } => Err(error), val => { diff --git a/crates/nu-command/src/core_commands/break_.rs b/crates/nu-command/src/core_commands/break_.rs new file mode 100644 index 000000000..1951466a4 --- /dev/null +++ b/crates/nu-command/src/core_commands/break_.rs @@ -0,0 +1,49 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Type}; + +#[derive(Clone)] +pub struct Break; + +impl Command for Break { + fn name(&self) -> &str { + "break" + } + + fn usage(&self) -> &str { + "Break a loop" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("break") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .category(Category::Core) + } + + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn is_parser_keyword(&self) -> bool { + true + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Err(ShellError::Break(call.head)) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Break out of a loop", + example: r#"loop { break }"#, + result: None, + }] + } +} diff --git a/crates/nu-command/src/core_commands/continue_.rs b/crates/nu-command/src/core_commands/continue_.rs new file mode 100644 index 000000000..c16af5eba --- /dev/null +++ b/crates/nu-command/src/core_commands/continue_.rs @@ -0,0 +1,49 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, Type}; + +#[derive(Clone)] +pub struct Continue; + +impl Command for Continue { + fn name(&self) -> &str { + "continue" + } + + fn usage(&self) -> &str { + "Continue a loop from the next iteration" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("continue") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .category(Category::Core) + } + + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn is_parser_keyword(&self) -> bool { + true + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + Err(ShellError::Continue(call.head)) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Continue a loop from the next iteration", + example: r#"for i in 1..10 { if $i == 5 { continue }; print $i }"#, + result: None, + }] + } +} diff --git a/crates/nu-command/src/core_commands/do_.rs b/crates/nu-command/src/core_commands/do_.rs index 15bb4b802..6f41bc1b8 100644 --- a/crates/nu-command/src/core_commands/do_.rs +++ b/crates/nu-command/src/core_commands/do_.rs @@ -1,4 +1,4 @@ -use nu_engine::{eval_block, CallExt}; +use nu_engine::{eval_block_with_early_return, CallExt}; use nu_protocol::ast::Call; use nu_protocol::engine::{Closure, Command, EngineState, Stack}; use nu_protocol::{ @@ -100,7 +100,7 @@ impl Command for Do { ) } } - let result = eval_block( + let result = eval_block_with_early_return( engine_state, &mut stack, block, diff --git a/crates/nu-command/src/core_commands/for_.rs b/crates/nu-command/src/core_commands/for_.rs index 0d9ce3228..763d40e5f 100644 --- a/crates/nu-command/src/core_commands/for_.rs +++ b/crates/nu-command/src/core_commands/for_.rs @@ -1,7 +1,9 @@ use nu_engine::{eval_block, eval_expression, CallExt}; use nu_protocol::ast::Call; use nu_protocol::engine::{Block, Command, EngineState, Stack}; -use nu_protocol::{Category, Example, ListStream, PipelineData, Signature, SyntaxShape, Value}; +use nu_protocol::{ + Category, Example, ListStream, PipelineData, ShellError, Signature, SyntaxShape, Value, +}; #[derive(Clone)] pub struct For; @@ -104,15 +106,27 @@ impl Command for For { ); //let block = engine_state.get_block(block_id); - eval_block( + match eval_block( &engine_state, stack, &block, PipelineData::new(head), redirect_stdout, redirect_stderr, - )? - .into_value(head); + ) { + Err(ShellError::Break(_)) => { + break; + } + Err(ShellError::Continue(_)) => { + continue; + } + Err(err) => { + return Err(err); + } + Ok(pipeline) => { + pipeline.into_value(head); + } + } } } Value::Range { val, .. } => { @@ -137,15 +151,27 @@ impl Command for For { ); //let block = engine_state.get_block(block_id); - eval_block( + match eval_block( &engine_state, stack, &block, PipelineData::new(head), redirect_stdout, redirect_stderr, - )? - .into_value(head); + ) { + Err(ShellError::Break(_)) => { + break; + } + Err(ShellError::Continue(_)) => { + continue; + } + Err(err) => { + return Err(err); + } + Ok(pipeline) => { + pipeline.into_value(head); + } + } } } x => { diff --git a/crates/nu-command/src/core_commands/loop_.rs b/crates/nu-command/src/core_commands/loop_.rs new file mode 100644 index 000000000..52c3f386a --- /dev/null +++ b/crates/nu-command/src/core_commands/loop_.rs @@ -0,0 +1,101 @@ +use std::sync::atomic::Ordering; + +use nu_engine::{eval_block, CallExt}; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Block, Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Loop; + +impl Command for Loop { + fn name(&self) -> &str { + "loop" + } + + fn usage(&self) -> &str { + "Run a block in a loop." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("loop") + .required("block", SyntaxShape::Block, "block to loop") + .category(Category::Core) + } + + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn is_parser_keyword(&self) -> bool { + true + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let block: Block = call.req(engine_state, stack, 0)?; + + loop { + if let Some(ctrlc) = &engine_state.ctrlc { + if ctrlc.load(Ordering::SeqCst) { + break; + } + } + + let block = engine_state.get_block(block.block_id); + match eval_block( + engine_state, + stack, + block, + PipelineData::new(call.head), + call.redirect_stdout, + call.redirect_stderr, + ) { + Err(ShellError::Break(_)) => { + break; + } + Err(ShellError::Continue(_)) => { + continue; + } + Err(err) => { + return Err(err); + } + Ok(pipeline) => { + pipeline.into_value(call.head); + } + } + } + Ok(PipelineData::new(call.head)) + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Loop while a condition is true", + example: "mut x = 0; loop { if $x > 10 { break }; $x = $x + 1 }; $x", + result: Some(Value::Int { + val: 11, + span: Span::test_data(), + }), + }] + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Loop {}) + } +} diff --git a/crates/nu-command/src/core_commands/mod.rs b/crates/nu-command/src/core_commands/mod.rs index e0b30c41a..139b159a2 100644 --- a/crates/nu-command/src/core_commands/mod.rs +++ b/crates/nu-command/src/core_commands/mod.rs @@ -1,6 +1,8 @@ mod alias; mod ast; +mod break_; mod commandline; +mod continue_; mod debug; mod def; mod def_env; @@ -22,10 +24,12 @@ mod hide_env; mod if_; mod ignore; mod let_; +mod loop_; mod metadata; mod module; mod mut_; pub(crate) mod overlay; +mod return_; mod try_; mod use_; mod version; @@ -33,7 +37,9 @@ mod while_; pub use alias::Alias; pub use ast::Ast; +pub use break_::Break; pub use commandline::Commandline; +pub use continue_::Continue; pub use debug::Debug; pub use def::Def; pub use def_env::DefEnv; @@ -55,10 +61,12 @@ pub use hide_env::HideEnv; pub use if_::If; pub use ignore::Ignore; pub use let_::Let; +pub use loop_::Loop; pub use metadata::Metadata; pub use module::Module; pub use mut_::Mut; pub use overlay::*; +pub use return_::Return; pub use try_::Try; pub use use_::Use; pub use version::Version; diff --git a/crates/nu-command/src/core_commands/return_.rs b/crates/nu-command/src/core_commands/return_.rs new file mode 100644 index 000000000..fe4be0623 --- /dev/null +++ b/crates/nu-command/src/core_commands/return_.rs @@ -0,0 +1,61 @@ +use nu_engine::CallExt; +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value, +}; + +#[derive(Clone)] +pub struct Return; + +impl Command for Return { + fn name(&self) -> &str { + "return" + } + + fn usage(&self) -> &str { + "Return early from a function" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("return") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .optional("return_value", SyntaxShape::Any, "optional value to return") + .category(Category::Core) + } + + fn extra_usage(&self) -> &str { + r#"This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html"# + } + + fn is_parser_keyword(&self) -> bool { + true + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let return_value: Option = call.opt(engine_state, stack, 0)?; + if let Some(value) = return_value { + Err(ShellError::Return(call.head, Box::new(value))) + } else { + Err(ShellError::Return( + call.head, + Box::new(Value::nothing(call.head)), + )) + } + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Return early", + example: r#"def foo [] { return }"#, + result: None, + }] + } +} diff --git a/crates/nu-command/src/core_commands/while_.rs b/crates/nu-command/src/core_commands/while_.rs index 5c9bccc3f..3ed315370 100644 --- a/crates/nu-command/src/core_commands/while_.rs +++ b/crates/nu-command/src/core_commands/while_.rs @@ -59,15 +59,27 @@ impl Command for While { Value::Bool { val, .. } => { if *val { let block = engine_state.get_block(block.block_id); - eval_block( + match eval_block( engine_state, stack, block, PipelineData::new(call.head), call.redirect_stdout, call.redirect_stderr, - )? - .into_value(call.head); + ) { + Err(ShellError::Break(_)) => { + break; + } + Err(ShellError::Continue(_)) => { + continue; + } + Err(err) => { + return Err(err); + } + Ok(pipeline) => { + pipeline.into_value(call.head); + } + } } else { break; } diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 73358b567..66c04de71 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -30,7 +30,9 @@ pub fn create_default_context() -> EngineState { bind_command! { Alias, Ast, + Break, Commandline, + Continue, Debug, Def, DefEnv, @@ -57,9 +59,11 @@ pub fn create_default_context() -> EngineState { OverlayNew, OverlayHide, Let, + Loop, Metadata, Module, Mut, + Return, Try, Use, Version, diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs index 70bf8fb01..b92264f2d 100644 --- a/crates/nu-command/src/example_test.rs +++ b/crates/nu-command/src/example_test.rs @@ -12,7 +12,7 @@ mod test_examples { Ansi, Date, Echo, From, If, Into, LetEnv, Math, Path, Random, Split, SplitColumn, SplitRow, Str, StrJoin, StrLength, StrReplace, Url, Wrap, }; - use crate::To; + use crate::{Break, Mut, To}; use itertools::Itertools; use nu_protocol::{ ast::Block, @@ -80,6 +80,8 @@ mod test_examples { working_set.add_decl(Box::new(Wrap)); working_set.add_decl(Box::new(LetEnv)); working_set.add_decl(Box::new(Echo)); + working_set.add_decl(Box::new(Break)); + working_set.add_decl(Box::new(Mut)); // Adding the command that is being tested to the working set working_set.add_decl(cmd); diff --git a/crates/nu-command/src/filters/each.rs b/crates/nu-command/src/filters/each.rs index 36fd98849..35d8f63d1 100644 --- a/crates/nu-command/src/filters/each.rs +++ b/crates/nu-command/src/filters/each.rs @@ -1,10 +1,10 @@ use super::utils::chain_error_with_input; -use nu_engine::{eval_block, CallExt}; +use nu_engine::{eval_block_with_early_return, CallExt}; use nu_protocol::ast::Call; use nu_protocol::engine::{Closure, Command, EngineState, Stack}; use nu_protocol::{ - Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, Signature, - Span, SyntaxShape, Type, Value, + Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, + Signature, Span, SyntaxShape, Type, Value, }; #[derive(Clone)] @@ -163,7 +163,7 @@ with 'transpose' first."# // it must be converted into an iterator using into_iter(). .into_iter() .enumerate() - .map(move |(idx, x)| { + .map_while(move |(idx, x)| { // with_env() is used here to ensure that each iteration uses // a different set of environment variables. // Hence, a 'cd' in the first loop won't affect the next loop. @@ -206,7 +206,7 @@ with 'transpose' first."# } let input_span = x.span(); - match eval_block( + match eval_block_with_early_return( &engine_state, &mut stack, &block, @@ -214,10 +214,11 @@ with 'transpose' first."# redirect_stdout, redirect_stderr, ) { - Ok(v) => v.into_value(span), + Ok(v) => Some(v.into_value(span)), + Err(ShellError::Break(_)) => None, Err(error) => { let error = chain_error_with_input(error, input_span); - Value::Error { error } + Some(Value::Error { error }) } } }) @@ -229,7 +230,7 @@ with 'transpose' first."# } => Ok(stream .into_iter() .enumerate() - .map(move |(idx, x)| { + .map_while(move |(idx, x)| { // with_env() is used here to ensure that each iteration uses // a different set of environment variables. // Hence, a 'cd' in the first loop won't affect the next loop. @@ -237,7 +238,8 @@ with 'transpose' first."# let x = match x { Ok(x) => x, - Err(err) => return Value::Error { error: err }, + Err(ShellError::Break(_)) => return None, + Err(err) => return Some(Value::Error { error: err }), }; if let Some(var) = block.signature.get_positional(0) { @@ -264,7 +266,7 @@ with 'transpose' first."# } let input_span = x.span(); - match eval_block( + match eval_block_with_early_return( &engine_state, &mut stack, &block, @@ -272,10 +274,11 @@ with 'transpose' first."# redirect_stdout, redirect_stderr, ) { - Ok(v) => v.into_value(span), + Ok(v) => Some(v.into_value(span)), + Err(ShellError::Break(_)) => None, Err(error) => { let error = chain_error_with_input(error, input_span); - Value::Error { error } + Some(Value::Error { error }) } } }) @@ -289,7 +292,7 @@ with 'transpose' first."# } } - eval_block( + eval_block_with_early_return( &engine_state, &mut stack, &block, diff --git a/crates/nu-command/src/filters/each_while.rs b/crates/nu-command/src/filters/each_while.rs index 387c61c1f..74f354db9 100644 --- a/crates/nu-command/src/filters/each_while.rs +++ b/crates/nu-command/src/filters/each_while.rs @@ -1,4 +1,4 @@ -use nu_engine::{eval_block, CallExt}; +use nu_engine::{eval_block_with_early_return, CallExt}; use nu_protocol::ast::Call; use nu_protocol::engine::{Closure, Command, EngineState, Stack}; use nu_protocol::{ @@ -165,7 +165,7 @@ impl Command for EachWhile { } } - match eval_block( + match eval_block_with_early_return( &engine_state, &mut stack, &block, @@ -227,7 +227,7 @@ impl Command for EachWhile { } } - match eval_block( + match eval_block_with_early_return( &engine_state, &mut stack, &block, @@ -257,7 +257,7 @@ impl Command for EachWhile { } } - eval_block( + eval_block_with_early_return( &engine_state, &mut stack, &block, diff --git a/crates/nu-command/src/filters/par_each.rs b/crates/nu-command/src/filters/par_each.rs index 74ea3c3db..1040bff54 100644 --- a/crates/nu-command/src/filters/par_each.rs +++ b/crates/nu-command/src/filters/par_each.rs @@ -1,4 +1,4 @@ -use nu_engine::{eval_block, CallExt}; +use nu_engine::{eval_block_with_early_return, CallExt}; use nu_protocol::ast::Call; use nu_protocol::engine::{Closure, Command, EngineState, Stack}; use nu_protocol::{ @@ -126,7 +126,7 @@ impl Command for ParEach { } let val_span = x.span(); - match eval_block( + match eval_block_with_early_return( engine_state, &mut stack, block, @@ -190,7 +190,7 @@ impl Command for ParEach { } let val_span = x.span(); - match eval_block( + match eval_block_with_early_return( engine_state, &mut stack, block, @@ -253,7 +253,7 @@ impl Command for ParEach { } let val_span = x.span(); - match eval_block( + match eval_block_with_early_return( engine_state, &mut stack, block, @@ -324,7 +324,7 @@ impl Command for ParEach { } } - match eval_block( + match eval_block_with_early_return( engine_state, &mut stack, block, @@ -351,7 +351,7 @@ impl Command for ParEach { } } - eval_block( + eval_block_with_early_return( engine_state, &mut stack, block, diff --git a/crates/nu-command/tests/commands/break_.rs b/crates/nu-command/tests/commands/break_.rs new file mode 100644 index 000000000..32275c30d --- /dev/null +++ b/crates/nu-command/tests/commands/break_.rs @@ -0,0 +1,37 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn break_for_loop() { + let actual = nu!( + cwd: ".", pipeline( + r#" + for i in 1..10 { if $i == 2 { break }; print $i } + "# + )); + + assert_eq!(actual.out, r#"1"#); +} + +#[test] +fn break_while_loop() { + let actual = nu!( + cwd: ".", pipeline( + r#" + while true { break }; print "hello" + "# + )); + + assert_eq!(actual.out, r#"hello"#); +} + +#[test] +fn break_each() { + let actual = nu!( + cwd: ".", pipeline( + r#" + [1, 2, 3, 4, 5] | each {|x| if $x > 3 { break }; $x} | math sum + "# + )); + + assert_eq!(actual.out, r#"6"#); +} diff --git a/crates/nu-command/tests/commands/continue_.rs b/crates/nu-command/tests/commands/continue_.rs new file mode 100644 index 000000000..c3b436357 --- /dev/null +++ b/crates/nu-command/tests/commands/continue_.rs @@ -0,0 +1,13 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn continue_for_loop() { + let actual = nu!( + cwd: ".", pipeline( + r#" + for i in 1..10 { if $i == 2 { continue }; print $i } + "# + )); + + assert_eq!(actual.out, r#"1345678910"#); +} diff --git a/crates/nu-command/tests/commands/mod.rs b/crates/nu-command/tests/commands/mod.rs index 547cd99f6..1851fbadc 100644 --- a/crates/nu-command/tests/commands/mod.rs +++ b/crates/nu-command/tests/commands/mod.rs @@ -2,9 +2,11 @@ mod alias; mod all; mod any; mod append; +mod break_; mod cal; mod cd; mod compact; +mod continue_; mod cp; mod date; mod def; @@ -61,6 +63,7 @@ mod redirection; mod reduce; mod reject; mod rename; +mod return_; mod reverse; mod rm; mod roll; diff --git a/crates/nu-command/tests/commands/return_.rs b/crates/nu-command/tests/commands/return_.rs new file mode 100644 index 000000000..e5c5e2c9c --- /dev/null +++ b/crates/nu-command/tests/commands/return_.rs @@ -0,0 +1,25 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn early_return_if_true() { + let actual = nu!( + cwd: ".", pipeline( + r#" + def foo [x] { if true { return 2 }; $x }; foo 100 + "# + )); + + assert_eq!(actual.out, r#"2"#); +} + +#[test] +fn early_return_if_false() { + let actual = nu!( + cwd: ".", pipeline( + r#" + def foo [x] { if false { return 2 }; $x }; foo 100 + "# + )); + + assert_eq!(actual.out, r#"100"#); +} diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index ef2d1a112..01b1b436b 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -156,7 +156,7 @@ pub fn eval_call( } } - let result = eval_block( + let result = eval_block_with_early_return( engine_state, &mut callee_stack, block, @@ -944,6 +944,27 @@ pub fn eval_element_with_input( } } +pub fn eval_block_with_early_return( + engine_state: &EngineState, + stack: &mut Stack, + block: &Block, + input: PipelineData, + redirect_stdout: bool, + redirect_stderr: bool, +) -> Result { + match eval_block( + engine_state, + stack, + block, + input, + redirect_stdout, + redirect_stderr, + ) { + Err(ShellError::Return(_, value)) => Ok(PipelineData::Value(*value, None)), + x => x, + } +} + pub fn eval_block( engine_state: &EngineState, stack: &mut Stack, diff --git a/crates/nu-engine/src/lib.rs b/crates/nu-engine/src/lib.rs index 0f6a8a15c..f12a204d3 100644 --- a/crates/nu-engine/src/lib.rs +++ b/crates/nu-engine/src/lib.rs @@ -11,7 +11,7 @@ pub use column::get_columns; pub use documentation::get_full_help; pub use env::*; pub use eval::{ - eval_block, eval_call, eval_expression, eval_expression_with_input, eval_operator, - eval_subexpression, eval_variable, redirect_env, + eval_block, eval_block_with_early_return, eval_call, eval_expression, + eval_expression_with_input, eval_operator, eval_subexpression, eval_variable, redirect_env, }; pub use glob_from::glob_from; diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 42fc2552a..beb5e1fa9 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -2,7 +2,7 @@ use miette::Diagnostic; use serde::{Deserialize, Serialize}; use thiserror::Error; -use crate::{ast::Operator, Span, Type}; +use crate::{ast::Operator, Span, Type, Value}; /// The fundamental error type for the evaluation engine. These cases represent different kinds of errors /// the evaluator might face, along with helpful spans to label. An error renderer will take this error value @@ -828,6 +828,18 @@ Either make sure {0} is a string, or add a 'to_string' entry for it in ENV_CONVE #[error("Eval block failed with pipeline input")] #[diagnostic(code(nu::shell::eval_block_with_input), url(docsrs))] EvalBlockWithInput(#[label("source value")] Span, #[related] Vec), + + /// Break event, which may become an error if used outside of a loop + #[error("Break used outside of loop")] + Break(#[label = "used outside of loop"] Span), + + /// Continue event, which may become an error if used outside of a loop + #[error("Continue used outside of loop")] + Continue(#[label = "used outside of loop"] Span), + + /// Return event, which may become an error if used outside of a function + #[error("Return used outside of function")] + Return(#[label = "used outside of function"] Span, Box), } impl From for ShellError {