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 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 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 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}