Skip to main content

nu_command/filters/
move_.rs

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
180// Move columns within a record
181fn 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    // Find indices of columns to be moved
190    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            // Check if pivot exists
207            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                    // Check if this pivot is also a column we are supposed to move
218                    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    // helper
292    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        // corresponds to: {a: 1, b: 2, c: 3, d: 4} | move a --after c
319        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4, e: 5} | move b e --after c
340        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4} | move d --before b
361        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4, e: 5} | move c e --before b
382        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4} | move c --first
399        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4, e: 5} | move c e --first
416        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4} | move b --last
433        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4, e: 5} | move c d --last
450        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        // corresponds to: {a: 1, b: 2, c: 3, d: 4, e: 5} | move d c a --after e
471        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}