diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index a516e3044c..03226b233b 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -75,6 +75,7 @@ pub fn create_default_context(cwd: impl AsRef) -> EngineState { ParEach, Prepend, Range, + Reduce, Reject, Reverse, Select, diff --git a/crates/nu-command/src/example_test.rs b/crates/nu-command/src/example_test.rs index ba6c949c76..e7ddefb4c4 100644 --- a/crates/nu-command/src/example_test.rs +++ b/crates/nu-command/src/example_test.rs @@ -12,7 +12,10 @@ use nu_protocol::{ use crate::To; #[cfg(test)] -use super::{Ansi, Date, From, Into, Math, Path, Random, Split, Str, StrCollect, Url}; +use super::{ + Ansi, Date, From, If, Into, Math, Path, Random, Split, Str, StrCollect, StrFindReplace, + StrLength, Url, +}; #[cfg(test)] pub fn test_examples(cmd: impl Command + 'static) { @@ -27,8 +30,11 @@ pub fn test_examples(cmd: impl Command + 'static) { let mut working_set = StateWorkingSet::new(&*engine_state); working_set.add_decl(Box::new(Str)); working_set.add_decl(Box::new(StrCollect)); + working_set.add_decl(Box::new(StrLength)); + working_set.add_decl(Box::new(StrFindReplace)); working_set.add_decl(Box::new(BuildString)); working_set.add_decl(Box::new(From)); + working_set.add_decl(Box::new(If)); working_set.add_decl(Box::new(To)); working_set.add_decl(Box::new(Into)); working_set.add_decl(Box::new(Random)); diff --git a/crates/nu-command/src/filters/mod.rs b/crates/nu-command/src/filters/mod.rs index 487ed01ecb..156c0057e4 100644 --- a/crates/nu-command/src/filters/mod.rs +++ b/crates/nu-command/src/filters/mod.rs @@ -19,6 +19,7 @@ mod nth; mod par_each; mod prepend; mod range; +mod reduce; mod reject; mod reverse; mod select; @@ -51,6 +52,7 @@ pub use nth::Nth; pub use par_each::ParEach; pub use prepend::Prepend; pub use range::Range; +pub use reduce::Reduce; pub use reject::Reject; pub use reverse::Reverse; pub use select::Select; diff --git a/crates/nu-command/src/filters/reduce.rs b/crates/nu-command/src/filters/reduce.rs new file mode 100644 index 0000000000..6044e5829b --- /dev/null +++ b/crates/nu-command/src/filters/reduce.rs @@ -0,0 +1,220 @@ +use nu_engine::{eval_block, CallExt}; + +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::{ + Example, IntoPipelineData, PipelineData, ShellError, Signature, Span, SyntaxShape, Value, +}; + +#[derive(Clone)] +pub struct Reduce; + +impl Command for Reduce { + fn name(&self) -> &str { + "reduce" + } + + fn signature(&self) -> Signature { + Signature::build("reduce") + .named( + "fold", + SyntaxShape::Any, + "reduce with initial value", + Some('f'), + ) + .required( + "block", + SyntaxShape::Block(Some(vec![SyntaxShape::Any])), + "reducing function", + ) + .switch("numbered", "iterate with an index", Some('n')) + } + + fn usage(&self) -> &str { + "Aggregate a list table to a single value using an accumulator block." + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[ 1 2 3 4 ] | reduce { $it.acc + $it.item }", + description: "Sum values of a list (same as 'math sum')", + result: Some(Value::Int { + val: 10, + span: Span::test_data(), + }), + }, + Example { + example: "[ 1 2 3 4 ] | reduce -f 10 { $it.acc + $it.item }", + description: "Sum values with a starting value (fold)", + result: Some(Value::Int { + val: 20, + span: Span::test_data(), + }), + }, + Example { + example: r#"[ i o t ] | reduce -f "Arthur, King of the Britons" { $it.acc | str find-replace -a $it.item "X" }"#, + description: "Replace selected characters in a string with 'X'", + result: Some(Value::String { + val: "ArXhur, KXng Xf Xhe BrXXXns".to_string(), + span: Span::test_data(), + }), + }, + Example { + example: r#"[ one longest three bar ] | reduce -n { + if ($it.item | str length) > ($it.acc | str length) { + $it.item + } else { + $it.acc + } + }"#, + description: "Find the longest string and its index", + result: Some(Value::Record { + cols: vec!["index".to_string(), "item".to_string()], + vals: vec![ + Value::Int { + val: 3, + span: Span::test_data(), + }, + Value::String { + val: "longest".to_string(), + span: Span::test_data(), + }, + ], + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + // TODO: How to make this interruptible? + // TODO: Change the vars to $acc and $it instead of $it.acc and $it.item + // (requires parser change) + + let span = call.head; + + let fold: Option = call.get_flag(engine_state, stack, "fold")?; + let numbered = call.has_flag("numbered"); + let block = if let Some(block_id) = call.nth(0).and_then(|b| b.as_block()) { + engine_state.get_block(block_id) + } else { + return Err(ShellError::SpannedLabeledError( + "Internal Error".to_string(), + "expected block".to_string(), + span, + )); + }; + + let mut stack = stack.collect_captures(&block.captures); + let orig_env_vars = stack.env_vars.clone(); + let orig_env_hidden = stack.env_hidden.clone(); + + let mut input_iter = input.into_iter(); + + let (off, start_val) = if let Some(val) = fold { + (0, val) + } else if let Some(val) = input_iter.next() { + (1, val) + } else { + return Err(ShellError::SpannedLabeledError( + "Expected input".to_string(), + "needs input".to_string(), + span, + )); + }; + + Ok(input_iter + .enumerate() + .fold(start_val, move |acc, (idx, x)| { + stack.with_env(&orig_env_vars, &orig_env_hidden); + + // if the acc coming from previous iter is indexed, drop the index + let acc = if let Value::Record { cols, vals, .. } = &acc { + if cols.len() == 2 && vals.len() == 2 { + if cols[0].eq("index") && cols[1].eq("item") { + vals[1].clone() + } else { + acc + } + } else { + acc + } + } else { + acc + }; + + if let Some(var) = block.signature.get_positional(0) { + if let Some(var_id) = &var.var_id { + let it = if numbered { + Value::Record { + cols: vec![ + "index".to_string(), + "acc".to_string(), + "item".to_string(), + ], + vals: vec![ + Value::Int { + val: idx as i64 + off, + span, + }, + acc, + x, + ], + span, + } + } else { + Value::Record { + cols: vec!["acc".to_string(), "item".to_string()], + vals: vec![acc, x], + span, + } + }; + + stack.add_var(*var_id, it); + } + } + + let v = match eval_block(engine_state, &mut stack, block, PipelineData::new(span)) { + Ok(v) => v.into_value(span), + Err(error) => Value::Error { error }, + }; + + if numbered { + // make sure the output is indexed + Value::Record { + cols: vec!["index".to_string(), "item".to_string()], + vals: vec![ + Value::Int { + val: idx as i64 + off, + span, + }, + v, + ], + span, + } + } else { + v + } + }) + .into_pipeline_data()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Reduce {}) + } +} diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 35c39647b9..ffb3cfaa9f 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -2815,6 +2815,7 @@ pub fn parse_block_expression( let (output, err) = lite_parse(&output[amt_to_skip..]); error = error.or(err); + // TODO: Finish this if let SyntaxShape::Block(Some(v)) = shape { if signature.is_none() && v.len() == 1 { // We'll assume there's an `$it` present