Skip to main content

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