Skip to main content

nu_command/filters/
default.rs

1use std::{borrow::Cow, ops::Deref};
2
3use nu_engine::{ClosureEval, command_prelude::*};
4use nu_protocol::{
5    ListStream, ReportMode, ShellWarning, Signals,
6    ast::{Expr, Expression},
7    report_shell_warning,
8};
9
10#[derive(Clone)]
11pub struct Default;
12
13impl Command for Default {
14    fn name(&self) -> &str {
15        "default"
16    }
17
18    fn signature(&self) -> Signature {
19        Signature::build("default")
20            // TODO: Give more specific type signature?
21            // TODO: Declare usage of cell paths in signature? (It seems to behave as if it uses cell paths)
22            .input_output_types(vec![(Type::Any, Type::Any)])
23            .required(
24                "default value",
25                SyntaxShape::Any,
26                "The value to use as a default.",
27            )
28            .rest(
29                "column name",
30                SyntaxShape::String,
31                "The name of the column.",
32            )
33            .switch(
34                "empty",
35                "Also replace empty items like \"\", {}, and [].",
36                Some('e'),
37            )
38            .category(Category::Filters)
39    }
40
41    // FIXME remove once deprecation warning is no longer needed
42    fn requires_ast_for_arguments(&self) -> bool {
43        true
44    }
45
46    fn description(&self) -> &str {
47        "Sets a default value if a row's column is missing or null."
48    }
49
50    fn run(
51        &self,
52        engine_state: &EngineState,
53        stack: &mut Stack,
54        call: &Call,
55        input: PipelineData,
56    ) -> Result<PipelineData, ShellError> {
57        let default_value: Value = call.req(engine_state, stack, 0)?;
58        let columns: Vec<String> = call.rest(engine_state, stack, 1)?;
59        let empty = call.has_flag(engine_state, stack, "empty")?;
60
61        // FIXME for deprecation of closure passed via variable
62        let default_value_expr = call.positional_nth(stack, 0);
63        let default_value =
64            DefaultValue::new(engine_state, stack, default_value, default_value_expr);
65
66        default(
67            engine_state,
68            call,
69            input,
70            default_value,
71            empty,
72            columns,
73            engine_state.signals(),
74        )
75    }
76
77    fn examples(&self) -> Vec<Example<'_>> {
78        vec![
79            Example {
80                description: "Give a default 'target' column to all file entries",
81                example: "ls -la | default 'nothing' target ",
82                result: None,
83            },
84            Example {
85                description: "Get the env value of `MY_ENV` with a default value 'abc' if not present",
86                example: "$env | get --optional MY_ENV | default 'abc'",
87                result: Some(Value::test_string("abc")),
88            },
89            Example {
90                description: "Replace the `null` value in a list",
91                example: "[1, 2, null, 4] | each { default 3 }",
92                result: Some(Value::list(
93                    vec![
94                        Value::test_int(1),
95                        Value::test_int(2),
96                        Value::test_int(3),
97                        Value::test_int(4),
98                    ],
99                    Span::test_data(),
100                )),
101            },
102            Example {
103                description: r#"Replace the missing value in the "a" column of a list"#,
104                example: "[{a:1 b:2} {b:1}] | default 'N/A' a",
105                result: Some(Value::test_list(vec![
106                    Value::test_record(record! {
107                        "a" => Value::test_int(1),
108                        "b" => Value::test_int(2),
109                    }),
110                    Value::test_record(record! {
111                        "a" => Value::test_string("N/A"),
112                        "b" => Value::test_int(1),
113                    }),
114                ])),
115            },
116            Example {
117                description: r#"Replace the empty string in the "a" column of a list"#,
118                example: "[{a:1 b:2} {a:'' b:1}] | default -e 'N/A' a",
119                result: Some(Value::test_list(vec![
120                    Value::test_record(record! {
121                        "a" => Value::test_int(1),
122                        "b" => Value::test_int(2),
123                    }),
124                    Value::test_record(record! {
125                        "a" => Value::test_string("N/A"),
126                        "b" => Value::test_int(1),
127                    }),
128                ])),
129            },
130            Example {
131                description: "Generate a default value from a closure",
132                example: "null | default { 1 + 2 }",
133                result: Some(Value::test_int(3)),
134            },
135            Example {
136                description: "Fill missing column values based on other columns",
137                example: "[{a:1 b:2} {b:1}] | upsert a {|rc| default { $rc.b + 1 } }",
138                result: Some(Value::test_list(vec![
139                    Value::test_record(record! {
140                        "a" => Value::test_int(1),
141                        "b" => Value::test_int(2),
142                    }),
143                    Value::test_record(record! {
144                        "a" => Value::test_int(2),
145                        "b" => Value::test_int(1),
146                    }),
147                ])),
148            },
149        ]
150    }
151}
152
153fn default(
154    engine_state: &EngineState,
155    call: &Call,
156    input: PipelineData,
157    mut default_value: DefaultValue,
158    default_when_empty: bool,
159    columns: Vec<String>,
160    signals: &Signals,
161) -> Result<PipelineData, ShellError> {
162    let mut input = if !columns.is_empty() {
163        input.into_stream_or_original(engine_state)
164    } else {
165        input
166    };
167
168    let input_span = input.span().unwrap_or(call.head);
169    let metadata = input.take_metadata();
170
171    // If user supplies columns, check if input is a record or list of records
172    // and set the default value for the specified record columns
173    if !columns.is_empty() {
174        if let PipelineData::Value(Value::Record { .. }, _) = input {
175            let record = input.into_value(input_span)?.into_record()?;
176            fill_record(
177                record,
178                input_span,
179                &mut default_value,
180                columns.as_slice(),
181                default_when_empty,
182            )
183            .map(|x| x.into_pipeline_data_with_metadata(metadata))
184        } else if matches!(
185            input,
186            PipelineData::ListStream(..) | PipelineData::Value(Value::List { .. }, _)
187        ) {
188            // Potential enhancement: add another branch for Value::List,
189            // and collect the iterator into a Result<Value::List, ShellError>
190            // so we can preemptively return an error for collected lists
191            let head = call.head;
192            Ok(input
193                .into_iter()
194                .map(move |item| {
195                    let span = item.span();
196                    if let Value::Record { val, .. } = item {
197                        fill_record(
198                            val.into_owned(),
199                            span,
200                            &mut default_value,
201                            columns.as_slice(),
202                            default_when_empty,
203                        )
204                        .unwrap_or_else(|err| Value::error(err, head))
205                    } else {
206                        item
207                    }
208                })
209                .into_pipeline_data_with_metadata(head, signals.clone(), metadata))
210        // If columns are given, but input does not use columns, return an error
211        } else {
212            Err(ShellError::PipelineMismatch {
213                exp_input_type: "record, table".to_string(),
214                dst_span: input_span,
215                src_span: input_span,
216            })
217        }
218    // Otherwise, if no column name is given, check if value is null
219    // or an empty string, list, or record when --empty is passed
220    } else if input.is_nothing()
221        || (default_when_empty
222            && matches!(input, PipelineData::Value(ref value, _) if value.is_empty()))
223    {
224        default_value.single_run_pipeline_data()
225    } else if default_when_empty && matches!(input, PipelineData::ListStream(..)) {
226        let PipelineData::ListStream(ls, _) = input else {
227            unreachable!()
228        };
229        let span = ls.span();
230        let mut stream = ls.into_inner().peekable();
231        if stream.peek().is_none() {
232            return default_value.single_run_pipeline_data();
233        }
234
235        // stream's internal state already preserves the original signals config, so if this
236        // Signals::empty list stream gets interrupted it will be caught by the underlying iterator
237        let ls = ListStream::new(stream, span, Signals::empty());
238        Ok(PipelineData::list_stream(ls, metadata))
239    // Otherwise, return the input as is
240    } else {
241        Ok(input.set_metadata(metadata))
242    }
243}
244
245/// A wrapper around the default value to handle closures and caching values
246enum DefaultValue {
247    Uncalculated(Box<Spanned<ClosureEval>>),
248    Calculated(Value),
249}
250
251impl DefaultValue {
252    fn new(
253        engine_state: &EngineState,
254        stack: &Stack,
255        value: Value,
256        expr: Option<&Expression>,
257    ) -> Self {
258        let span = value.span();
259
260        // FIXME temporary workaround to warn people of breaking change from #15654
261        let value = match closure_variable_warning(stack, engine_state, value, expr) {
262            Ok(val) => val,
263            Err(default_value) => return default_value,
264        };
265
266        match value {
267            Value::Closure { val, .. } => {
268                let closure_eval = ClosureEval::new(engine_state, stack, *val);
269                DefaultValue::Uncalculated(Box::new(closure_eval.into_spanned(span)))
270            }
271            _ => DefaultValue::Calculated(value),
272        }
273    }
274
275    fn value(&mut self) -> Result<Value, ShellError> {
276        match self {
277            DefaultValue::Uncalculated(closure) => {
278                let value = closure
279                    .item
280                    .run_with_input(PipelineData::empty())?
281                    .into_value(closure.span)?;
282                *self = DefaultValue::Calculated(value.clone());
283                Ok(value)
284            }
285            DefaultValue::Calculated(value) => Ok(value.clone()),
286        }
287    }
288
289    /// Used when we know the value won't need to be cached to allow streaming.
290    fn single_run_pipeline_data(self) -> Result<PipelineData, ShellError> {
291        match self {
292            DefaultValue::Uncalculated(mut closure) => {
293                closure.item.run_with_input(PipelineData::empty())
294            }
295            DefaultValue::Calculated(val) => Ok(val.into_pipeline_data()),
296        }
297    }
298}
299
300/// Given a record, fill missing columns with a default value
301fn fill_record(
302    mut record: Record,
303    span: Span,
304    default_value: &mut DefaultValue,
305    columns: &[String],
306    empty: bool,
307) -> Result<Value, ShellError> {
308    for col in columns {
309        if let Some(val) = record.get_mut(col) {
310            if matches!(val, Value::Nothing { .. }) || (empty && val.is_empty()) {
311                *val = default_value.value()?;
312            }
313        } else {
314            record.push(col.clone(), default_value.value()?);
315        }
316    }
317    Ok(Value::record(record, span))
318}
319
320fn closure_variable_warning(
321    stack: &Stack,
322    engine_state: &EngineState,
323    value: Value,
324    value_expr: Option<&Expression>,
325) -> Result<Value, DefaultValue> {
326    // only warn if we are passed a closure inside a variable
327    let from_variable = matches!(
328        value_expr,
329        Some(Expression {
330            expr: Expr::FullCellPath(_),
331            ..
332        })
333    );
334
335    let span = value.span();
336    match (&value, from_variable) {
337        // this is a closure from inside a variable
338        (Value::Closure { .. }, true) => {
339            let span_contents = String::from_utf8_lossy(engine_state.get_span_contents(span));
340            let carapace_suggestion = "re-run carapace init with version v1.3.3 or later\nor, change this to `{ $carapace_completer }`";
341            let label = match span_contents {
342                Cow::Borrowed("$carapace_completer") => carapace_suggestion.to_string(),
343                Cow::Owned(s) if s.deref() == "$carapace_completer" => {
344                    carapace_suggestion.to_string()
345                }
346                _ => format!("change this to {{ {span_contents} }}").to_string(),
347            };
348
349            report_shell_warning(
350                Some(stack),
351                engine_state,
352                &ShellWarning::Deprecated {
353                    dep_type: "Behavior".to_string(),
354                    label,
355                    span,
356                    help: Some(
357                        "Since 0.105.0, closure literals passed to default are lazily evaluated, rather than returned as a value.
358In a future release, closures passed by variable will also be lazily evaluated.".to_string(),
359                    ),
360                    report_mode: ReportMode::FirstUse,
361                },
362            );
363
364            // bypass the normal DefaultValue::new logic
365            Err(DefaultValue::Calculated(value))
366        }
367        _ => Ok(value),
368    }
369}
370
371#[cfg(test)]
372mod test {
373    use super::*;
374
375    #[test]
376    fn test_examples() -> nu_test_support::Result {
377        nu_test_support::test().examples(Default)
378    }
379}