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