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