From 833825ae9a72c30668088dd77afc8cf96b2dc2b3 Mon Sep 17 00:00:00 2001 From: Leon Date: Mon, 21 Nov 2022 23:35:11 +1000 Subject: [PATCH] Allow iteration blocks to have an optional extra index parameter (alternative to `-n` flags) (#6994) Alters `all`, `any`, `each while`, `each`, `insert`, `par-each`, `reduce`, `update`, `upsert` and `where`, so that their blocks take an optional parameter containing the index. --- crates/nu-command/src/filters/all.rs | 19 +++++- crates/nu-command/src/filters/any.rs | 19 +++++- crates/nu-command/src/filters/each.rs | 54 +++++++++++---- crates/nu-command/src/filters/each_while.rs | 49 ++++++++++--- crates/nu-command/src/filters/insert.rs | 43 +++++++++++- crates/nu-command/src/filters/par_each.rs | 76 +++++++++++++++++++-- crates/nu-command/src/filters/reduce.rs | 57 ++++++++++------ crates/nu-command/src/filters/update.rs | 23 +++++-- crates/nu-command/src/filters/upsert.rs | 26 ++++--- crates/nu-command/src/filters/where_.rs | 55 +++++++++++++-- crates/nu-command/tests/commands/all.rs | 10 +++ crates/nu-command/tests/commands/any.rs | 10 +++ crates/nu-command/tests/commands/each.rs | 30 ++++++++ crates/nu-command/tests/commands/insert.rs | 19 ++++++ crates/nu-command/tests/commands/reduce.rs | 10 +++ crates/nu-command/tests/commands/update.rs | 19 ++++++ crates/nu-command/tests/commands/upsert.rs | 29 ++++++++ crates/nu-command/tests/commands/where_.rs | 10 +++ src/tests/test_parser.rs | 2 +- 19 files changed, 486 insertions(+), 74 deletions(-) diff --git a/crates/nu-command/src/filters/all.rs b/crates/nu-command/src/filters/all.rs index f32f0f1f7..54722cbfa 100644 --- a/crates/nu-command/src/filters/all.rs +++ b/crates/nu-command/src/filters/all.rs @@ -54,6 +54,11 @@ impl Command for All { example: "[2 4 6 8] | all {|e| $e mod 2 == 0 }", result: Some(Value::test_bool(true)), }, + Example { + description: "Check that all values are equal to twice their index", + example: "[0 2 4 6] | all {|el ind| $el == $ind * 2 }", + result: Some(Value::test_bool(true)), + }, ] } // This is almost entirely a copy-paste of `any`'s run(), so make sure any changes to this are @@ -81,7 +86,7 @@ impl Command for All { let ctrlc = engine_state.ctrlc.clone(); let engine_state = engine_state.clone(); - for value in input.into_interruptible_iter(ctrlc) { + for (idx, value) in input.into_interruptible_iter(ctrlc).enumerate() { // 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. @@ -90,6 +95,18 @@ impl Command for All { if let Some(var_id) = var_id { stack.add_var(var_id, value.clone()); } + // Optional index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Int { + val: idx as i64, + span, + }, + ); + } + } let eval = eval_block( &engine_state, diff --git a/crates/nu-command/src/filters/any.rs b/crates/nu-command/src/filters/any.rs index df83ed3b2..bac5356dc 100644 --- a/crates/nu-command/src/filters/any.rs +++ b/crates/nu-command/src/filters/any.rs @@ -53,6 +53,11 @@ impl Command for Any { example: "[2 4 1 6 8] | any {|e| $e mod 2 == 1 }", result: Some(Value::test_bool(true)), }, + Example { + description: "Check if any value is equal to twice its own index", + example: "[9 8 7 6] | any {|el ind| $el == $ind * 2 }", + result: Some(Value::test_bool(true)), + }, ] } // This is almost entirely a copy-paste of `all`'s run(), so make sure any changes to this are @@ -80,7 +85,7 @@ impl Command for Any { let ctrlc = engine_state.ctrlc.clone(); let engine_state = engine_state.clone(); - for value in input.into_interruptible_iter(ctrlc) { + for (idx, value) in input.into_interruptible_iter(ctrlc).enumerate() { // 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. @@ -89,6 +94,18 @@ impl Command for Any { if let Some(var_id) = var_id { stack.add_var(var_id, value.clone()); } + // Optional index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Int { + val: idx as i64, + span, + }, + ); + } + } let eval = eval_block( &engine_state, diff --git a/crates/nu-command/src/filters/each.rs b/crates/nu-command/src/filters/each.rs index 572e62053..36fd98849 100644 --- a/crates/nu-command/src/filters/each.rs +++ b/crates/nu-command/src/filters/each.rs @@ -16,7 +16,7 @@ impl Command for Each { } fn usage(&self) -> &str { - "Run a closure on each row of input" + "Run a closure on each row of the input list, creating a new list with the results." } fn extra_usage(&self) -> &str { @@ -41,11 +41,15 @@ with 'transpose' first."# )]) .required( "closure", - SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), + SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Int])), "the closure to run", ) .switch("keep-empty", "keep empty result cells", Some('k')) - .switch("numbered", "iterate with an index", Some('n')) + .switch( + "numbered", + "iterate with an index (deprecated; use a two-parameter block instead)", + Some('n'), + ) .category(Category::Filters) } @@ -80,7 +84,7 @@ with 'transpose' first."# vec![ Example { - example: "[1 2 3] | each { |it| 2 * $it }", + example: "[1 2 3] | each {|e| 2 * $e }", description: "Multiplies elements in list", result: Some(Value::List { vals: stream_test_1, @@ -88,19 +92,26 @@ with 'transpose' first."# }), }, Example { - example: r#"[1 2 3] | each { |it| if $it == 2 { echo "found 2!"} }"#, - description: "Iterate over each element, keeping only values that succeed", + example: r#"[1 2 3 2] | each {|e| if $e == 2 { "two" } }"#, + description: "Produce a list that has \"two\" for each 2 in the input", result: Some(Value::List { - vals: vec![Value::String { - val: "found 2!".to_string(), - span: Span::test_data(), - }], + vals: vec![ + Value::String { + val: "two".to_string(), + span: Span::test_data(), + }, + Value::String { + val: "two".to_string(), + span: Span::test_data(), + }, + ], span: Span::test_data(), }), }, Example { - example: r#"[1 2 3] | each -n { |it| if $it.item == 2 { echo $"found 2 at ($it.index)!"} }"#, - description: "Iterate over each element, print the matching value and its index", + example: r#"[1 2 3] | each {|el ind| if $el == 2 { $"found 2 at ($ind)!"} }"#, + description: + "Iterate over each element, producing a list showing indexes of any 2s", result: Some(Value::List { vals: vec![Value::String { val: "found 2 at 1!".to_string(), @@ -110,7 +121,7 @@ with 'transpose' first."# }), }, Example { - example: r#"[1 2 3] | each --keep-empty { |it| if $it == 2 { echo "found 2!"} }"#, + example: r#"[1 2 3] | each --keep-empty {|e| if $e == 2 { echo "found 2!"} }"#, description: "Iterate over each element, keeping all results", result: Some(Value::List { vals: stream_test_2, @@ -148,6 +159,8 @@ with 'transpose' first."# PipelineData::Value(Value::Range { .. }, ..) | PipelineData::Value(Value::List { .. }, ..) | PipelineData::ListStream { .. } => Ok(input + // To enumerate over the input (for the index argument), + // it must be converted into an iterator using into_iter(). .into_iter() .enumerate() .map(move |(idx, x)| { @@ -158,6 +171,7 @@ with 'transpose' first."# if let Some(var) = block.signature.get_positional(0) { if let Some(var_id) = &var.var_id { + // -n changes the first argument into an {index, item} record. if numbered { stack.add_var( *var_id, @@ -178,6 +192,18 @@ with 'transpose' first."# } } } + // Optional second index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Int { + val: idx as i64, + span, + }, + ); + } + } let input_span = x.span(); match eval_block( @@ -254,6 +280,8 @@ with 'transpose' first."# } }) .into_pipeline_data(ctrlc)), + // This match allows non-iterables to be accepted, + // which is currently considered undesirable (Nov 2022). PipelineData::Value(x, ..) => { if let Some(var) = block.signature.get_positional(0) { if let Some(var_id) = &var.var_id { diff --git a/crates/nu-command/src/filters/each_while.rs b/crates/nu-command/src/filters/each_while.rs index dcf07aac5..387c61c1f 100644 --- a/crates/nu-command/src/filters/each_while.rs +++ b/crates/nu-command/src/filters/each_while.rs @@ -15,7 +15,7 @@ impl Command for EachWhile { } fn usage(&self) -> &str { - "Run a block on each element of input until a null is found" + "Run a block on each row of the input list until a null is found, then create a new list with the results." } fn search_terms(&self) -> Vec<&str> { @@ -30,10 +30,14 @@ impl Command for EachWhile { )]) .required( "closure", - SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), + SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Int])), "the closure to run", ) - .switch("numbered", "iterate with an index", Some('n')) + .switch( + "numbered", + "iterate with an index (deprecated; use a two-parameter block instead)", + Some('n'), + ) .category(Category::Filters) } @@ -61,24 +65,24 @@ impl Command for EachWhile { vec![ Example { - example: "[1 2 3] | each while { |it| if $it < 3 { $it * 2 } else { null } }", - description: "Multiplies elements below three by two", + example: "[1 2 3 2 1] | each while {|e| if $e < 3 { $e * 2 } }", + description: "Produces a list of each element before the 3, doubled", result: Some(Value::List { vals: stream_test_1, span: Span::test_data(), }), }, Example { - example: r#"[1 2 stop 3 4] | each while { |it| if $it == 'stop' { null } else { $"Output: ($it)" } }"#, - description: "Output elements till reaching 'stop'", + example: r#"[1 2 stop 3 4] | each while {|e| if $e != 'stop' { $"Output: ($e)" } }"#, + description: "Output elements until reaching 'stop'", result: Some(Value::List { vals: stream_test_2, span: Span::test_data(), }), }, Example { - example: r#"[1 2 3] | each while -n { |it| if $it.item < 2 { $"value ($it.item) at ($it.index)!"} else { null } }"#, - description: "Iterate over each element, print the matching value and its index", + example: r#"[1 2 3] | each while {|el ind| if $el < 2 { $"value ($el) at ($ind)!"} }"#, + description: "Iterate over each element, printing the matching value and its index", result: Some(Value::List { vals: vec![Value::String { val: "value 1 at 0!".to_string(), @@ -115,6 +119,9 @@ impl Command for EachWhile { PipelineData::Value(Value::Range { .. }, ..) | PipelineData::Value(Value::List { .. }, ..) | PipelineData::ListStream { .. } => Ok(input + // To enumerate over the input (for the index argument), + // it must be converted into an iterator using into_iter(). + // TODO: Could this be changed to .into_interruptible_iter(ctrlc) ? .into_iter() .enumerate() .map_while(move |(idx, x)| { @@ -145,6 +152,18 @@ impl Command for EachWhile { } } } + // Optional second index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Int { + val: idx as i64, + span, + }, + ); + } + } match eval_block( &engine_state, @@ -229,6 +248,8 @@ impl Command for EachWhile { }) .fuse() .into_pipeline_data(ctrlc)), + // This match allows non-iterables to be accepted, + // which is currently considered undesirable (Nov 2022). PipelineData::Value(x, ..) => { if let Some(var) = block.signature.get_positional(0) { if let Some(var_id) = &var.var_id { @@ -253,7 +274,17 @@ impl Command for EachWhile { #[cfg(test)] mod test { use super::*; + use nu_test_support::{nu, pipeline}; + #[test] + fn uses_optional_index_argument() { + let actual = nu!( + cwd: ".", pipeline( + r#"[7 8 9 10] | each while {|el ind| $el + $ind } | to nuon"# + )); + + assert_eq!(actual.out, "[7, 9, 11, 13]"); + } #[test] fn test_examples() { use crate::test_examples; diff --git a/crates/nu-command/src/filters/insert.rs b/crates/nu-command/src/filters/insert.rs index f652a813c..9e265c430 100644 --- a/crates/nu-command/src/filters/insert.rs +++ b/crates/nu-command/src/filters/insert.rs @@ -38,7 +38,7 @@ impl Command for Insert { } fn usage(&self) -> &str { - "Insert a new column." + "Insert a new column, using an expression or block to create each row's values." } fn search_terms(&self) -> Vec<&str> { @@ -57,7 +57,7 @@ impl Command for Insert { fn examples(&self) -> Vec { vec![Example { - description: "Insert a new entry into a record", + description: "Insert a new entry into a single record", example: "{'name': 'nu', 'stars': 5} | insert alias 'Nushell'", result: Some(Value::Record { cols: vec!["name".into(), "stars".into(), "alias".into()], @@ -68,6 +68,34 @@ impl Command for Insert { ], span: Span::test_data(), }), + }, Example { + description: "Insert a column with values equal to their row index, plus the value of 'foo' in each row", + example: "[[foo]; [7] [8] [9]] | insert bar {|el ind| $el.foo + $ind }", + result: Some(Value::List { + vals: vec![Value::Record { + cols: vec!["foo".into(), "bar".into()], + vals: vec![ + Value::test_int(7), + Value::test_int(7), + ], + span: Span::test_data(), + }, Value::Record { + cols: vec!["foo".into(), "bar".into()], + vals: vec![ + Value::test_int(8), + Value::test_int(9), + ], + span: Span::test_data(), + }, Value::Record { + cols: vec!["foo".into(), "bar".into()], + vals: vec![ + Value::test_int(9), + Value::test_int(11), + ], + span: Span::test_data(), + }], + span: Span::test_data(), + }), }] } } @@ -98,6 +126,9 @@ fn insert( let orig_env_vars = stack.env_vars.clone(); let orig_env_hidden = stack.env_hidden.clone(); + // enumerate() can't be used here because it converts records into tables + // when combined with into_pipeline_data(). Hence, the index is tracked manually like so. + let mut idx: i64 = 0; input.map( move |mut input| { // with_env() is used here to ensure that each iteration uses @@ -105,11 +136,19 @@ fn insert( // Hence, a 'cd' in the first loop won't affect the next loop. stack.with_env(&orig_env_vars, &orig_env_hidden); + // Element argument if let Some(var) = block.signature.get_positional(0) { if let Some(var_id) = &var.var_id { stack.add_var(*var_id, input.clone()) } } + // Optional index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, Value::Int { val: idx, span }); + } + idx += 1; + } let output = eval_block( &engine_state, diff --git a/crates/nu-command/src/filters/par_each.rs b/crates/nu-command/src/filters/par_each.rs index 653ee03bb..74ea3c3db 100644 --- a/crates/nu-command/src/filters/par_each.rs +++ b/crates/nu-command/src/filters/par_each.rs @@ -18,7 +18,7 @@ impl Command for ParEach { } fn usage(&self) -> &str { - "Run a closure on each element of input in parallel" + "Run a closure on each row of the input list in parallel, creating a new list with the results." } fn signature(&self) -> nu_protocol::Signature { @@ -29,18 +29,23 @@ impl Command for ParEach { )]) .required( "closure", - SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), + SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Int])), "the closure to run", ) - .switch("numbered", "iterate with an index", Some('n')) + .switch( + "numbered", + "iterate with an index (deprecated; use a two-parameter block instead)", + Some('n'), + ) .category(Category::Filters) } fn examples(&self) -> Vec { vec![ Example { - example: "[1 2 3] | par-each { |it| 2 * $it }", - description: "Multiplies elements in list", + example: "[1 2 3] | par-each { 2 * $in }", + description: + "Multiplies each number. Note that the list will become arbitrarily disordered.", result: None, }, Example { @@ -107,6 +112,18 @@ impl Command for ParEach { } } } + // Optional second index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Int { + val: idx as i64, + span, + }, + ); + } + } let val_span = x.span(); match eval_block( @@ -159,6 +176,18 @@ impl Command for ParEach { } } } + // Optional second index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Int { + val: idx as i64, + span, + }, + ); + } + } let val_span = x.span(); match eval_block( @@ -210,6 +239,18 @@ impl Command for ParEach { } } } + // Optional second index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Int { + val: idx as i64, + span, + }, + ); + } + } let val_span = x.span(); match eval_block( @@ -270,6 +311,18 @@ impl Command for ParEach { } } } + // Optional second index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Int { + val: idx as i64, + span, + }, + ); + } + } match eval_block( engine_state, @@ -287,6 +340,8 @@ impl Command for ParEach { .into_iter() .flatten() .into_pipeline_data(ctrlc)), + // This match allows non-iterables to be accepted, + // which is currently considered undesirable (Nov 2022). PipelineData::Value(x, ..) => { let block = engine_state.get_block(block_id); @@ -313,6 +368,17 @@ impl Command for ParEach { #[cfg(test)] mod test { use super::*; + use nu_test_support::{nu, pipeline}; + + #[test] + fn uses_optional_index_argument() { + let actual = nu!( + cwd: ".", pipeline( + r#"[7,8,9,10] | par-each {|el ind| $ind } | describe"# + )); + + assert_eq!(actual.out, "list"); + } #[test] fn test_examples() { diff --git a/crates/nu-command/src/filters/reduce.rs b/crates/nu-command/src/filters/reduce.rs index b54937161..dfbe08b1d 100644 --- a/crates/nu-command/src/filters/reduce.rs +++ b/crates/nu-command/src/filters/reduce.rs @@ -27,14 +27,22 @@ impl Command for Reduce { ) .required( "closure", - SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Any])), + SyntaxShape::Closure(Some(vec![ + SyntaxShape::Any, + SyntaxShape::Any, + SyntaxShape::Int, + ])), "reducing function", ) - .switch("numbered", "iterate with an index", Some('n')) + .switch( + "numbered", + "iterate with an index (deprecated; use a 3-parameter block instead)", + Some('n'), + ) } fn usage(&self) -> &str { - "Aggregate a list table to a single value using an accumulator block." + "Aggregate a list to a single value using an accumulator block." } fn search_terms(&self) -> Vec<&str> { @@ -52,10 +60,10 @@ impl Command for Reduce { }), }, Example { - example: "[ 1 2 3 ] | reduce -n {|it, acc| $acc.item + $it.item }", - description: "Sum values of a list (same as 'math sum')", + example: "[ 8 7 6 ] | reduce {|it, acc, ind| $acc + $it + $ind }", + description: "Sum values of a list, plus their indexes", result: Some(Value::Int { - val: 6, + val: 22, span: Span::test_data(), }), }, @@ -70,24 +78,13 @@ impl Command for Reduce { Example { example: r#"[ i o t ] | reduce -f "Arthur, King of the Britons" {|it, acc| $acc | str replace -a $it "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(), - }), + result: Some(Value::test_string("ArXhur, KXng Xf Xhe BrXXXns")), }, Example { - example: r#"[ one longest three bar ] | reduce -n { |it, acc| - if ($it.item | str length) > ($acc.item | str length) { - $it.item - } else { - $acc.item - } - }"#, - description: "Find the longest string and its index", - result: Some(Value::String { - val: "longest".to_string(), - span: Span::test_data(), - }), + example: r#"['foo.gz', 'bar.gz', 'baz.gz'] | reduce -f '' {|str all ind| $"($all)(if $ind != 0 {'; '})($ind + 1)-($str)" }"#, + description: + "Add ascending numbers to each of the filenames, and join with semicolons.", + result: Some(Value::test_string("1-foo.gz; 2-bar.gz; 3-baz.gz")), }, ] } @@ -114,6 +111,8 @@ impl Command for Reduce { let redirect_stdout = call.redirect_stdout; let redirect_stderr = call.redirect_stderr; + // To enumerate over the input (for the index argument), + // it must be converted into an iterator using into_iter(). let mut input_iter = input.into_iter(); let (off, start_val) = if let Some(val) = fold { @@ -170,12 +169,14 @@ impl Command for Reduce { // Hence, a 'cd' in the first loop won't affect the next loop. stack.with_env(&orig_env_vars, &orig_env_hidden); + // Element argument if let Some(var) = block.signature.get_positional(0) { if let Some(var_id) = &var.var_id { stack.add_var(*var_id, x); } } + // Accumulator argument if let Some(var) = block.signature.get_positional(1) { if let Some(var_id) = &var.var_id { acc = if numbered { @@ -201,6 +202,18 @@ impl Command for Reduce { stack.add_var(*var_id, acc); } } + // Optional third index argument + if let Some(var) = block.signature.get_positional(2) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Int { + val: idx as i64, + span, + }, + ); + } + } acc = eval_block( engine_state, diff --git a/crates/nu-command/src/filters/update.rs b/crates/nu-command/src/filters/update.rs index 07a028892..b461230ea 100644 --- a/crates/nu-command/src/filters/update.rs +++ b/crates/nu-command/src/filters/update.rs @@ -25,7 +25,7 @@ impl Command for Update { .required( "replacement value", SyntaxShape::Any, - "the new value to give the cell(s)", + "the new value to give the cell(s), or a block to create the value", ) .category(Category::Filters) } @@ -48,7 +48,7 @@ impl Command for Update { vec![ Example { description: "Update a column value", - example: "echo {'name': 'nu', 'stars': 5} | update name 'Nushell'", + example: "{'name': 'nu', 'stars': 5} | update name 'Nushell'", result: Some(Value::Record { cols: vec!["name".into(), "stars".into()], vals: vec![Value::test_string("Nushell"), Value::test_int(5)], @@ -57,16 +57,21 @@ impl Command for Update { }, Example { description: "Use in block form for more involved updating logic", - example: "echo [[count fruit]; [1 'apple']] | update count {|f| $f.count + 1}", + example: "[[count fruit]; [1 'apple']] | update count {|row index| ($row.fruit | str length) + $index }", result: Some(Value::List { vals: vec![Value::Record { cols: vec!["count".into(), "fruit".into()], - vals: vec![Value::test_int(2), Value::test_string("apple")], + vals: vec![Value::test_int(5), Value::test_string("apple")], span: Span::test_data(), }], span: Span::test_data(), }), }, + Example { + description: "Alter each value in the 'authors' column to use a single string instead of a list", + example: "[[project, authors]; ['nu', ['Andrés', 'JT', 'Yehuda']]] | update authors {|row| $row.authors | str join ','}", + result: Some(Value::List { vals: vec![Value::Record { cols: vec!["project".into(), "authors".into()], vals: vec![Value::test_string("nu"), Value::test_string("Andrés,JT,Yehuda")], span: Span::test_data()}], span: Span::test_data()}), + }, ] } } @@ -97,6 +102,9 @@ fn update( let orig_env_vars = stack.env_vars.clone(); let orig_env_hidden = stack.env_hidden.clone(); + // enumerate() can't be used here because it converts records into tables + // when combined with into_pipeline_data(). Hence, the index is tracked manually like so. + let mut idx: i64 = 0; input.map( move |mut input| { // with_env() is used here to ensure that each iteration uses @@ -109,6 +117,13 @@ fn update( stack.add_var(*var_id, input.clone()) } } + // Optional index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, Value::Int { val: idx, span }); + } + idx += 1; + } let output = eval_block( &engine_state, diff --git a/crates/nu-command/src/filters/upsert.rs b/crates/nu-command/src/filters/upsert.rs index 54d1306db..ec852e7e5 100644 --- a/crates/nu-command/src/filters/upsert.rs +++ b/crates/nu-command/src/filters/upsert.rs @@ -25,7 +25,7 @@ impl Command for Upsert { .required( "replacement value", SyntaxShape::Any, - "the new value to give the cell(s)", + "the new value to give the cell(s), or a block to create the value", ) .category(Category::Filters) } @@ -50,21 +50,17 @@ impl Command for Upsert { fn examples(&self) -> Vec { vec![Example { - description: "Update a column value", + description: "Update a record's value", example: "{'name': 'nu', 'stars': 5} | upsert name 'Nushell'", result: Some(Value::Record { cols: vec!["name".into(), "stars".into()], vals: vec![Value::test_string("Nushell"), Value::test_int(5)], span: Span::test_data()}), }, Example { - description: "Insert a new column", + description: "Insert a new entry into a single record", example: "{'name': 'nu', 'stars': 5} | upsert language 'Rust'", result: Some(Value::Record { cols: vec!["name".into(), "stars".into(), "language".into()], vals: vec![Value::test_string("nu"), Value::test_int(5), Value::test_string("Rust")], span: Span::test_data()}), }, Example { - description: "Use in block form for more involved updating logic", - example: "[[count fruit]; [1 'apple']] | upsert count {|f| $f.count + 1}", - result: Some(Value::List { vals: vec![Value::Record { cols: vec!["count".into(), "fruit".into()], vals: vec![Value::test_int(2), Value::test_string("apple")], span: Span::test_data()}], span: Span::test_data()}), - }, Example { - description: "Use in block form for more involved updating logic", - example: "[[project, authors]; ['nu', ['Andrés', 'JT', 'Yehuda']]] | upsert authors {|a| $a.authors | str join ','}", - result: Some(Value::List { vals: vec![Value::Record { cols: vec!["project".into(), "authors".into()], vals: vec![Value::test_string("nu"), Value::test_string("Andrés,JT,Yehuda")], span: Span::test_data()}], span: Span::test_data()}), + description: "Use in closure form for more involved updating logic", + example: "[[count fruit]; [1 'apple']] | upsert count {|row index| ($row.fruit | str length) + $index }", + result: Some(Value::List { vals: vec![Value::Record { cols: vec!["count".into(), "fruit".into()], vals: vec![Value::test_int(5), Value::test_string("apple")], span: Span::test_data()}], span: Span::test_data()}), }, Example { description: "Upsert an int into a list, updating an existing value based on the index", @@ -117,6 +113,9 @@ fn upsert( let orig_env_vars = stack.env_vars.clone(); let orig_env_hidden = stack.env_hidden.clone(); + // enumerate() can't be used here because it converts records into tables + // when combined with into_pipeline_data(). Hence, the index is tracked manually like so. + let mut idx: i64 = 0; input.map( move |mut input| { // with_env() is used here to ensure that each iteration uses @@ -129,6 +128,13 @@ fn upsert( stack.add_var(*var_id, input.clone()) } } + // Optional index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var(*var_id, Value::Int { val: idx, span }); + } + idx += 1; + } let output = eval_block( &engine_state, diff --git a/crates/nu-command/src/filters/where_.rs b/crates/nu-command/src/filters/where_.rs index e0ac728fb..19e91117c 100644 --- a/crates/nu-command/src/filters/where_.rs +++ b/crates/nu-command/src/filters/where_.rs @@ -31,8 +31,8 @@ impl Command for Where { .optional("cond", SyntaxShape::RowCondition, "condition") .named( "closure", - SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), - "use where with a closure", + SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Int])), + "use with a closure instead", Some('b'), ) .category(Category::Filters) @@ -65,8 +65,11 @@ impl Command for Where { PipelineData::Value(Value::Range { .. }, ..) | PipelineData::Value(Value::List { .. }, ..) | PipelineData::ListStream { .. } => Ok(input + // To enumerate over the input (for the index argument), + // it must be converted into an iterator using into_iter(). .into_iter() - .filter_map(move |x| { + .enumerate() + .filter_map(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. @@ -77,6 +80,18 @@ impl Command for Where { stack.add_var(*var_id, x.clone()); } } + // Optional index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Int { + val: idx as i64, + span, + }, + ); + } + } match eval_block( &engine_state, @@ -108,7 +123,8 @@ impl Command for Where { .. } => Ok(stream .into_iter() - .filter_map(move |x| { + .enumerate() + .filter_map(move |(idx, x)| { // see note above about with_env() stack.with_env(&orig_env_vars, &orig_env_hidden); @@ -122,6 +138,18 @@ impl Command for Where { stack.add_var(*var_id, x.clone()); } } + // Optional index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Int { + val: idx as i64, + span, + }, + ); + } + } match eval_block( &engine_state, @@ -145,6 +173,8 @@ impl Command for Where { } }) .into_pipeline_data(ctrlc)), + // This match allows non-iterables to be accepted, + // which is currently considered undesirable (Nov 2022). PipelineData::Value(x, ..) => { // see note above about with_env() stack.with_env(&orig_env_vars, &orig_env_hidden); @@ -197,7 +227,8 @@ impl Command for Where { let redirect_stderr = call.redirect_stderr; Ok(input .into_iter() - .filter_map(move |value| { + .enumerate() + .filter_map(move |(idx, value)| { stack.with_env(&orig_env_vars, &orig_env_hidden); if let Some(var) = block.signature.get_positional(0) { @@ -205,6 +236,18 @@ impl Command for Where { stack.add_var(*var_id, value.clone()); } } + // Optional index argument + if let Some(var) = block.signature.get_positional(1) { + if let Some(var_id) = &var.var_id { + stack.add_var( + *var_id, + Value::Int { + val: idx as i64, + span, + }, + ); + } + } let result = eval_block( &engine_state, &mut stack, @@ -283,7 +326,7 @@ impl Command for Where { // TODO: This should work but does not. (Note that `Let` must be present in the working_set in `example_test.rs`). // See https://github.com/nushell/nushell/issues/7034 // Example { - // description: "Get all numbers above 3 with an existing block condition", + // description: "List all numbers above 3, using an existing closure condition", // example: "let a = {$in > 3}; [1, 2, 5, 6] | where -b $a", // result: Some(Value::List { // vals: vec![ diff --git a/crates/nu-command/tests/commands/all.rs b/crates/nu-command/tests/commands/all.rs index 3dfb3ad09..0f5b778bf 100644 --- a/crates/nu-command/tests/commands/all.rs +++ b/crates/nu-command/tests/commands/all.rs @@ -108,6 +108,16 @@ fn early_exits_with_0_param_blocks() { assert_eq!(actual.out, "1false"); } +#[test] +fn uses_optional_index_argument() { + let actual = nu!( + cwd: ".", pipeline( + r#"[7 8 9] | all {|el ind| print $ind | true }"# + )); + + assert_eq!(actual.out, "012true"); +} + #[test] fn unique_env_each_iteration() { let actual = nu!( diff --git a/crates/nu-command/tests/commands/any.rs b/crates/nu-command/tests/commands/any.rs index dee6e59e6..74b2d7924 100644 --- a/crates/nu-command/tests/commands/any.rs +++ b/crates/nu-command/tests/commands/any.rs @@ -84,6 +84,16 @@ fn early_exits_with_0_param_blocks() { assert_eq!(actual.out, "1true"); } +#[test] +fn uses_optional_index_argument() { + let actual = nu!( + cwd: ".", pipeline( + r#"[7 8 9] | any {|el ind| print $ind | false }"# + )); + + assert_eq!(actual.out, "012false"); +} + #[test] fn unique_env_each_iteration() { let actual = nu!( diff --git a/crates/nu-command/tests/commands/each.rs b/crates/nu-command/tests/commands/each.rs index df1ba48a6..7886a5aee 100644 --- a/crates/nu-command/tests/commands/each.rs +++ b/crates/nu-command/tests/commands/each.rs @@ -71,3 +71,33 @@ fn each_implicit_it_in_block() { assert_eq!(actual.out, "ace"); } + +#[test] +fn uses_optional_index_argument() { + let actual = nu!( + cwd: ".", pipeline( + r#"[7 8 9 10] | each {|el ind| $ind } | to nuon"# + )); + + assert_eq!(actual.out, "[0, 1, 2, 3]"); +} + +#[test] +fn each_while_uses_optional_index_argument() { + let actual = nu!( + cwd: ".", pipeline( + r#"[7 8 9 10] | each while {|el ind| $ind } | to nuon"# + )); + + assert_eq!(actual.out, "[0, 1, 2, 3]"); +} + +#[test] +fn par_each_uses_optional_index_argument() { + let actual = nu!( + cwd: ".", pipeline( + r#"[7 8 9 10] | par-each {|el ind| $ind } | to nuon"# + )); + + assert_eq!(actual.out, "[0, 1, 2, 3]"); +} diff --git a/crates/nu-command/tests/commands/insert.rs b/crates/nu-command/tests/commands/insert.rs index 5ac10f6fa..c8c36fc04 100644 --- a/crates/nu-command/tests/commands/insert.rs +++ b/crates/nu-command/tests/commands/insert.rs @@ -14,6 +14,15 @@ fn insert_the_column() { assert_eq!(actual.out, "0.7.0"); } +#[test] +fn doesnt_convert_record_to_table() { + let actual = nu!( + cwd: ".", r#"{a:1} | insert b 2 | to nuon"# + ); + + assert_eq!(actual.out, "{a: 1, b: 2}"); +} + #[test] fn insert_the_column_conflict() { let actual = nu!( @@ -76,3 +85,13 @@ fn insert_past_end_list() { assert_eq!(actual.out, r#"[1,2,3,null,null,"abc"]"#); } + +#[test] +fn uses_optional_index_argument() { + let actual = nu!( + cwd: ".", pipeline( + r#"[[a]; [7] [6]] | insert b {|el ind| $ind + 1 + $el.a } | to nuon"# + )); + + assert_eq!(actual.out, "[[a, b]; [7, 8], [6, 8]]"); +} diff --git a/crates/nu-command/tests/commands/reduce.rs b/crates/nu-command/tests/commands/reduce.rs index fb496e810..e26d1f0c3 100644 --- a/crates/nu-command/tests/commands/reduce.rs +++ b/crates/nu-command/tests/commands/reduce.rs @@ -119,3 +119,13 @@ fn error_reduce_empty() { assert!(actual.err.contains("needs input")); } + +#[test] +fn uses_optional_index_argument() { + let actual = nu!( + cwd: ".", pipeline( + r#"[18 19 20] | reduce -f 0 {|elem accum index| $accum + $index } | to nuon"# + )); + + assert_eq!(actual.out, "3"); +} diff --git a/crates/nu-command/tests/commands/update.rs b/crates/nu-command/tests/commands/update.rs index 63fef7dc4..6860da515 100644 --- a/crates/nu-command/tests/commands/update.rs +++ b/crates/nu-command/tests/commands/update.rs @@ -14,6 +14,15 @@ fn sets_the_column() { assert_eq!(actual.out, "0.7.0"); } +#[test] +fn doesnt_convert_record_to_table() { + let actual = nu!( + cwd: ".", r#"{a:1} | update a 2 | to nuon"# + ); + + assert_eq!(actual.out, "{a: 2}"); +} + #[cfg(features = "inc")] #[test] fn sets_the_column_from_a_block_run_output() { @@ -105,3 +114,13 @@ fn update_nonexistent_column() { assert!(actual.err.contains("cannot find column 'b'")); } + +#[test] +fn uses_optional_index_argument() { + let actual = nu!( + cwd: ".", pipeline( + r#"[[a]; [7] [6]] | update a {|el ind| $ind + 1 + $el.a } | to nuon"# + )); + + assert_eq!(actual.out, "[[a]; [8], [8]]"); +} diff --git a/crates/nu-command/tests/commands/upsert.rs b/crates/nu-command/tests/commands/upsert.rs index ac8e5c9b1..a1a1ae699 100644 --- a/crates/nu-command/tests/commands/upsert.rs +++ b/crates/nu-command/tests/commands/upsert.rs @@ -14,6 +14,15 @@ fn sets_the_column() { assert_eq!(actual.out, "0.7.0"); } +#[test] +fn doesnt_convert_record_to_table() { + let actual = nu!( + cwd: ".", r#"{a:1} | upsert a 2 | to nuon"# + ); + + assert_eq!(actual.out, "{a: 2}"); +} + #[cfg(features = "inc")] #[test] fn sets_the_column_from_a_block_run_output() { @@ -58,3 +67,23 @@ fn sets_the_column_from_a_subexpression() { assert_eq!(actual.out, "true"); } + +#[test] +fn uses_optional_index_argument_inserting() { + let actual = nu!( + cwd: ".", pipeline( + r#"[[a]; [7] [6]] | upsert b {|el ind| $ind + 1 + $el.a } | to nuon"# + )); + + assert_eq!(actual.out, "[[a, b]; [7, 8], [6, 8]]"); +} + +#[test] +fn uses_optional_index_argument_updating() { + let actual = nu!( + cwd: ".", pipeline( + r#"[[a]; [7] [6]] | upsert a {|el ind| $ind + 1 + $el.a } | to nuon"# + )); + + assert_eq!(actual.out, "[[a]; [8], [8]]"); +} diff --git a/crates/nu-command/tests/commands/where_.rs b/crates/nu-command/tests/commands/where_.rs index 5cc37747d..9b9b49f4f 100644 --- a/crates/nu-command/tests/commands/where_.rs +++ b/crates/nu-command/tests/commands/where_.rs @@ -72,6 +72,16 @@ fn where_not_in_table() { assert_eq!(actual.out, "4"); } +#[test] +fn uses_optional_index_argument() { + let actual = nu!( + cwd: ".", + r#"[7 8 9 10] | where {|el ind| $ind < 2 } | to nuon"# + ); + + assert_eq!(actual.out, "[7, 8]"); +} + #[cfg(feature = "database")] #[test] fn binary_operator_comparisons() { diff --git a/src/tests/test_parser.rs b/src/tests/test_parser.rs index 390dc64d3..46a4f31d2 100644 --- a/src/tests/test_parser.rs +++ b/src/tests/test_parser.rs @@ -349,7 +349,7 @@ fn proper_missing_param() -> TestResult { #[test] fn block_arity_check1() -> TestResult { - fail_test(r#"ls | each { |x, y| 1}"#, "expected 1 block parameter") + fail_test(r#"ls | each { |x, y, z| 1}"#, "expected 2 block parameters") } #[test]