1use std::ops::Not;
2
3use nu_engine::command_prelude::*;
4
5#[derive(Clone, Debug)]
6enum Location {
7 Before(Spanned<String>),
8 After(Spanned<String>),
9 Last,
10 First,
11}
12
13#[derive(Clone)]
14pub struct Move;
15
16impl Command for Move {
17 fn name(&self) -> &str {
18 "move"
19 }
20
21 fn description(&self) -> &str {
22 "Moves columns relative to other columns or make them the first/last columns. Flags are mutually exclusive."
23 }
24
25 fn signature(&self) -> nu_protocol::Signature {
26 Signature::build("move")
27 .input_output_types(vec![
28 (Type::record(), Type::record()),
29 (Type::table(), Type::table()),
30 ])
31 .rest("columns", SyntaxShape::String, "The columns to move.")
32 .named(
33 "after",
34 SyntaxShape::String,
35 "the column that will precede the columns moved",
36 None,
37 )
38 .named(
39 "before",
40 SyntaxShape::String,
41 "the column that will be the next after the columns moved",
42 None,
43 )
44 .switch("first", "makes the columns be the first ones", None)
45 .switch("last", "makes the columns be the last ones", None)
46 .category(Category::Filters)
47 }
48
49 fn examples(&self) -> Vec<Example<'_>> {
50 vec![
51 Example {
52 example: "[[name value index]; [foo a 1] [bar b 2] [baz c 3]] | move index --before name",
53 description: "Move a column before the first column",
54 result: Some(Value::test_list(vec![
55 Value::test_record(record! {
56 "index" => Value::test_int(1),
57 "name" => Value::test_string("foo"),
58 "value" => Value::test_string("a"),
59 }),
60 Value::test_record(record! {
61 "index" => Value::test_int(2),
62 "name" => Value::test_string("bar"),
63 "value" => Value::test_string("b"),
64 }),
65 Value::test_record(record! {
66 "index" => Value::test_int(3),
67 "name" => Value::test_string("baz"),
68 "value" => Value::test_string("c"),
69 }),
70 ])),
71 },
72 Example {
73 example: "[[name value index]; [foo a 1] [bar b 2] [baz c 3]] | move value name --after index",
74 description: "Move multiple columns after the last column and reorder them",
75 result: Some(Value::test_list(vec![
76 Value::test_record(record! {
77 "index" => Value::test_int(1),
78 "value" => Value::test_string("a"),
79 "name" => Value::test_string("foo"),
80 }),
81 Value::test_record(record! {
82 "index" => Value::test_int(2),
83 "value" => Value::test_string("b"),
84 "name" => Value::test_string("bar"),
85 }),
86 Value::test_record(record! {
87 "index" => Value::test_int(3),
88 "value" => Value::test_string("c"),
89 "name" => Value::test_string("baz"),
90 }),
91 ])),
92 },
93 Example {
94 example: "{ name: foo, value: a, index: 1 } | move name --before index",
95 description: "Move columns of a record",
96 result: Some(Value::test_record(record! {
97 "value" => Value::test_string("a"),
98 "name" => Value::test_string("foo"),
99 "index" => Value::test_int(1),
100 })),
101 },
102 ]
103 }
104
105 fn run(
106 &self,
107 engine_state: &EngineState,
108 stack: &mut Stack,
109 call: &Call,
110 input: PipelineData,
111 ) -> Result<PipelineData, ShellError> {
112 let head = call.head;
113 let columns: Vec<Value> = call.rest(engine_state, stack, 0)?;
114 let after: Option<Value> = call.get_flag(engine_state, stack, "after")?;
115 let before: Option<Value> = call.get_flag(engine_state, stack, "before")?;
116 let first = call.has_flag(engine_state, stack, "first")?;
117 let last = call.has_flag(engine_state, stack, "last")?;
118
119 let location = match (after, before, first, last) {
120 (Some(v), None, false, false) => Location::After(Spanned {
121 span: v.span(),
122 item: v.coerce_into_string()?,
123 }),
124 (None, Some(v), false, false) => Location::Before(Spanned {
125 span: v.span(),
126 item: v.coerce_into_string()?,
127 }),
128 (None, None, true, false) => Location::First,
129 (None, None, false, true) => Location::Last,
130 (None, None, false, false) => {
131 return Err(ShellError::GenericError {
132 error: "Cannot move columns".into(),
133 msg: "Missing required location flag".into(),
134 span: Some(head),
135 help: None,
136 inner: vec![],
137 });
138 }
139 _ => {
140 return Err(ShellError::GenericError {
141 error: "Cannot move columns".into(),
142 msg: "Use only a single flag".into(),
143 span: Some(head),
144 help: None,
145 inner: vec![],
146 });
147 }
148 };
149
150 let metadata = input.metadata();
151
152 match input {
153 PipelineData::Value(Value::List { .. }, ..) | PipelineData::ListStream { .. } => {
154 let res = input.into_iter().map(move |x| match x.as_record() {
155 Ok(record) => match move_record_columns(record, &columns, &location, head) {
156 Ok(val) => val,
157 Err(error) => Value::error(error, head),
158 },
159 Err(error) => Value::error(error, head),
160 });
161
162 Ok(res.into_pipeline_data_with_metadata(
163 head,
164 engine_state.signals().clone(),
165 metadata,
166 ))
167 }
168 PipelineData::Value(Value::Record { val, .. }, ..) => {
169 Ok(move_record_columns(&val, &columns, &location, head)?.into_pipeline_data())
170 }
171 other => Err(ShellError::OnlySupportsThisInputType {
172 exp_input_type: "record or table".to_string(),
173 wrong_type: other.get_type().to_string(),
174 dst_span: head,
175 src_span: Span::new(head.start, head.start),
176 }),
177 }
178 }
179}
180
181fn move_record_columns(
183 record: &Record,
184 columns: &[Value],
185 location: &Location,
186 span: Span,
187) -> Result<Value, ShellError> {
188 let mut column_idx: Vec<usize> = Vec::with_capacity(columns.len());
189
190 for column in columns.iter() {
192 if let Some(idx) = record.index_of(column.coerce_string()?) {
193 column_idx.push(idx);
194 } else {
195 return Err(ShellError::GenericError {
196 error: "Cannot move columns".into(),
197 msg: "column does not exist".into(),
198 span: Some(column.span()),
199 help: None,
200 inner: vec![],
201 });
202 }
203 }
204
205 let mut out = Record::with_capacity(record.len());
206
207 match &location {
208 Location::Before(pivot) | Location::After(pivot) => {
209 if !record.contains(&pivot.item) {
211 return Err(ShellError::GenericError {
212 error: "Cannot move columns".into(),
213 msg: "column does not exist".into(),
214 span: Some(pivot.span),
215 help: None,
216 inner: vec![],
217 });
218 }
219
220 for (i, (inp_col, inp_val)) in record.iter().enumerate() {
221 if inp_col == &pivot.item {
222 if column_idx.contains(&i) {
224 return Err(ShellError::IncompatibleParameters {
225 left_message: "Column cannot be moved".to_string(),
226 left_span: inp_val.span(),
227 right_message: "relative to itself".to_string(),
228 right_span: pivot.span,
229 });
230 }
231
232 if let Location::After(..) = location {
233 out.push(inp_col.clone(), inp_val.clone());
234 }
235
236 insert_moved(record, span, &column_idx, &mut out)?;
237
238 if let Location::Before(..) = location {
239 out.push(inp_col.clone(), inp_val.clone());
240 }
241 } else if !column_idx.contains(&i) {
242 out.push(inp_col.clone(), inp_val.clone());
243 }
244 }
245 }
246 Location::First => {
247 insert_moved(record, span, &column_idx, &mut out)?;
248
249 out.extend(where_unmoved(record, &column_idx));
250 }
251 Location::Last => {
252 out.extend(where_unmoved(record, &column_idx));
253
254 insert_moved(record, span, &column_idx, &mut out)?;
255 }
256 };
257
258 Ok(Value::record(out, span))
259}
260
261fn where_unmoved<'a>(
262 record: &'a Record,
263 column_idx: &'a [usize],
264) -> impl Iterator<Item = (String, Value)> + use<'a> {
265 record
266 .iter()
267 .enumerate()
268 .filter(|(i, _)| column_idx.contains(i).not())
269 .map(|(_, (c, v))| (c.clone(), v.clone()))
270}
271
272fn insert_moved(
273 record: &Record,
274 span: Span,
275 column_idx: &[usize],
276 out: &mut Record,
277) -> Result<(), ShellError> {
278 for idx in column_idx.iter() {
279 if let Some((col, val)) = record.get_index(*idx) {
280 out.push(col.clone(), val.clone());
281 } else {
282 return Err(ShellError::NushellFailedSpanned {
283 msg: "Error indexing input columns".to_string(),
284 label: "originates from here".to_string(),
285 span,
286 });
287 }
288 }
289 Ok(())
290}
291
292#[cfg(test)]
293mod test {
294 use super::*;
295
296 fn get_test_record(columns: Vec<&str>, values: Vec<i64>) -> Record {
298 let test_span = Span::test_data();
299 Record::from_raw_cols_vals(
300 columns.iter().map(|col| col.to_string()).collect(),
301 values.iter().map(|val| Value::test_int(*val)).collect(),
302 test_span,
303 test_span,
304 )
305 .unwrap()
306 }
307
308 #[test]
309 fn test_examples() {
310 use crate::test_examples;
311
312 test_examples(Move {})
313 }
314
315 #[test]
316 fn move_after_with_single_column() {
317 let test_span = Span::test_data();
318 let test_record = get_test_record(vec!["a", "b", "c", "d"], vec![1, 2, 3, 4]);
319 let after: Location = Location::After(Spanned {
320 item: "c".to_string(),
321 span: test_span,
322 });
323 let columns = ["a"].map(Value::test_string);
324
325 let result = move_record_columns(&test_record, &columns, &after, test_span);
327
328 assert!(result.is_ok());
329
330 let result_record = result.unwrap().into_record().unwrap();
331 let result_columns = result_record.into_columns().collect::<Vec<String>>();
332
333 assert_eq!(result_columns, ["b", "c", "a", "d"]);
334 }
335
336 #[test]
337 fn move_after_with_multiple_columns() {
338 let test_span = Span::test_data();
339 let test_record = get_test_record(vec!["a", "b", "c", "d", "e"], vec![1, 2, 3, 4, 5]);
340 let after: Location = Location::After(Spanned {
341 item: "c".to_string(),
342 span: test_span,
343 });
344 let columns = ["b", "e"].map(Value::test_string);
345
346 let result = move_record_columns(&test_record, &columns, &after, test_span);
348
349 assert!(result.is_ok());
350
351 let result_record = result.unwrap().into_record().unwrap();
352 let result_columns = result_record.into_columns().collect::<Vec<String>>();
353
354 assert_eq!(result_columns, ["a", "c", "b", "e", "d"]);
355 }
356
357 #[test]
358 fn move_before_with_single_column() {
359 let test_span = Span::test_data();
360 let test_record = get_test_record(vec!["a", "b", "c", "d"], vec![1, 2, 3, 4]);
361 let before: Location = Location::Before(Spanned {
362 item: "b".to_string(),
363 span: test_span,
364 });
365 let columns = ["d"].map(Value::test_string);
366
367 let result = move_record_columns(&test_record, &columns, &before, test_span);
369
370 assert!(result.is_ok());
371
372 let result_record = result.unwrap().into_record().unwrap();
373 let result_columns = result_record.into_columns().collect::<Vec<String>>();
374
375 assert_eq!(result_columns, ["a", "d", "b", "c"]);
376 }
377
378 #[test]
379 fn move_before_with_multiple_columns() {
380 let test_span = Span::test_data();
381 let test_record = get_test_record(vec!["a", "b", "c", "d", "e"], vec![1, 2, 3, 4, 5]);
382 let before: Location = Location::Before(Spanned {
383 item: "b".to_string(),
384 span: test_span,
385 });
386 let columns = ["c", "e"].map(Value::test_string);
387
388 let result = move_record_columns(&test_record, &columns, &before, test_span);
390
391 assert!(result.is_ok());
392
393 let result_record = result.unwrap().into_record().unwrap();
394 let result_columns = result_record.into_columns().collect::<Vec<String>>();
395
396 assert_eq!(result_columns, ["a", "c", "e", "b", "d"]);
397 }
398
399 #[test]
400 fn move_first_with_single_column() {
401 let test_span = Span::test_data();
402 let test_record = get_test_record(vec!["a", "b", "c", "d"], vec![1, 2, 3, 4]);
403 let columns = ["c"].map(Value::test_string);
404
405 let result = move_record_columns(&test_record, &columns, &Location::First, test_span);
407
408 assert!(result.is_ok());
409
410 let result_record = result.unwrap().into_record().unwrap();
411 let result_columns = result_record.into_columns().collect::<Vec<String>>();
412
413 assert_eq!(result_columns, ["c", "a", "b", "d"]);
414 }
415
416 #[test]
417 fn move_first_with_multiple_columns() {
418 let test_span = Span::test_data();
419 let test_record = get_test_record(vec!["a", "b", "c", "d", "e"], vec![1, 2, 3, 4, 5]);
420 let columns = ["c", "e"].map(Value::test_string);
421
422 let result = move_record_columns(&test_record, &columns, &Location::First, test_span);
424
425 assert!(result.is_ok());
426
427 let result_record = result.unwrap().into_record().unwrap();
428 let result_columns = result_record.into_columns().collect::<Vec<String>>();
429
430 assert_eq!(result_columns, ["c", "e", "a", "b", "d"]);
431 }
432
433 #[test]
434 fn move_last_with_single_column() {
435 let test_span = Span::test_data();
436 let test_record = get_test_record(vec!["a", "b", "c", "d"], vec![1, 2, 3, 4]);
437 let columns = ["b"].map(Value::test_string);
438
439 let result = move_record_columns(&test_record, &columns, &Location::Last, test_span);
441
442 assert!(result.is_ok());
443
444 let result_record = result.unwrap().into_record().unwrap();
445 let result_columns = result_record.into_columns().collect::<Vec<String>>();
446
447 assert_eq!(result_columns, ["a", "c", "d", "b"]);
448 }
449
450 #[test]
451 fn move_last_with_multiple_columns() {
452 let test_span = Span::test_data();
453 let test_record = get_test_record(vec!["a", "b", "c", "d", "e"], vec![1, 2, 3, 4, 5]);
454 let columns = ["c", "d"].map(Value::test_string);
455
456 let result = move_record_columns(&test_record, &columns, &Location::Last, test_span);
458
459 assert!(result.is_ok());
460
461 let result_record = result.unwrap().into_record().unwrap();
462 let result_columns = result_record.into_columns().collect::<Vec<String>>();
463
464 assert_eq!(result_columns, ["a", "b", "e", "c", "d"]);
465 }
466
467 #[test]
468 fn move_with_multiple_columns_reorders_columns() {
469 let test_span = Span::test_data();
470 let test_record = get_test_record(vec!["a", "b", "c", "d", "e"], vec![1, 2, 3, 4, 5]);
471 let after: Location = Location::After(Spanned {
472 item: "e".to_string(),
473 span: test_span,
474 });
475 let columns = ["d", "c", "a"].map(Value::test_string);
476
477 let result = move_record_columns(&test_record, &columns, &after, test_span);
479
480 assert!(result.is_ok());
481
482 let result_record = result.unwrap().into_record().unwrap();
483 let result_columns = result_record.into_columns().collect::<Vec<String>>();
484
485 assert_eq!(result_columns, ["b", "e", "d", "c", "a"]);
486 }
487
488 #[test]
489 fn move_fails_when_pivot_not_present() {
490 let test_span = Span::test_data();
491 let test_record = get_test_record(vec!["a", "b"], vec![1, 2]);
492 let before: Location = Location::Before(Spanned {
493 item: "non-existent".to_string(),
494 span: test_span,
495 });
496 let columns = ["a"].map(Value::test_string);
497
498 let result = move_record_columns(&test_record, &columns, &before, test_span);
499
500 assert!(result.is_err());
501 }
502
503 #[test]
504 fn move_fails_when_column_not_present() {
505 let test_span = Span::test_data();
506 let test_record = get_test_record(vec!["a", "b"], vec![1, 2]);
507 let before: Location = Location::Before(Spanned {
508 item: "b".to_string(),
509 span: test_span,
510 });
511 let columns = ["a", "non-existent"].map(Value::test_string);
512
513 let result = move_record_columns(&test_record, &columns, &before, test_span);
514
515 assert!(result.is_err());
516 }
517
518 #[test]
519 fn move_fails_when_column_is_also_pivot() {
520 let test_span = Span::test_data();
521 let test_record = get_test_record(vec!["a", "b", "c", "d"], vec![1, 2, 3, 4]);
522 let after: Location = Location::After(Spanned {
523 item: "b".to_string(),
524 span: test_span,
525 });
526 let columns = ["b", "d"].map(Value::test_string);
527
528 let result = move_record_columns(&test_record, &columns, &after, test_span);
529
530 assert!(result.is_err());
531 }
532}