nu_command/filters/
move_.rs

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
181// Move columns within a record
182fn 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    // Find indices of columns to be moved
191    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            // Check if pivot exists
210            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                    // Check if this pivot is also a column we are supposed to move
223                    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    // helper
297    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        // corresponds to: {a: 1, b: 2, c: 3, d: 4} | move a --after c
326        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4, e: 5} | move b e --after c
347        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4} | move d --before b
368        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4, e: 5} | move c e --before b
389        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4} | move c --first
406        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4, e: 5} | move c e --first
423        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4} | move b --last
440        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4, e: 5} | move c d --last
457        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4, e: 5} | move d c a --after e
478        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}