nu_command/filters/drop/
column.rs

1use nu_engine::command_prelude::*;
2
3use std::collections::HashSet;
4
5#[derive(Clone)]
6pub struct DropColumn;
7
8impl Command for DropColumn {
9    fn name(&self) -> &str {
10        "drop column"
11    }
12
13    fn signature(&self) -> Signature {
14        Signature::build(self.name())
15            .input_output_types(vec![
16                (Type::table(), Type::table()),
17                (Type::record(), Type::record()),
18            ])
19            .optional(
20                "columns",
21                SyntaxShape::Int,
22                "Starting from the end, the number of columns to remove.",
23            )
24            .category(Category::Filters)
25    }
26
27    fn description(&self) -> &str {
28        "Remove N columns at the right-hand end of the input table. To remove columns by name, use `reject`."
29    }
30
31    fn search_terms(&self) -> Vec<&str> {
32        vec!["delete", "remove"]
33    }
34
35    fn run(
36        &self,
37        engine_state: &EngineState,
38        stack: &mut Stack,
39        call: &Call,
40        input: PipelineData,
41    ) -> Result<PipelineData, ShellError> {
42        // the number of columns to drop
43        let columns: Option<Spanned<i64>> = call.opt(engine_state, stack, 0)?;
44
45        let columns = if let Some(columns) = columns {
46            if columns.item < 0 {
47                return Err(ShellError::NeedsPositiveValue { span: columns.span });
48            } else {
49                columns.item as usize
50            }
51        } else {
52            1
53        };
54
55        drop_cols(engine_state, input, call.head, columns)
56    }
57
58    fn examples(&self) -> Vec<Example<'_>> {
59        vec![
60            Example {
61                description: "Remove the last column of a table",
62                example: "[[lib, extension]; [nu-lib, rs] [nu-core, rb]] | drop column",
63                result: Some(Value::test_list(vec![
64                    Value::test_record(record! { "lib" => Value::test_string("nu-lib") }),
65                    Value::test_record(record! { "lib" => Value::test_string("nu-core") }),
66                ])),
67            },
68            Example {
69                description: "Remove the last column of a record",
70                example: "{lib: nu-lib, extension: rs} | drop column",
71                result: Some(Value::test_record(
72                    record! { "lib" => Value::test_string("nu-lib") },
73                )),
74            },
75        ]
76    }
77}
78
79fn drop_cols(
80    engine_state: &EngineState,
81    input: PipelineData,
82    head: Span,
83    columns: usize,
84) -> Result<PipelineData, ShellError> {
85    // For simplicity and performance, we use the first row's columns
86    // as the columns for the whole table, and assume that later rows/records
87    // have these same columns. However, this can give weird results like:
88    // `[{a: 1}, {b: 2}] | drop column`
89    // This will drop the column "a" instead of "b" even though column "b"
90    // is displayed farther to the right.
91    let metadata = input.metadata();
92    match input {
93        PipelineData::ListStream(stream, ..) => {
94            let mut stream = stream.into_iter();
95            if let Some(mut first) = stream.next() {
96                let drop_cols = drop_cols_set(&mut first, head, columns)?;
97
98                Ok(std::iter::once(first)
99                    .chain(stream.map(move |mut v| {
100                        match drop_record_cols(&mut v, head, &drop_cols) {
101                            Ok(()) => v,
102                            Err(e) => Value::error(e, head),
103                        }
104                    }))
105                    .into_pipeline_data_with_metadata(
106                        head,
107                        engine_state.signals().clone(),
108                        metadata,
109                    ))
110            } else {
111                Ok(PipelineData::empty())
112            }
113        }
114        PipelineData::Value(mut v, ..) => {
115            let span = v.span();
116            match v {
117                Value::List { mut vals, .. } => {
118                    if let Some((first, rest)) = vals.split_first_mut() {
119                        let drop_cols = drop_cols_set(first, head, columns)?;
120                        for val in rest {
121                            drop_record_cols(val, head, &drop_cols)?
122                        }
123                    }
124                    Ok(Value::list(vals, span).into_pipeline_data_with_metadata(metadata))
125                }
126                Value::Record {
127                    val: ref mut record,
128                    ..
129                } => {
130                    let len = record.len().saturating_sub(columns);
131                    record.to_mut().truncate(len);
132                    Ok(v.into_pipeline_data_with_metadata(metadata))
133                }
134                // Propagate errors
135                Value::Error { error, .. } => Err(*error),
136                val => Err(unsupported_value_error(&val, head)),
137            }
138        }
139        PipelineData::Empty => Ok(PipelineData::empty()),
140        PipelineData::ByteStream(stream, ..) => Err(ShellError::OnlySupportsThisInputType {
141            exp_input_type: "table or record".into(),
142            wrong_type: stream.type_().describe().into(),
143            dst_span: head,
144            src_span: stream.span(),
145        }),
146    }
147}
148
149fn drop_cols_set(val: &mut Value, head: Span, drop: usize) -> Result<HashSet<String>, ShellError> {
150    if let Value::Record { val: record, .. } = val {
151        let len = record.len().saturating_sub(drop);
152        Ok(record.to_mut().drain(len..).map(|(col, _)| col).collect())
153    } else {
154        Err(unsupported_value_error(val, head))
155    }
156}
157
158fn drop_record_cols(
159    val: &mut Value,
160    head: Span,
161    drop_cols: &HashSet<String>,
162) -> Result<(), ShellError> {
163    if let Value::Record { val, .. } = val {
164        val.to_mut().retain(|col, _| !drop_cols.contains(col));
165        Ok(())
166    } else {
167        Err(unsupported_value_error(val, head))
168    }
169}
170
171fn unsupported_value_error(val: &Value, head: Span) -> ShellError {
172    ShellError::OnlySupportsThisInputType {
173        exp_input_type: "table or record".into(),
174        wrong_type: val.get_type().to_string(),
175        dst_span: head,
176        src_span: val.span(),
177    }
178}
179
180#[cfg(test)]
181mod test {
182    use super::*;
183
184    #[test]
185    fn test_examples() {
186        crate::test_examples(DropColumn)
187    }
188}