Fix replacement closures for update, insert, and upsert (#11258)

# Description
This PR addresses #11204 which points out that using a closure for the
replacement value with `update`, `insert`, or `upsert` does not work for
lists.

# User-Facing Changes
- Replacement closures should now work for lists in `upsert`, `insert`,
and `update`. E.g., `[0] | update 0 {|i| $i + 1 }` now gives `[1]`
instead of an unhelpful error.
- `[1 2] | insert 4 20` no longer works. Before, this would give `[1, 2,
null, null, 20]`, but now it gives an error. This was done to match the
intended behavior in `Value::insert_data_at_cell_path`, whereas the
behavior before was probably unintentional. Following
`Value::insert_data_at_cell_path`, inserting at the end of a list is
also fine, so the valid indices for `upsert` and `insert` are
`0..=length` just like `Vec::insert` or list inserts in other languages.

# Tests + Formatting
Added tests for `upsert`, `insert`, and `update`:
- Replacement closures for lists, list streams, records, and tables
- Other list stream tests
This commit is contained in:
Ian Manske
2023-12-09 21:22:45 +00:00
committed by GitHub
parent 94b27267fd
commit fa5d7babb9
7 changed files with 1116 additions and 383 deletions

View File

@ -1,9 +1,9 @@
use nu_engine::{eval_block, CallExt};
use nu_protocol::ast::{Call, CellPath, PathMember};
use nu_protocol::ast::{Block, Call, CellPath, PathMember};
use nu_protocol::engine::{Closure, Command, EngineState, Stack};
use nu_protocol::{
record, Category, Example, FromValue, IntoInterruptiblePipelineData, IntoPipelineData,
PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value,
};
#[derive(Clone)]
@ -57,49 +57,64 @@ impl Command for Insert {
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Insert a new entry into a single record",
example: "{'name': 'nu', 'stars': 5} | insert alias 'Nushell'",
result: Some(Value::test_record(record! {
"name" => Value::test_string("nu"),
"stars" => Value::test_int(5),
"alias" => Value::test_string("Nushell"),
})),
},
Example {
description: "Insert a new column into a table, populating all rows",
example: "[[project, lang]; ['Nushell', 'Rust']] | insert type 'shell'",
result: Some(Value::test_list (
vec![Value::test_record(record! {
vec![
Example {
description: "Insert a new entry into a single record",
example: "{'name': 'nu', 'stars': 5} | insert alias 'Nushell'",
result: Some(Value::test_record(record! {
"name" => Value::test_string("nu"),
"stars" => Value::test_int(5),
"alias" => Value::test_string("Nushell"),
})),
},
Example {
description: "Insert a new column into a table, populating all rows",
example: "[[project, lang]; ['Nushell', 'Rust']] | insert type 'shell'",
result: Some(Value::test_list(vec![Value::test_record(record! {
"project" => Value::test_string("Nushell"),
"lang" => Value::test_string("Rust"),
"type" => Value::test_string("shell"),
})],
)),
},
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]] | enumerate | insert bar {|e| $e.item.foo + $e.index } | flatten",
result: Some(Value::test_list (
vec![
})])),
},
Example {
description: "Insert a new column with values computed based off the other columns",
example: "[[foo]; [7] [8] [9]] | insert bar {|row| $row.foo * 2 }",
result: Some(Value::test_list(vec![
Value::test_record(record! {
"index" => Value::test_int(0),
"foo" => Value::test_int(7),
"bar" => Value::test_int(7),
"foo" => Value::test_int(7),
"bar" => Value::test_int(14),
}),
Value::test_record(record! {
"index" => Value::test_int(1),
"foo" => Value::test_int(8),
"bar" => Value::test_int(9),
"foo" => Value::test_int(8),
"bar" => Value::test_int(16),
}),
Value::test_record(record! {
"index" => Value::test_int(2),
"foo" => Value::test_int(9),
"bar" => Value::test_int(11),
"foo" => Value::test_int(9),
"bar" => Value::test_int(18),
}),
],
)),
}]
])),
},
Example {
description: "Insert a new value into a list at an index",
example: "[1 2 4] | insert 2 3",
result: Some(Value::test_list(vec![
Value::test_int(1),
Value::test_int(2),
Value::test_int(3),
Value::test_int(4),
])),
},
Example {
description: "Insert a new value at the end of a list",
example: "[1 2 3] | insert 3 4",
result: Some(Value::test_list(vec![
Value::test_int(1),
Value::test_int(2),
Value::test_int(3),
Value::test_int(4),
])),
},
]
}
}
@ -109,7 +124,6 @@ fn insert(
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let metadata = input.metadata();
let span = call.head;
let cell_path: CellPath = call.req(engine_state, stack, 0)?;
@ -118,99 +132,263 @@ fn insert(
let redirect_stdout = call.redirect_stdout;
let redirect_stderr = call.redirect_stderr;
let engine_state = engine_state.clone();
let ctrlc = engine_state.ctrlc.clone();
// Replace is a block, so set it up and run it instead of using it as the replacement
if replacement.as_block().is_ok() {
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id).clone();
let mut stack = stack.captures_to_stack(capture_block.captures);
let orig_env_vars = stack.env_vars.clone();
let orig_env_hidden = stack.env_hidden.clone();
input
.map(
move |mut input| {
// 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.
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())
match input {
PipelineData::Value(mut value, metadata) => {
if replacement.as_block().is_ok() {
match (cell_path.members.first(), &mut value) {
(Some(PathMember::String { .. }), Value::List { vals, .. }) => {
let span = replacement.span();
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id);
let stack = stack.captures_to_stack(capture_block.captures.clone());
for val in vals {
let mut stack = stack.clone();
insert_value_by_closure(
val,
span,
engine_state,
&mut stack,
redirect_stdout,
redirect_stderr,
block,
&cell_path.members,
false,
)?;
}
}
(first, _) => {
insert_single_value_by_closure(
&mut value,
replacement,
engine_state,
stack,
redirect_stdout,
redirect_stderr,
&cell_path.members,
matches!(first, Some(PathMember::Int { .. })),
)?;
}
}
} else {
value.insert_data_at_cell_path(&cell_path.members, replacement, span)?;
}
Ok(value.into_pipeline_data_with_metadata(metadata))
}
PipelineData::ListStream(mut stream, metadata) => {
if let Some((
&PathMember::Int {
val,
span: path_span,
..
},
path,
)) = cell_path.members.split_first()
{
let mut pre_elems = vec![];
let output = eval_block(
&engine_state,
&mut stack,
&block,
input.clone().into_pipeline_data(),
redirect_stdout,
redirect_stderr,
);
for idx in 0..val {
if let Some(v) = stream.next() {
pre_elems.push(v);
} else {
return Err(ShellError::InsertAfterNextFreeIndex {
available_idx: idx,
span: path_span,
});
}
}
match output {
Ok(pd) => {
let span = pd.span().unwrap_or(span);
if let Err(e) = input.insert_data_at_cell_path(
&cell_path.members,
pd.into_value(span),
span,
) {
return Value::error(e, span);
if path.is_empty() {
if replacement.as_block().is_ok() {
let span = replacement.span();
let value = stream.next();
let end_of_stream = value.is_none();
let value = value.unwrap_or(Value::nothing(span));
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id);
let mut stack = stack.captures_to_stack(capture_block.captures);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, value.clone())
}
}
let output = eval_block(
engine_state,
&mut stack,
block,
value.clone().into_pipeline_data(),
redirect_stdout,
redirect_stderr,
)?;
pre_elems.push(output.into_value(span));
if !end_of_stream {
pre_elems.push(value);
}
} else {
pre_elems.push(replacement);
}
} else if let Some(mut value) = stream.next() {
if replacement.as_block().is_ok() {
insert_single_value_by_closure(
&mut value,
replacement,
engine_state,
stack,
redirect_stdout,
redirect_stderr,
path,
true,
)?;
} else {
value.insert_data_at_cell_path(path, replacement, span)?;
}
pre_elems.push(value)
} else {
return Err(ShellError::AccessBeyondEnd {
max_idx: pre_elems.len() - 1,
span: path_span,
});
}
Ok(pre_elems
.into_iter()
.chain(stream)
.into_pipeline_data_with_metadata(metadata, ctrlc))
} else if replacement.as_block().is_ok() {
let engine_state = engine_state.clone();
let replacement_span = replacement.span();
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id).clone();
let stack = stack.captures_to_stack(capture_block.captures.clone());
Ok(stream
.map(move |mut input| {
// Recreate the stack for each iteration to
// isolate environment variable changes, etc.
let mut stack = stack.clone();
let err = insert_value_by_closure(
&mut input,
replacement_span,
&engine_state,
&mut stack,
redirect_stdout,
redirect_stderr,
&block,
&cell_path.members,
false,
);
if let Err(e) = err {
Value::error(e, span)
} else {
input
}
Err(e) => Value::error(e, span),
}
},
ctrlc,
)
.map(|x| x.set_metadata(metadata))
} else {
if let Some(PathMember::Int { val, .. }) = cell_path.members.first() {
let mut input = input.into_iter();
let mut pre_elems = vec![];
for _ in 0..*val {
if let Some(v) = input.next() {
pre_elems.push(v);
} else {
pre_elems.push(Value::nothing(span))
}
})
.into_pipeline_data_with_metadata(metadata, ctrlc))
} else {
Ok(stream
.map(move |mut input| {
if let Err(e) = input.insert_data_at_cell_path(
&cell_path.members,
replacement.clone(),
span,
) {
Value::error(e, span)
} else {
input
}
})
.into_pipeline_data_with_metadata(metadata, ctrlc))
}
return Ok(pre_elems
.into_iter()
.chain(vec![replacement])
.chain(input)
.into_pipeline_data_with_metadata(metadata, ctrlc));
}
input
.map(
move |mut input| {
let replacement = replacement.clone();
if let Err(e) =
input.insert_data_at_cell_path(&cell_path.members, replacement, span)
{
return Value::error(e, span);
}
input
},
ctrlc,
)
.map(|x| x.set_metadata(metadata))
PipelineData::Empty => Err(ShellError::IncompatiblePathAccess {
type_name: "empty pipeline".to_string(),
span,
}),
PipelineData::ExternalStream { .. } => Err(ShellError::IncompatiblePathAccess {
type_name: "external stream".to_string(),
span,
}),
}
}
#[allow(clippy::too_many_arguments)]
fn insert_value_by_closure(
value: &mut Value,
span: Span,
engine_state: &EngineState,
stack: &mut Stack,
redirect_stdout: bool,
redirect_stderr: bool,
block: &Block,
cell_path: &[PathMember],
first_path_member_int: bool,
) -> Result<(), ShellError> {
let input_at_path = value.clone().follow_cell_path(cell_path, false);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(
*var_id,
if first_path_member_int {
input_at_path.clone().unwrap_or(Value::nothing(span))
} else {
value.clone()
},
)
}
}
let input_at_path = input_at_path
.map(IntoPipelineData::into_pipeline_data)
.unwrap_or(PipelineData::Empty);
let output = eval_block(
engine_state,
stack,
block,
input_at_path,
redirect_stdout,
redirect_stderr,
)?;
value.insert_data_at_cell_path(cell_path, output.into_value(span), span)
}
#[allow(clippy::too_many_arguments)]
fn insert_single_value_by_closure(
value: &mut Value,
replacement: Value,
engine_state: &EngineState,
stack: &mut Stack,
redirect_stdout: bool,
redirect_stderr: bool,
cell_path: &[PathMember],
first_path_member_int: bool,
) -> Result<(), ShellError> {
let span = replacement.span();
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id);
let mut stack = stack.captures_to_stack(capture_block.captures);
insert_value_by_closure(
value,
span,
engine_state,
&mut stack,
redirect_stdout,
redirect_stderr,
block,
cell_path,
first_path_member_int,
)
}
#[cfg(test)]
mod test {
use super::*;

View File

@ -1,9 +1,9 @@
use nu_engine::{eval_block, CallExt};
use nu_protocol::ast::{Call, CellPath, PathMember};
use nu_protocol::ast::{Block, Call, CellPath, PathMember};
use nu_protocol::engine::{Closure, Command, EngineState, Stack};
use nu_protocol::{
record, Category, Example, FromValue, IntoInterruptiblePipelineData, IntoPipelineData,
PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value,
};
#[derive(Clone)]
@ -63,18 +63,8 @@ impl Command for Update {
})),
},
Example {
description: "Use in closure form for more involved updating logic",
example: "[[count fruit]; [1 'apple']] | enumerate | update item.count {|e| ($e.item.fruit | str length) + $e.index } | get item",
result: Some(Value::test_list(
vec![Value::test_record(record! {
"count" => Value::test_int(5),
"fruit" => Value::test_string("apple"),
})],
)),
},
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 ','}",
description: "Use a closure to alter each value in the 'authors' column to a single string",
example: "[[project, authors]; ['nu', ['Andrés', 'JT', 'Yehuda']]] | update authors {|row| $row.authors | str join ',' }",
result: Some(Value::test_list(
vec![Value::test_record(record! {
"project" => Value::test_string("nu"),
@ -84,14 +74,28 @@ impl Command for Update {
},
Example {
description: "You can also use a simple command to update 'authors' to a single string",
example: "[[project, authors]; ['nu', ['Andrés', 'JT', 'Yehuda']]] | update authors {|| str join ','}",
example: "[[project, authors]; ['nu', ['Andrés', 'JT', 'Yehuda']]] | update authors { str join ',' }",
result: Some(Value::test_list(
vec![Value::test_record(record! {
"project" => Value::test_string("nu"),
"authors" => Value::test_string("Andrés,JT,Yehuda"),
})],
)),
}
},
Example {
description: "Update a value at an index in a list",
example: "[1 2 3] | update 1 4",
result: Some(Value::test_list(
vec![Value::test_int(1), Value::test_int(4), Value::test_int(3)]
)),
},
Example {
description: "Use a closure to compute a new value at an index",
example: "[1 2 3] | update 1 {|i| $i + 2 }",
result: Some(Value::test_list(
vec![Value::test_int(1), Value::test_int(4), Value::test_int(3)]
)),
},
]
}
}
@ -110,111 +114,222 @@ fn update(
let redirect_stdout = call.redirect_stdout;
let redirect_stderr = call.redirect_stderr;
let engine_state = engine_state.clone();
let ctrlc = engine_state.ctrlc.clone();
// Let's capture the metadata for ls_colors
let metadata = input.metadata();
let mdclone = metadata.clone();
// Replace is a block, so set it up and run it instead of using it as the replacement
if replacement.as_block().is_ok() {
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id).clone();
let mut stack = stack.captures_to_stack(capture_block.captures);
let orig_env_vars = stack.env_vars.clone();
let orig_env_hidden = stack.env_hidden.clone();
Ok(input
.map(
move |mut input| {
// 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.
stack.with_env(&orig_env_vars, &orig_env_hidden);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, input.clone())
match input {
PipelineData::Value(mut value, metadata) => {
if replacement.as_block().is_ok() {
match (cell_path.members.first(), &mut value) {
(Some(PathMember::String { .. }), Value::List { vals, .. }) => {
let span = replacement.span();
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id);
let stack = stack.captures_to_stack(capture_block.captures.clone());
for val in vals {
let mut stack = stack.clone();
update_value_by_closure(
val,
span,
engine_state,
&mut stack,
redirect_stdout,
redirect_stderr,
block,
&cell_path.members,
false,
)?;
}
}
(first, _) => {
update_single_value_by_closure(
&mut value,
replacement,
engine_state,
stack,
redirect_stdout,
redirect_stderr,
&cell_path.members,
matches!(first, Some(PathMember::Int { .. })),
)?;
}
}
} else {
value.update_data_at_cell_path(&cell_path.members, replacement)?;
}
Ok(value.into_pipeline_data_with_metadata(metadata))
}
PipelineData::ListStream(mut stream, metadata) => {
if let Some((
&PathMember::Int {
val,
span: path_span,
..
},
path,
)) = cell_path.members.split_first()
{
let mut pre_elems = vec![];
let input_at_path =
match input.clone().follow_cell_path(&cell_path.members, false) {
Err(e) => return Value::error(e, span),
Ok(v) => v,
};
let output = eval_block(
&engine_state,
&mut stack,
&block,
input_at_path.into_pipeline_data_with_metadata(metadata.clone()),
for idx in 0..=val {
if let Some(v) = stream.next() {
pre_elems.push(v);
} else if idx == 0 {
return Err(ShellError::AccessEmptyContent { span: path_span });
} else {
return Err(ShellError::AccessBeyondEnd {
max_idx: idx - 1,
span: path_span,
});
}
}
// cannot fail since loop above does at least one iteration or returns an error
let value = pre_elems.last_mut().expect("one element");
if replacement.as_block().is_ok() {
update_single_value_by_closure(
value,
replacement,
engine_state,
stack,
redirect_stdout,
redirect_stderr,
);
path,
true,
)?;
} else {
value.update_data_at_cell_path(path, replacement)?;
}
match output {
Ok(pd) => {
if let Err(e) = input
.update_data_at_cell_path(&cell_path.members, pd.into_value(span))
{
return Value::error(e, span);
}
Ok(pre_elems
.into_iter()
.chain(stream)
.into_pipeline_data_with_metadata(metadata, ctrlc))
} else if replacement.as_block().is_ok() {
let replacement_span = replacement.span();
let engine_state = engine_state.clone();
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id).clone();
let stack = stack.captures_to_stack(capture_block.captures.clone());
Ok(stream
.map(move |mut input| {
// Recreate the stack for each iteration to
// isolate environment variable changes, etc.
let mut stack = stack.clone();
let err = update_value_by_closure(
&mut input,
replacement_span,
&engine_state,
&mut stack,
redirect_stdout,
redirect_stderr,
&block,
&cell_path.members,
false,
);
if let Err(e) = err {
Value::error(e, span)
} else {
input
}
Err(e) => Value::error(e, span),
}
},
ctrlc,
)?
.set_metadata(mdclone))
} else {
if let Some(PathMember::Int { val, span, .. }) = cell_path.members.first() {
let mut input = input.into_iter();
let mut pre_elems = vec![];
for idx in 0..*val {
if let Some(v) = input.next() {
pre_elems.push(v);
} else if idx == 0 {
return Err(ShellError::AccessEmptyContent { span: *span });
} else {
return Err(ShellError::AccessBeyondEnd {
max_idx: idx - 1,
span: *span,
});
}
})
.into_pipeline_data_with_metadata(metadata, ctrlc))
} else {
Ok(stream
.map(move |mut input| {
if let Err(e) =
input.update_data_at_cell_path(&cell_path.members, replacement.clone())
{
Value::error(e, span)
} else {
input
}
})
.into_pipeline_data_with_metadata(metadata, ctrlc))
}
// Skip over the replaced value
let _ = input.next();
return Ok(pre_elems
.into_iter()
.chain(vec![replacement])
.chain(input)
.into_pipeline_data_with_metadata(metadata, ctrlc));
}
Ok(input
.map(
move |mut input| {
let replacement = replacement.clone();
if let Err(e) = input.update_data_at_cell_path(&cell_path.members, replacement)
{
return Value::error(e, span);
}
input
},
ctrlc,
)?
.set_metadata(metadata))
PipelineData::Empty => Err(ShellError::IncompatiblePathAccess {
type_name: "empty pipeline".to_string(),
span,
}),
PipelineData::ExternalStream { .. } => Err(ShellError::IncompatiblePathAccess {
type_name: "external stream".to_string(),
span,
}),
}
}
#[allow(clippy::too_many_arguments)]
fn update_value_by_closure(
value: &mut Value,
span: Span,
engine_state: &EngineState,
stack: &mut Stack,
redirect_stdout: bool,
redirect_stderr: bool,
block: &Block,
cell_path: &[PathMember],
first_path_member_int: bool,
) -> Result<(), ShellError> {
let input_at_path = value.clone().follow_cell_path(cell_path, false)?;
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(
*var_id,
if first_path_member_int {
input_at_path.clone()
} else {
value.clone()
},
)
}
}
let output = eval_block(
engine_state,
stack,
block,
input_at_path.into_pipeline_data(),
redirect_stdout,
redirect_stderr,
)?;
value.update_data_at_cell_path(cell_path, output.into_value(span))
}
#[allow(clippy::too_many_arguments)]
fn update_single_value_by_closure(
value: &mut Value,
replacement: Value,
engine_state: &EngineState,
stack: &mut Stack,
redirect_stdout: bool,
redirect_stderr: bool,
cell_path: &[PathMember],
first_path_member_int: bool,
) -> Result<(), ShellError> {
let span = replacement.span();
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id);
let mut stack = stack.captures_to_stack(capture_block.captures);
update_value_by_closure(
value,
span,
engine_state,
&mut stack,
redirect_stdout,
redirect_stderr,
block,
cell_path,
first_path_member_int,
)
}
#[cfg(test)]
mod test {
use super::*;

View File

@ -1,9 +1,9 @@
use nu_engine::{eval_block, CallExt};
use nu_protocol::ast::{Call, CellPath, PathMember};
use nu_protocol::ast::{Block, Call, CellPath, PathMember};
use nu_protocol::engine::{Closure, Command, EngineState, Stack};
use nu_protocol::{
record, Category, Example, FromValue, IntoInterruptiblePipelineData, IntoPipelineData,
PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value,
};
#[derive(Clone)]
@ -57,19 +57,28 @@ impl Command for Upsert {
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Update a record's value",
example: "{'name': 'nu', 'stars': 5} | upsert name 'Nushell'",
result: Some(Value::test_record(record! {
"name" => Value::test_string("Nushell"),
"stars" => Value::test_int(5),
})),
},
Example {
description: "Update each row of a table",
example: "[[name lang]; [Nushell ''] [Reedline '']] | upsert lang 'Rust'",
result: Some(Value::test_list(
vec![
vec![
Example {
description: "Update a record's value",
example: "{'name': 'nu', 'stars': 5} | upsert name 'Nushell'",
result: Some(Value::test_record(record! {
"name" => Value::test_string("Nushell"),
"stars" => Value::test_int(5),
})),
},
Example {
description: "Insert a new entry into a record",
example: "{'name': 'nu', 'stars': 5} | upsert language 'Rust'",
result: Some(Value::test_record(record! {
"name" => Value::test_string("nu"),
"stars" => Value::test_int(5),
"language" => Value::test_string("Rust"),
})),
},
Example {
description: "Update each row of a table",
example: "[[name lang]; [Nushell ''] [Reedline '']] | upsert lang 'Rust'",
result: Some(Value::test_list(vec![
Value::test_record(record! {
"name" => Value::test_string("Nushell"),
"lang" => Value::test_string("Rust"),
@ -78,46 +87,45 @@ impl Command for Upsert {
"name" => Value::test_string("Reedline"),
"lang" => Value::test_string("Rust"),
}),
],
)),
},
Example {
description: "Insert a new entry into a single record",
example: "{'name': 'nu', 'stars': 5} | upsert language 'Rust'",
result: Some(Value::test_record(record! {
"name" => Value::test_string("nu"),
"stars" => Value::test_int(5),
"language" => Value::test_string("Rust"),
})),
}, Example {
description: "Use in closure form for more involved updating logic",
example: "[[count fruit]; [1 'apple']] | enumerate | upsert item.count {|e| ($e.item.fruit | str length) + $e.index } | get item",
result: Some(Value::test_list(
vec![Value::test_record(record! {
"count" => Value::test_int(5),
"fruit" => Value::test_string("apple"),
})],
)),
},
Example {
description: "Upsert an int into a list, updating an existing value based on the index",
example: "[1 2 3] | upsert 0 2",
result: Some(Value::test_list(
vec![Value::test_int(2), Value::test_int(2), Value::test_int(3)],
)),
},
Example {
description: "Upsert an int into a list, inserting a new value based on the index",
example: "[1 2 3] | upsert 3 4",
result: Some(Value::test_list(
vec![
])),
},
Example {
description: "Insert a new column with values computed based off the other columns",
example: "[[foo]; [7] [8] [9]] | upsert bar {|row| $row.foo * 2 }",
result: Some(Value::test_list(vec![
Value::test_record(record! {
"foo" => Value::test_int(7),
"bar" => Value::test_int(14),
}),
Value::test_record(record! {
"foo" => Value::test_int(8),
"bar" => Value::test_int(16),
}),
Value::test_record(record! {
"foo" => Value::test_int(9),
"bar" => Value::test_int(18),
}),
])),
},
Example {
description: "Upsert into a list, updating an existing value at an index",
example: "[1 2 3] | upsert 0 2",
result: Some(Value::test_list(vec![
Value::test_int(2),
Value::test_int(2),
Value::test_int(3),
])),
},
Example {
description: "Upsert into a list, inserting a new value at the end",
example: "[1 2 3] | upsert 3 4",
result: Some(Value::test_list(vec![
Value::test_int(1),
Value::test_int(2),
Value::test_int(3),
Value::test_int(4),
],
)),
},
])),
},
]
}
}
@ -128,7 +136,6 @@ fn upsert(
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let metadata = input.metadata();
let span = call.head;
let cell_path: CellPath = call.req(engine_state, stack, 0)?;
@ -137,101 +144,256 @@ fn upsert(
let redirect_stdout = call.redirect_stdout;
let redirect_stderr = call.redirect_stderr;
let engine_state = engine_state.clone();
let ctrlc = engine_state.ctrlc.clone();
// Replace is a block, so set it up and run it instead of using it as the replacement
if replacement.as_block().is_ok() {
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id).clone();
let mut stack = stack.captures_to_stack(capture_block.captures);
let orig_env_vars = stack.env_vars.clone();
let orig_env_hidden = stack.env_hidden.clone();
input
.map(
move |mut input| {
// 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.
stack.with_env(&orig_env_vars, &orig_env_hidden);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, input.clone())
match input {
PipelineData::Value(mut value, metadata) => {
if replacement.as_block().is_ok() {
match (cell_path.members.first(), &mut value) {
(Some(PathMember::String { .. }), Value::List { vals, .. }) => {
let span = replacement.span();
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id);
let stack = stack.captures_to_stack(capture_block.captures.clone());
for val in vals {
let mut stack = stack.clone();
upsert_value_by_closure(
val,
span,
engine_state,
&mut stack,
redirect_stdout,
redirect_stderr,
block,
&cell_path.members,
false,
)?;
}
}
let output = eval_block(
&engine_state,
&mut stack,
&block,
input.clone().into_pipeline_data(),
redirect_stdout,
redirect_stderr,
);
match output {
Ok(pd) => {
if let Err(e) = input
.upsert_data_at_cell_path(&cell_path.members, pd.into_value(span))
{
return Value::error(e, span);
}
input
}
Err(e) => Value::error(e, span),
(first, _) => {
upsert_single_value_by_closure(
&mut value,
replacement,
engine_state,
stack,
redirect_stdout,
redirect_stderr,
&cell_path.members,
matches!(first, Some(PathMember::Int { .. })),
)?;
}
}
} else {
value.upsert_data_at_cell_path(&cell_path.members, replacement)?;
}
Ok(value.into_pipeline_data_with_metadata(metadata))
}
PipelineData::ListStream(mut stream, metadata) => {
if let Some((
&PathMember::Int {
val,
span: path_span,
..
},
ctrlc,
)
.map(|x| x.set_metadata(metadata))
} else {
if let Some(PathMember::Int { val, span, .. }) = cell_path.members.first() {
let mut input = input.into_iter();
let mut pre_elems = vec![];
path,
)) = cell_path.members.split_first()
{
let mut pre_elems = vec![];
for idx in 0..*val {
if let Some(v) = input.next() {
pre_elems.push(v);
for idx in 0..val {
if let Some(v) = stream.next() {
pre_elems.push(v);
} else {
return Err(ShellError::InsertAfterNextFreeIndex {
available_idx: idx,
span: path_span,
});
}
}
if path.is_empty() {
let span = replacement.span();
let value = stream.next().unwrap_or(Value::nothing(span));
if replacement.as_block().is_ok() {
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id);
let mut stack = stack.captures_to_stack(capture_block.captures);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(*var_id, value.clone())
}
}
let output = eval_block(
engine_state,
&mut stack,
block,
value.clone().into_pipeline_data(),
redirect_stdout,
redirect_stderr,
)?;
pre_elems.push(output.into_value(span));
} else {
pre_elems.push(replacement);
}
} else if let Some(mut value) = stream.next() {
if replacement.as_block().is_ok() {
upsert_single_value_by_closure(
&mut value,
replacement,
engine_state,
stack,
redirect_stdout,
redirect_stderr,
path,
true,
)?;
} else {
value.upsert_data_at_cell_path(path, replacement)?;
}
pre_elems.push(value)
} else {
return Err(ShellError::AccessBeyondEnd {
max_idx: idx,
span: *span,
max_idx: pre_elems.len() - 1,
span: path_span,
});
}
Ok(pre_elems
.into_iter()
.chain(stream)
.into_pipeline_data_with_metadata(metadata, ctrlc))
} else if replacement.as_block().is_ok() {
let engine_state = engine_state.clone();
let replacement_span = replacement.span();
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id).clone();
let stack = stack.captures_to_stack(capture_block.captures.clone());
Ok(stream
.map(move |mut input| {
// Recreate the stack for each iteration to
// isolate environment variable changes, etc.
let mut stack = stack.clone();
let err = upsert_value_by_closure(
&mut input,
replacement_span,
&engine_state,
&mut stack,
redirect_stdout,
redirect_stderr,
&block,
&cell_path.members,
false,
);
if let Err(e) = err {
Value::error(e, span)
} else {
input
}
})
.into_pipeline_data_with_metadata(metadata, ctrlc))
} else {
Ok(stream
.map(move |mut input| {
if let Err(e) =
input.upsert_data_at_cell_path(&cell_path.members, replacement.clone())
{
Value::error(e, span)
} else {
input
}
})
.into_pipeline_data_with_metadata(metadata, ctrlc))
}
// Skip over the replaced value
let _ = input.next();
return Ok(pre_elems
.into_iter()
.chain(vec![replacement])
.chain(input)
.into_pipeline_data_with_metadata(metadata, ctrlc));
}
input
.map(
move |mut input| {
let replacement = replacement.clone();
if let Err(e) = input.upsert_data_at_cell_path(&cell_path.members, replacement)
{
return Value::error(e, span);
}
input
},
ctrlc,
)
.map(|x| x.set_metadata(metadata))
PipelineData::Empty => Err(ShellError::IncompatiblePathAccess {
type_name: "empty pipeline".to_string(),
span,
}),
PipelineData::ExternalStream { .. } => Err(ShellError::IncompatiblePathAccess {
type_name: "external stream".to_string(),
span,
}),
}
}
#[allow(clippy::too_many_arguments)]
fn upsert_value_by_closure(
value: &mut Value,
span: Span,
engine_state: &EngineState,
stack: &mut Stack,
redirect_stdout: bool,
redirect_stderr: bool,
block: &Block,
cell_path: &[PathMember],
first_path_member_int: bool,
) -> Result<(), ShellError> {
let input_at_path = value.clone().follow_cell_path(cell_path, false);
if let Some(var) = block.signature.get_positional(0) {
if let Some(var_id) = &var.var_id {
stack.add_var(
*var_id,
if first_path_member_int {
input_at_path.clone().unwrap_or(Value::nothing(span))
} else {
value.clone()
},
)
}
}
let input_at_path = input_at_path
.map(IntoPipelineData::into_pipeline_data)
.unwrap_or(PipelineData::Empty);
let output = eval_block(
engine_state,
stack,
block,
input_at_path,
redirect_stdout,
redirect_stderr,
)?;
value.upsert_data_at_cell_path(cell_path, output.into_value(span))
}
#[allow(clippy::too_many_arguments)]
fn upsert_single_value_by_closure(
value: &mut Value,
replacement: Value,
engine_state: &EngineState,
stack: &mut Stack,
redirect_stdout: bool,
redirect_stderr: bool,
cell_path: &[PathMember],
first_path_member_int: bool,
) -> Result<(), ShellError> {
let span = replacement.span();
let capture_block = Closure::from_value(replacement)?;
let block = engine_state.get_block(capture_block.block_id);
let mut stack = stack.captures_to_stack(capture_block.captures);
upsert_value_by_closure(
value,
span,
engine_state,
&mut stack,
redirect_stdout,
redirect_stderr,
block,
cell_path,
first_path_member_int,
)
}
#[cfg(test)]
mod test {
use super::*;