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