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.
This commit is contained in:
Leon 2022-11-21 23:35:11 +10:00 committed by GitHub
parent 899383c30c
commit 833825ae9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 486 additions and 74 deletions

View File

@ -54,6 +54,11 @@ impl Command for All {
example: "[2 4 6 8] | all {|e| $e mod 2 == 0 }", example: "[2 4 6 8] | all {|e| $e mod 2 == 0 }",
result: Some(Value::test_bool(true)), 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 // 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 ctrlc = engine_state.ctrlc.clone();
let engine_state = engine_state.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 // with_env() is used here to ensure that each iteration uses
// a different set of environment variables. // a different set of environment variables.
// Hence, a 'cd' in the first loop won't affect the next loop. // 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 { if let Some(var_id) = var_id {
stack.add_var(var_id, value.clone()); 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( let eval = eval_block(
&engine_state, &engine_state,

View File

@ -53,6 +53,11 @@ impl Command for Any {
example: "[2 4 1 6 8] | any {|e| $e mod 2 == 1 }", example: "[2 4 1 6 8] | any {|e| $e mod 2 == 1 }",
result: Some(Value::test_bool(true)), 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 // 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 ctrlc = engine_state.ctrlc.clone();
let engine_state = engine_state.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 // with_env() is used here to ensure that each iteration uses
// a different set of environment variables. // a different set of environment variables.
// Hence, a 'cd' in the first loop won't affect the next loop. // 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 { if let Some(var_id) = var_id {
stack.add_var(var_id, value.clone()); 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( let eval = eval_block(
&engine_state, &engine_state,

View File

@ -16,7 +16,7 @@ impl Command for Each {
} }
fn usage(&self) -> &str { 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 { fn extra_usage(&self) -> &str {
@ -41,11 +41,15 @@ with 'transpose' first."#
)]) )])
.required( .required(
"closure", "closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Int])),
"the closure to run", "the closure to run",
) )
.switch("keep-empty", "keep empty result cells", Some('k')) .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) .category(Category::Filters)
} }
@ -80,7 +84,7 @@ with 'transpose' first."#
vec![ vec![
Example { Example {
example: "[1 2 3] | each { |it| 2 * $it }", example: "[1 2 3] | each {|e| 2 * $e }",
description: "Multiplies elements in list", description: "Multiplies elements in list",
result: Some(Value::List { result: Some(Value::List {
vals: stream_test_1, vals: stream_test_1,
@ -88,19 +92,26 @@ with 'transpose' first."#
}), }),
}, },
Example { Example {
example: r#"[1 2 3] | each { |it| if $it == 2 { echo "found 2!"} }"#, example: r#"[1 2 3 2] | each {|e| if $e == 2 { "two" } }"#,
description: "Iterate over each element, keeping only values that succeed", description: "Produce a list that has \"two\" for each 2 in the input",
result: Some(Value::List { result: Some(Value::List {
vals: vec![Value::String { vals: vec![
val: "found 2!".to_string(), Value::String {
span: Span::test_data(), val: "two".to_string(),
}], span: Span::test_data(),
},
Value::String {
val: "two".to_string(),
span: Span::test_data(),
},
],
span: Span::test_data(), span: Span::test_data(),
}), }),
}, },
Example { Example {
example: r#"[1 2 3] | each -n { |it| if $it.item == 2 { echo $"found 2 at ($it.index)!"} }"#, example: r#"[1 2 3] | each {|el ind| if $el == 2 { $"found 2 at ($ind)!"} }"#,
description: "Iterate over each element, print the matching value and its index", description:
"Iterate over each element, producing a list showing indexes of any 2s",
result: Some(Value::List { result: Some(Value::List {
vals: vec![Value::String { vals: vec![Value::String {
val: "found 2 at 1!".to_string(), val: "found 2 at 1!".to_string(),
@ -110,7 +121,7 @@ with 'transpose' first."#
}), }),
}, },
Example { 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", description: "Iterate over each element, keeping all results",
result: Some(Value::List { result: Some(Value::List {
vals: stream_test_2, vals: stream_test_2,
@ -148,6 +159,8 @@ with 'transpose' first."#
PipelineData::Value(Value::Range { .. }, ..) PipelineData::Value(Value::Range { .. }, ..)
| PipelineData::Value(Value::List { .. }, ..) | PipelineData::Value(Value::List { .. }, ..)
| PipelineData::ListStream { .. } => Ok(input | 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() .into_iter()
.enumerate() .enumerate()
.map(move |(idx, x)| { .map(move |(idx, x)| {
@ -158,6 +171,7 @@ with 'transpose' first."#
if let Some(var) = block.signature.get_positional(0) { if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id { if let Some(var_id) = &var.var_id {
// -n changes the first argument into an {index, item} record.
if numbered { if numbered {
stack.add_var( stack.add_var(
*var_id, *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(); let input_span = x.span();
match eval_block( match eval_block(
@ -254,6 +280,8 @@ with 'transpose' first."#
} }
}) })
.into_pipeline_data(ctrlc)), .into_pipeline_data(ctrlc)),
// This match allows non-iterables to be accepted,
// which is currently considered undesirable (Nov 2022).
PipelineData::Value(x, ..) => { PipelineData::Value(x, ..) => {
if let Some(var) = block.signature.get_positional(0) { if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id { if let Some(var_id) = &var.var_id {

View File

@ -15,7 +15,7 @@ impl Command for EachWhile {
} }
fn usage(&self) -> &str { 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> { fn search_terms(&self) -> Vec<&str> {
@ -30,10 +30,14 @@ impl Command for EachWhile {
)]) )])
.required( .required(
"closure", "closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Int])),
"the closure to run", "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) .category(Category::Filters)
} }
@ -61,24 +65,24 @@ impl Command for EachWhile {
vec![ vec![
Example { Example {
example: "[1 2 3] | each while { |it| if $it < 3 { $it * 2 } else { null } }", example: "[1 2 3 2 1] | each while {|e| if $e < 3 { $e * 2 } }",
description: "Multiplies elements below three by two", description: "Produces a list of each element before the 3, doubled",
result: Some(Value::List { result: Some(Value::List {
vals: stream_test_1, vals: stream_test_1,
span: Span::test_data(), span: Span::test_data(),
}), }),
}, },
Example { Example {
example: r#"[1 2 stop 3 4] | each while { |it| if $it == 'stop' { null } else { $"Output: ($it)" } }"#, example: r#"[1 2 stop 3 4] | each while {|e| if $e != 'stop' { $"Output: ($e)" } }"#,
description: "Output elements till reaching 'stop'", description: "Output elements until reaching 'stop'",
result: Some(Value::List { result: Some(Value::List {
vals: stream_test_2, vals: stream_test_2,
span: Span::test_data(), span: Span::test_data(),
}), }),
}, },
Example { Example {
example: r#"[1 2 3] | each while -n { |it| if $it.item < 2 { $"value ($it.item) at ($it.index)!"} else { null } }"#, example: r#"[1 2 3] | each while {|el ind| if $el < 2 { $"value ($el) at ($ind)!"} }"#,
description: "Iterate over each element, print the matching value and its index", description: "Iterate over each element, printing the matching value and its index",
result: Some(Value::List { result: Some(Value::List {
vals: vec![Value::String { vals: vec![Value::String {
val: "value 1 at 0!".to_string(), val: "value 1 at 0!".to_string(),
@ -115,6 +119,9 @@ impl Command for EachWhile {
PipelineData::Value(Value::Range { .. }, ..) PipelineData::Value(Value::Range { .. }, ..)
| PipelineData::Value(Value::List { .. }, ..) | PipelineData::Value(Value::List { .. }, ..)
| PipelineData::ListStream { .. } => Ok(input | 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() .into_iter()
.enumerate() .enumerate()
.map_while(move |(idx, x)| { .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( match eval_block(
&engine_state, &engine_state,
@ -229,6 +248,8 @@ impl Command for EachWhile {
}) })
.fuse() .fuse()
.into_pipeline_data(ctrlc)), .into_pipeline_data(ctrlc)),
// This match allows non-iterables to be accepted,
// which is currently considered undesirable (Nov 2022).
PipelineData::Value(x, ..) => { PipelineData::Value(x, ..) => {
if let Some(var) = block.signature.get_positional(0) { if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id { if let Some(var_id) = &var.var_id {
@ -253,7 +274,17 @@ impl Command for EachWhile {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; 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] #[test]
fn test_examples() { fn test_examples() {
use crate::test_examples; use crate::test_examples;

View File

@ -38,7 +38,7 @@ impl Command for Insert {
} }
fn usage(&self) -> &str { 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> { fn search_terms(&self) -> Vec<&str> {
@ -57,7 +57,7 @@ impl Command for Insert {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![Example { 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'", example: "{'name': 'nu', 'stars': 5} | insert alias 'Nushell'",
result: Some(Value::Record { result: Some(Value::Record {
cols: vec!["name".into(), "stars".into(), "alias".into()], cols: vec!["name".into(), "stars".into(), "alias".into()],
@ -68,6 +68,34 @@ impl Command for Insert {
], ],
span: Span::test_data(), 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_vars = stack.env_vars.clone();
let orig_env_hidden = stack.env_hidden.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( input.map(
move |mut input| { move |mut input| {
// with_env() is used here to ensure that each iteration uses // 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. // Hence, a 'cd' in the first loop won't affect the next loop.
stack.with_env(&orig_env_vars, &orig_env_hidden); stack.with_env(&orig_env_vars, &orig_env_hidden);
// Element argument
if let Some(var) = block.signature.get_positional(0) { if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id { if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, input.clone()) 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( let output = eval_block(
&engine_state, &engine_state,

View File

@ -18,7 +18,7 @@ impl Command for ParEach {
} }
fn usage(&self) -> &str { 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 { fn signature(&self) -> nu_protocol::Signature {
@ -29,18 +29,23 @@ impl Command for ParEach {
)]) )])
.required( .required(
"closure", "closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Int])),
"the closure to run", "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) .category(Category::Filters)
} }
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![ vec![
Example { Example {
example: "[1 2 3] | par-each { |it| 2 * $it }", example: "[1 2 3] | par-each { 2 * $in }",
description: "Multiplies elements in list", description:
"Multiplies each number. Note that the list will become arbitrarily disordered.",
result: None, result: None,
}, },
Example { 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(); let val_span = x.span();
match eval_block( 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(); let val_span = x.span();
match eval_block( 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(); let val_span = x.span();
match eval_block( 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( match eval_block(
engine_state, engine_state,
@ -287,6 +340,8 @@ impl Command for ParEach {
.into_iter() .into_iter()
.flatten() .flatten()
.into_pipeline_data(ctrlc)), .into_pipeline_data(ctrlc)),
// This match allows non-iterables to be accepted,
// which is currently considered undesirable (Nov 2022).
PipelineData::Value(x, ..) => { PipelineData::Value(x, ..) => {
let block = engine_state.get_block(block_id); let block = engine_state.get_block(block_id);
@ -313,6 +368,17 @@ impl Command for ParEach {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; 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<int>");
}
#[test] #[test]
fn test_examples() { fn test_examples() {

View File

@ -27,14 +27,22 @@ impl Command for Reduce {
) )
.required( .required(
"closure", "closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Any])), SyntaxShape::Closure(Some(vec![
SyntaxShape::Any,
SyntaxShape::Any,
SyntaxShape::Int,
])),
"reducing function", "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 { 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> { fn search_terms(&self) -> Vec<&str> {
@ -52,10 +60,10 @@ impl Command for Reduce {
}), }),
}, },
Example { Example {
example: "[ 1 2 3 ] | reduce -n {|it, acc| $acc.item + $it.item }", example: "[ 8 7 6 ] | reduce {|it, acc, ind| $acc + $it + $ind }",
description: "Sum values of a list (same as 'math sum')", description: "Sum values of a list, plus their indexes",
result: Some(Value::Int { result: Some(Value::Int {
val: 6, val: 22,
span: Span::test_data(), span: Span::test_data(),
}), }),
}, },
@ -70,24 +78,13 @@ impl Command for Reduce {
Example { Example {
example: r#"[ i o t ] | reduce -f "Arthur, King of the Britons" {|it, acc| $acc | str replace -a $it "X" }"#, 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'", description: "Replace selected characters in a string with 'X'",
result: Some(Value::String { result: Some(Value::test_string("ArXhur, KXng Xf Xhe BrXXXns")),
val: "ArXhur, KXng Xf Xhe BrXXXns".to_string(),
span: Span::test_data(),
}),
}, },
Example { Example {
example: r#"[ one longest three bar ] | reduce -n { |it, acc| example: r#"['foo.gz', 'bar.gz', 'baz.gz'] | reduce -f '' {|str all ind| $"($all)(if $ind != 0 {'; '})($ind + 1)-($str)" }"#,
if ($it.item | str length) > ($acc.item | str length) { description:
$it.item "Add ascending numbers to each of the filenames, and join with semicolons.",
} else { result: Some(Value::test_string("1-foo.gz; 2-bar.gz; 3-baz.gz")),
$acc.item
}
}"#,
description: "Find the longest string and its index",
result: Some(Value::String {
val: "longest".to_string(),
span: Span::test_data(),
}),
}, },
] ]
} }
@ -114,6 +111,8 @@ impl Command for Reduce {
let redirect_stdout = call.redirect_stdout; let redirect_stdout = call.redirect_stdout;
let redirect_stderr = call.redirect_stderr; 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 mut input_iter = input.into_iter();
let (off, start_val) = if let Some(val) = fold { 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. // Hence, a 'cd' in the first loop won't affect the next loop.
stack.with_env(&orig_env_vars, &orig_env_hidden); stack.with_env(&orig_env_vars, &orig_env_hidden);
// Element argument
if let Some(var) = block.signature.get_positional(0) { if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id { if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, x); stack.add_var(*var_id, x);
} }
} }
// Accumulator argument
if let Some(var) = block.signature.get_positional(1) { if let Some(var) = block.signature.get_positional(1) {
if let Some(var_id) = &var.var_id { if let Some(var_id) = &var.var_id {
acc = if numbered { acc = if numbered {
@ -201,6 +202,18 @@ impl Command for Reduce {
stack.add_var(*var_id, acc); 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( acc = eval_block(
engine_state, engine_state,

View File

@ -25,7 +25,7 @@ impl Command for Update {
.required( .required(
"replacement value", "replacement value",
SyntaxShape::Any, 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) .category(Category::Filters)
} }
@ -48,7 +48,7 @@ impl Command for Update {
vec![ vec![
Example { Example {
description: "Update a column value", 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 { result: Some(Value::Record {
cols: vec!["name".into(), "stars".into()], cols: vec!["name".into(), "stars".into()],
vals: vec![Value::test_string("Nushell"), Value::test_int(5)], vals: vec![Value::test_string("Nushell"), Value::test_int(5)],
@ -57,16 +57,21 @@ impl Command for Update {
}, },
Example { Example {
description: "Use in block form for more involved updating logic", 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 { result: Some(Value::List {
vals: vec![Value::Record { vals: vec![Value::Record {
cols: vec!["count".into(), "fruit".into()], 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(),
}], }],
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_vars = stack.env_vars.clone();
let orig_env_hidden = stack.env_hidden.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( input.map(
move |mut input| { move |mut input| {
// with_env() is used here to ensure that each iteration uses // with_env() is used here to ensure that each iteration uses
@ -109,6 +117,13 @@ fn update(
stack.add_var(*var_id, input.clone()) 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( let output = eval_block(
&engine_state, &engine_state,

View File

@ -25,7 +25,7 @@ impl Command for Upsert {
.required( .required(
"replacement value", "replacement value",
SyntaxShape::Any, 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) .category(Category::Filters)
} }
@ -50,21 +50,17 @@ impl Command for Upsert {
fn examples(&self) -> Vec<Example> { fn examples(&self) -> Vec<Example> {
vec![Example { vec![Example {
description: "Update a column value", description: "Update a record's value",
example: "{'name': 'nu', 'stars': 5} | upsert name 'Nushell'", 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()}), 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 { }, Example {
description: "Insert a new column", description: "Insert a new entry into a single record",
example: "{'name': 'nu', 'stars': 5} | upsert language 'Rust'", 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()}), 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 { }, Example {
description: "Use in block form for more involved updating logic", description: "Use in closure form for more involved updating logic",
example: "[[count fruit]; [1 'apple']] | upsert count {|f| $f.count + 1}", 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(2), Value::test_string("apple")], span: Span::test_data()}], span: Span::test_data()}), 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: "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()}),
}, },
Example { Example {
description: "Upsert an int into a list, updating an existing value based on the index", 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_vars = stack.env_vars.clone();
let orig_env_hidden = stack.env_hidden.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( input.map(
move |mut input| { move |mut input| {
// with_env() is used here to ensure that each iteration uses // with_env() is used here to ensure that each iteration uses
@ -129,6 +128,13 @@ fn upsert(
stack.add_var(*var_id, input.clone()) 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( let output = eval_block(
&engine_state, &engine_state,

View File

@ -31,8 +31,8 @@ impl Command for Where {
.optional("cond", SyntaxShape::RowCondition, "condition") .optional("cond", SyntaxShape::RowCondition, "condition")
.named( .named(
"closure", "closure",
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Int])),
"use where with a closure", "use with a closure instead",
Some('b'), Some('b'),
) )
.category(Category::Filters) .category(Category::Filters)
@ -65,8 +65,11 @@ impl Command for Where {
PipelineData::Value(Value::Range { .. }, ..) PipelineData::Value(Value::Range { .. }, ..)
| PipelineData::Value(Value::List { .. }, ..) | PipelineData::Value(Value::List { .. }, ..)
| PipelineData::ListStream { .. } => Ok(input | 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() .into_iter()
.filter_map(move |x| { .enumerate()
.filter_map(move |(idx, x)| {
// with_env() is used here to ensure that each iteration uses // with_env() is used here to ensure that each iteration uses
// a different set of environment variables. // a different set of environment variables.
// Hence, a 'cd' in the first loop won't affect the next loop. // 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()); 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( match eval_block(
&engine_state, &engine_state,
@ -108,7 +123,8 @@ impl Command for Where {
.. ..
} => Ok(stream } => Ok(stream
.into_iter() .into_iter()
.filter_map(move |x| { .enumerate()
.filter_map(move |(idx, x)| {
// see note above about with_env() // see note above about with_env()
stack.with_env(&orig_env_vars, &orig_env_hidden); stack.with_env(&orig_env_vars, &orig_env_hidden);
@ -122,6 +138,18 @@ impl Command for Where {
stack.add_var(*var_id, x.clone()); 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( match eval_block(
&engine_state, &engine_state,
@ -145,6 +173,8 @@ impl Command for Where {
} }
}) })
.into_pipeline_data(ctrlc)), .into_pipeline_data(ctrlc)),
// This match allows non-iterables to be accepted,
// which is currently considered undesirable (Nov 2022).
PipelineData::Value(x, ..) => { PipelineData::Value(x, ..) => {
// see note above about with_env() // see note above about with_env()
stack.with_env(&orig_env_vars, &orig_env_hidden); stack.with_env(&orig_env_vars, &orig_env_hidden);
@ -197,7 +227,8 @@ impl Command for Where {
let redirect_stderr = call.redirect_stderr; let redirect_stderr = call.redirect_stderr;
Ok(input Ok(input
.into_iter() .into_iter()
.filter_map(move |value| { .enumerate()
.filter_map(move |(idx, value)| {
stack.with_env(&orig_env_vars, &orig_env_hidden); stack.with_env(&orig_env_vars, &orig_env_hidden);
if let Some(var) = block.signature.get_positional(0) { if let Some(var) = block.signature.get_positional(0) {
@ -205,6 +236,18 @@ impl Command for Where {
stack.add_var(*var_id, value.clone()); 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( let result = eval_block(
&engine_state, &engine_state,
&mut stack, &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`). // 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 // See https://github.com/nushell/nushell/issues/7034
// Example { // 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", // example: "let a = {$in > 3}; [1, 2, 5, 6] | where -b $a",
// result: Some(Value::List { // result: Some(Value::List {
// vals: vec![ // vals: vec![

View File

@ -108,6 +108,16 @@ fn early_exits_with_0_param_blocks() {
assert_eq!(actual.out, "1false"); 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] #[test]
fn unique_env_each_iteration() { fn unique_env_each_iteration() {
let actual = nu!( let actual = nu!(

View File

@ -84,6 +84,16 @@ fn early_exits_with_0_param_blocks() {
assert_eq!(actual.out, "1true"); 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] #[test]
fn unique_env_each_iteration() { fn unique_env_each_iteration() {
let actual = nu!( let actual = nu!(

View File

@ -71,3 +71,33 @@ fn each_implicit_it_in_block() {
assert_eq!(actual.out, "ace"); 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]");
}

View File

@ -14,6 +14,15 @@ fn insert_the_column() {
assert_eq!(actual.out, "0.7.0"); 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] #[test]
fn insert_the_column_conflict() { fn insert_the_column_conflict() {
let actual = nu!( let actual = nu!(
@ -76,3 +85,13 @@ fn insert_past_end_list() {
assert_eq!(actual.out, r#"[1,2,3,null,null,"abc"]"#); 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]]");
}

View File

@ -119,3 +119,13 @@ fn error_reduce_empty() {
assert!(actual.err.contains("needs input")); 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");
}

View File

@ -14,6 +14,15 @@ fn sets_the_column() {
assert_eq!(actual.out, "0.7.0"); 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")] #[cfg(features = "inc")]
#[test] #[test]
fn sets_the_column_from_a_block_run_output() { 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'")); 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]]");
}

View File

@ -14,6 +14,15 @@ fn sets_the_column() {
assert_eq!(actual.out, "0.7.0"); 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")] #[cfg(features = "inc")]
#[test] #[test]
fn sets_the_column_from_a_block_run_output() { 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"); 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]]");
}

View File

@ -72,6 +72,16 @@ fn where_not_in_table() {
assert_eq!(actual.out, "4"); 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")] #[cfg(feature = "database")]
#[test] #[test]
fn binary_operator_comparisons() { fn binary_operator_comparisons() {

View File

@ -349,7 +349,7 @@ fn proper_missing_param() -> TestResult {
#[test] #[test]
fn block_arity_check1() -> TestResult { 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] #[test]