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