Skip to main content

nu_command/filters/
update.rs

1use nu_engine::{ClosureEval, ClosureEvalOnce, command_prelude::*};
2use nu_protocol::ast::PathMember;
3
4#[derive(Clone)]
5pub struct Update;
6
7impl Command for Update {
8    fn name(&self) -> &str {
9        "update"
10    }
11
12    fn signature(&self) -> Signature {
13        Signature::build("update")
14            .input_output_types(vec![
15                (Type::record(), Type::record()),
16                (Type::table(), Type::table()),
17                (
18                    Type::List(Box::new(Type::Any)),
19                    Type::List(Box::new(Type::Any)),
20                ),
21                (Type::Custom("matrix".into()), Type::Custom("matrix".into())),
22            ])
23            .required(
24                "field",
25                SyntaxShape::CellPath,
26                "The name of the column to update.",
27            )
28            .required(
29                "replacement value",
30                SyntaxShape::Any,
31                "The new value to give the cell(s), or a closure to create the value.",
32            )
33            .allow_variants_without_examples(true)
34            .category(Category::Filters)
35    }
36
37    fn description(&self) -> &str {
38        "Update an existing column to have a new value."
39    }
40
41    fn extra_description(&self) -> &str {
42        "When updating a column, the closure will be run for each row, and the current row will be passed as the first argument. \
43Referencing `$in` inside the closure will provide the value at the column for the current row.
44
45When updating a specific index, the closure will instead be run once. The first argument to the closure and the `$in` value will both be the current value at the index."
46    }
47
48    fn run(
49        &self,
50        engine_state: &EngineState,
51        stack: &mut Stack,
52        call: &Call,
53        input: PipelineData,
54    ) -> Result<PipelineData, ShellError> {
55        update(engine_state, stack, call, input)
56    }
57
58    fn examples(&self) -> Vec<Example<'_>> {
59        vec![
60            Example {
61                description: "Update a column value.",
62                example: "{'name': 'nu', 'stars': 5} | update name 'Nushell'",
63                result: Some(Value::test_record(record! {
64                    "name" =>  Value::test_string("Nushell"),
65                    "stars" => Value::test_int(5),
66                })),
67            },
68            Example {
69                description: "Use a closure to alter each value in the 'authors' column to a single string.",
70                example: "[[project, authors]; ['nu', ['Andrés', 'JT', 'Yehuda']]] | update authors {|row| $row.authors | str join ',' }",
71                result: Some(Value::test_list(vec![Value::test_record(record! {
72                    "project" => Value::test_string("nu"),
73                    "authors" => Value::test_string("Andrés,JT,Yehuda"),
74                })])),
75            },
76            Example {
77                description: "Implicitly use the `$in` value in a closure to update 'authors'.",
78                example: "[[project, authors]; ['nu', ['Andrés', 'JT', 'Yehuda']]] | update authors { str join ',' }",
79                result: Some(Value::test_list(vec![Value::test_record(record! {
80                    "project" => Value::test_string("nu"),
81                    "authors" => Value::test_string("Andrés,JT,Yehuda"),
82                })])),
83            },
84            Example {
85                description: "Update a value at an index in a list.",
86                example: "[1 2 3] | update 1 4",
87                result: Some(Value::test_list(vec![
88                    Value::test_int(1),
89                    Value::test_int(4),
90                    Value::test_int(3),
91                ])),
92            },
93            Example {
94                description: "Use a closure to compute a new value at an index.",
95                example: "[1 2 3] | update 1 {|i| $i + 2 }",
96                result: Some(Value::test_list(vec![
97                    Value::test_int(1),
98                    Value::test_int(4),
99                    Value::test_int(3),
100                ])),
101            },
102            Example {
103                description: "Update a row in a matrix",
104                example: "[[1 2] [3 4]] | into matrix | update 0 [5 6] | matrix into-nu | to nuon",
105                result: Some(Value::test_string("[[5.0, 6.0], [3.0, 4.0]]")),
106            },
107        ]
108    }
109}
110
111fn update_recursive(
112    engine_state: &EngineState,
113    stack: &mut Stack,
114    head_span: Span,
115    replacement: Value,
116    input: PipelineData,
117    cell_paths: &[PathMember],
118) -> Result<PipelineData, ShellError> {
119    match input {
120        PipelineData::Value(mut value, metadata) => {
121            if let Value::Closure { val, .. } = replacement {
122                if matches!(cell_paths.first(), Some(PathMember::Int { .. })) {
123                    update_single_value_by_closure(
124                        &mut value,
125                        ClosureEvalOnce::new(engine_state, stack, *val),
126                        head_span,
127                        cell_paths,
128                        false,
129                    )?;
130                } else {
131                    let mut closure = ClosureEval::new(engine_state, stack, *val);
132                    update_value_by_closure(&mut value, &mut closure, head_span, cell_paths)?;
133                }
134            } else {
135                value.update_data_at_cell_path(cell_paths, replacement)?;
136            }
137            Ok(value.into_pipeline_data_with_metadata(metadata))
138        }
139        PipelineData::ListStream(stream, metadata) => {
140            if let Some((
141                &PathMember::Int {
142                    val,
143                    span: path_span,
144                    optional,
145                },
146                path,
147            )) = cell_paths.split_first()
148            {
149                let mut stream = stream.into_iter();
150                let mut pre_elems = vec![];
151
152                for idx in 0..=val {
153                    if let Some(v) = stream.next() {
154                        pre_elems.push(v);
155                    } else if optional {
156                        return Ok(pre_elems
157                            .into_iter()
158                            .chain(stream)
159                            .into_pipeline_data_with_metadata(
160                                head_span,
161                                engine_state.signals().clone(),
162                                metadata,
163                            ));
164                    } else if idx == 0 {
165                        return Err(ShellError::AccessEmptyContent { span: path_span });
166                    } else {
167                        return Err(ShellError::AccessBeyondEnd {
168                            max_idx: idx - 1,
169                            span: path_span,
170                        });
171                    }
172                }
173
174                // cannot fail since loop above does at least one iteration or returns an error
175                let value = pre_elems.last_mut().expect("one element");
176
177                if let Value::Closure { val, .. } = replacement {
178                    update_single_value_by_closure(
179                        value,
180                        ClosureEvalOnce::new(engine_state, stack, *val),
181                        head_span,
182                        path,
183                        true,
184                    )?;
185                } else {
186                    value.update_data_at_cell_path(path, replacement)?;
187                }
188
189                Ok(pre_elems
190                    .into_iter()
191                    .chain(stream)
192                    .into_pipeline_data_with_metadata(
193                        head_span,
194                        engine_state.signals().clone(),
195                        metadata,
196                    ))
197            } else if let Some(new_cell_paths) = Value::try_put_int_path_member_on_top(cell_paths) {
198                update_recursive(
199                    engine_state,
200                    stack,
201                    head_span,
202                    replacement,
203                    PipelineData::ListStream(stream, metadata),
204                    &new_cell_paths,
205                )
206            } else if let Value::Closure { val, .. } = replacement {
207                let mut closure = ClosureEval::new(engine_state, stack, *val);
208                let cell_paths = cell_paths.to_vec();
209                let stream = stream.map(move |mut value| {
210                    let err =
211                        update_value_by_closure(&mut value, &mut closure, head_span, &cell_paths);
212
213                    if let Err(e) = err {
214                        Value::error(e, head_span)
215                    } else {
216                        value
217                    }
218                });
219                Ok(PipelineData::list_stream(stream, metadata))
220            } else {
221                let cell_paths = cell_paths.to_vec();
222                let stream = stream.map(move |mut value| {
223                    if let Err(e) = value.update_data_at_cell_path(&cell_paths, replacement.clone())
224                    {
225                        Value::error(e, head_span)
226                    } else {
227                        value
228                    }
229                });
230
231                Ok(PipelineData::list_stream(stream, metadata))
232            }
233        }
234        PipelineData::Empty => Err(ShellError::IncompatiblePathAccess {
235            type_name: "empty pipeline".to_string(),
236            span: head_span,
237        }),
238        PipelineData::ByteStream(stream, ..) => Err(ShellError::IncompatiblePathAccess {
239            type_name: stream.type_().describe().into(),
240            span: head_span,
241        }),
242    }
243}
244
245fn update(
246    engine_state: &EngineState,
247    stack: &mut Stack,
248    call: &Call,
249    input: PipelineData,
250) -> Result<PipelineData, ShellError> {
251    let head = call.head;
252    let cell_path: CellPath = call.req(engine_state, stack, 0)?;
253    let replacement: Value = call.req(engine_state, stack, 1)?;
254    let is_custom = matches!(&input, PipelineData::Value(Value::Custom { .. }, _));
255    let input = if is_custom {
256        input
257    } else {
258        input.into_stream_or_original(engine_state)
259    };
260
261    update_recursive(
262        engine_state,
263        stack,
264        head,
265        replacement,
266        input,
267        &cell_path.members,
268    )
269}
270
271/// Applies `closure` to every leaf cell reached by following `cell_path` inside `value`.
272///
273/// Captures the initial value as the row context before descending, then delegates to
274/// [`update_value_by_closure_recursive`] for the actual traversal.
275fn update_value_by_closure(
276    value: &mut Value,
277    closure: &mut ClosureEval,
278    span: Span,
279    cell_path: &[PathMember],
280) -> Result<(), ShellError> {
281    let row_value = value.clone();
282    update_value_by_closure_recursive(value, &row_value, closure, span, cell_path)
283}
284
285/// Recursively walks `cell_path` inside `value`, applying `closure` at every leaf cell.
286///
287/// # Parameters
288/// * `value` - The current cell being traversed. At the leaf it is overwritten with the closure result.
289/// * `row_value` - The enclosing *row* passed as the positional argument (`|row|` / `$it`)
290///   to the closure. When descending through a list-of-records this is updated to each
291///   individual nested record, so the closure always receives the most-immediately-enclosing
292///   row rather than a projected list of column values.
293/// * `closure` - The user-supplied closure, evaluated once per leaf cell.
294/// * `span` - Span used when converting the closure output back to a [`Value`].
295/// * `cell_path` - Remaining path members to traverse.
296fn update_value_by_closure_recursive(
297    value: &mut Value,
298    row_value: &Value,
299    closure: &mut ClosureEval,
300    span: Span,
301    cell_path: &[PathMember],
302) -> Result<(), ShellError> {
303    // Base case: no more path to traverse — evaluate the closure at this leaf cell.
304    // `$in` receives the current cell value; the optional positional arg receives the row.
305    let Some((member, path)) = cell_path.split_first() else {
306        let new_value = closure
307            .add_arg(row_value.clone())?
308            .run_with_input(value.clone().into_pipeline_data())?
309            .into_value(span)?;
310        *value = new_value;
311        return Ok(());
312    };
313
314    let v_span = value.span();
315
316    match member {
317        PathMember::String {
318            val: col_name,
319            span: path_span,
320            casing,
321            optional,
322        } => {
323            // Copy the span once per arm so all inner branches share the same value.
324            let path_span = Span::new(path_span.start, path_span.end);
325            match value {
326                Value::List { vals, .. } => {
327                    for val in vals.to_mut() {
328                        // Each nested record becomes the new row context so that `|row|`
329                        // refers to the immediately-enclosing record, not the outer row.
330                        let row_context = val.clone();
331                        let val_span = val.span();
332
333                        match val {
334                            Value::Record { val: record, .. } => {
335                                if let Some(cell) =
336                                    record.to_mut().cased_mut(*casing).get_mut(col_name)
337                                {
338                                    update_value_by_closure_recursive(
339                                        cell,
340                                        &row_context,
341                                        closure,
342                                        path_span,
343                                        path,
344                                    )?;
345                                } else if !*optional {
346                                    return Err(ShellError::CantFindColumn {
347                                        col_name: col_name.clone(),
348                                        span: Some(path_span),
349                                        src_span: val_span,
350                                    });
351                                }
352                            }
353                            Value::Error { error, .. } => return Err(*error.clone()),
354                            _ => {
355                                if !*optional {
356                                    return Err(ShellError::CantFindColumn {
357                                        col_name: col_name.clone(),
358                                        span: Some(path_span),
359                                        src_span: val_span,
360                                    });
361                                }
362                            }
363                        }
364                    }
365                }
366                Value::Record { val: record, .. } => {
367                    if let Some(cell) = record.to_mut().cased_mut(*casing).get_mut(col_name) {
368                        update_value_by_closure_recursive(
369                            cell, row_value, closure, path_span, path,
370                        )?;
371                    } else if !*optional {
372                        return Err(ShellError::CantFindColumn {
373                            col_name: col_name.clone(),
374                            span: Some(path_span),
375                            src_span: v_span,
376                        });
377                    }
378                }
379                Value::Error { error, .. } => return Err(*error.clone()),
380                v => {
381                    if !*optional {
382                        return Err(ShellError::CantFindColumn {
383                            col_name: col_name.clone(),
384                            span: Some(path_span),
385                            src_span: v.span(),
386                        });
387                    }
388                }
389            }
390        }
391        PathMember::Int {
392            val: row_num,
393            span: path_span,
394            optional,
395        } => {
396            let path_span = Span::new(path_span.start, path_span.end);
397            match value {
398                Value::List { vals, .. } => {
399                    if *row_num < vals.len() {
400                        let cell = &mut vals.to_mut()[*row_num];
401                        update_value_by_closure_recursive(
402                            cell, row_value, closure, path_span, path,
403                        )?;
404                    } else if !*optional {
405                        if vals.is_empty() {
406                            return Err(ShellError::AccessEmptyContent { span: path_span });
407                        }
408
409                        return Err(ShellError::AccessBeyondEnd {
410                            max_idx: vals.len() - 1,
411                            span: path_span,
412                        });
413                    }
414                }
415                Value::Error { error, .. } => return Err(*error.clone()),
416                v => {
417                    return Err(ShellError::NotAList {
418                        dst_span: path_span,
419                        src_span: v.span(),
420                    });
421                }
422            }
423        }
424    }
425
426    Ok(())
427}
428
429fn update_single_value_by_closure(
430    value: &mut Value,
431    closure: ClosureEvalOnce,
432    span: Span,
433    cell_path: &[PathMember],
434    cell_value_as_arg: bool,
435) -> Result<(), ShellError> {
436    let value_at_path = value.follow_cell_path(cell_path)?;
437
438    // Don't run the closure for optional paths that don't exist
439    let is_optional = cell_path.iter().any(|member| match member {
440        PathMember::String { optional, .. } => *optional,
441        PathMember::Int { optional, .. } => *optional,
442    });
443    if is_optional && matches!(value_at_path.as_ref(), Value::Nothing { .. }) {
444        return Ok(());
445    }
446
447    // FIXME: this leads to inconsistent behaviors between
448    // `{a: b} | update a {|x| print $x}` and
449    // `[{a: b}] | update 0.a {|x| print $x}`
450    let arg = if cell_value_as_arg {
451        value_at_path.as_ref()
452    } else {
453        &*value
454    };
455
456    let new_value = closure
457        .add_arg(arg.clone())?
458        .run_with_input(value_at_path.into_owned().into_pipeline_data())?
459        .into_value(span)?;
460
461    value.update_data_at_cell_path(cell_path, new_value)
462}
463
464#[cfg(test)]
465mod test {
466    use super::*;
467
468    #[test]
469    fn test_examples() -> nu_test_support::Result {
470        nu_test_support::test().examples(Update)
471    }
472}