nu_command/strings/str_/trim/
trim_.rs

1use nu_cmd_base::input_handler::{CmdArgument, operate};
2use nu_engine::command_prelude::*;
3
4#[derive(Clone)]
5pub struct StrTrim;
6
7struct Arguments {
8    to_trim: Option<char>,
9    trim_side: TrimSide,
10    cell_paths: Option<Vec<CellPath>>,
11    mode: ActionMode,
12}
13
14impl CmdArgument for Arguments {
15    fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
16        self.cell_paths.take()
17    }
18}
19
20pub enum TrimSide {
21    Left,
22    Right,
23    Both,
24}
25
26impl Command for StrTrim {
27    fn name(&self) -> &str {
28        "str trim"
29    }
30
31    fn signature(&self) -> Signature {
32        Signature::build("str trim")
33            .input_output_types(vec![
34                (Type::String, Type::String),
35                (
36                    Type::List(Box::new(Type::String)),
37                    Type::List(Box::new(Type::String)),
38                ),
39                (Type::table(), Type::table()),
40                (Type::record(), Type::record()),
41            ])
42            .allow_variants_without_examples(true)
43            .rest(
44                "rest",
45                SyntaxShape::CellPath,
46                "For a data structure input, trim strings at the given cell paths.",
47            )
48            .named(
49                "char",
50                SyntaxShape::String,
51                "character to trim (default: whitespace)",
52                Some('c'),
53            )
54            .switch(
55                "left",
56                "trims characters only from the beginning of the string",
57                Some('l'),
58            )
59            .switch(
60                "right",
61                "trims characters only from the end of the string",
62                Some('r'),
63            )
64            .category(Category::Strings)
65    }
66    fn description(&self) -> &str {
67        "Trim whitespace or specific character."
68    }
69
70    fn search_terms(&self) -> Vec<&str> {
71        vec!["whitespace", "strip", "lstrip", "rstrip"]
72    }
73
74    fn is_const(&self) -> bool {
75        true
76    }
77
78    fn run(
79        &self,
80        engine_state: &EngineState,
81        stack: &mut Stack,
82        call: &Call,
83        input: PipelineData,
84    ) -> Result<PipelineData, ShellError> {
85        let character = call.get_flag::<Spanned<String>>(engine_state, stack, "char")?;
86        let cell_paths: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
87        let left = call.has_flag(engine_state, stack, "left")?;
88        let right = call.has_flag(engine_state, stack, "right")?;
89        run(
90            character,
91            cell_paths,
92            (left, right),
93            call,
94            input,
95            engine_state,
96        )
97    }
98
99    fn run_const(
100        &self,
101        working_set: &StateWorkingSet,
102        call: &Call,
103        input: PipelineData,
104    ) -> Result<PipelineData, ShellError> {
105        let character = call.get_flag_const::<Spanned<String>>(working_set, "char")?;
106        let cell_paths: Vec<CellPath> = call.rest_const(working_set, 0)?;
107        let left = call.has_flag_const(working_set, "left")?;
108        let right = call.has_flag_const(working_set, "right")?;
109        run(
110            character,
111            cell_paths,
112            (left, right),
113            call,
114            input,
115            working_set.permanent(),
116        )
117    }
118
119    fn examples(&self) -> Vec<Example<'_>> {
120        vec![
121            Example {
122                description: "Trim whitespace",
123                example: "'Nu shell ' | str trim",
124                result: Some(Value::test_string("Nu shell")),
125            },
126            Example {
127                description: "Trim a specific character (not the whitespace)",
128                example: "'=== Nu shell ===' | str trim --char '='",
129                result: Some(Value::test_string(" Nu shell ")),
130            },
131            Example {
132                description: "Trim whitespace from the beginning of string",
133                example: "' Nu shell ' | str trim --left",
134                result: Some(Value::test_string("Nu shell ")),
135            },
136            Example {
137                description: "Trim whitespace from the end of string",
138                example: "' Nu shell ' | str trim --right",
139                result: Some(Value::test_string(" Nu shell")),
140            },
141            Example {
142                description: "Trim a specific character only from the end of the string",
143                example: "'=== Nu shell ===' | str trim --right --char '='",
144                result: Some(Value::test_string("=== Nu shell ")),
145            },
146        ]
147    }
148}
149
150fn run(
151    character: Option<Spanned<String>>,
152    cell_paths: Vec<CellPath>,
153    (left, right): (bool, bool),
154    call: &Call,
155    input: PipelineData,
156    engine_state: &EngineState,
157) -> Result<PipelineData, ShellError> {
158    let to_trim = match character.as_ref() {
159        Some(v) => {
160            if v.item.chars().count() > 1 {
161                return Err(ShellError::GenericError {
162                    error: "Trim only works with single character".into(),
163                    msg: "needs single character".into(),
164                    span: Some(v.span),
165                    help: None,
166                    inner: vec![],
167                });
168            }
169            v.item.chars().next()
170        }
171        None => None,
172    };
173
174    let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
175    let mode = match cell_paths {
176        None => ActionMode::Global,
177        Some(_) => ActionMode::Local,
178    };
179
180    let trim_side = match (left, right) {
181        (true, true) => TrimSide::Both,
182        (true, false) => TrimSide::Left,
183        (false, true) => TrimSide::Right,
184        (false, false) => TrimSide::Both,
185    };
186
187    let args = Arguments {
188        to_trim,
189        trim_side,
190        cell_paths,
191        mode,
192    };
193    operate(action, args, input, call.head, engine_state.signals())
194}
195
196#[derive(Debug, Copy, Clone)]
197pub enum ActionMode {
198    Local,
199    Global,
200}
201
202fn action(input: &Value, arg: &Arguments, head: Span) -> Value {
203    let char_ = arg.to_trim;
204    let trim_side = &arg.trim_side;
205    let mode = &arg.mode;
206    match input {
207        Value::String { val: s, .. } => Value::string(trim(s, char_, trim_side), head),
208        // Propagate errors by explicitly matching them before the final case.
209        Value::Error { .. } => input.clone(),
210        other => {
211            let span = other.span();
212
213            match mode {
214                ActionMode::Global => match other {
215                    Value::Record { val: record, .. } => {
216                        let new_record = record
217                            .iter()
218                            .map(|(k, v)| (k.clone(), action(v, arg, head)))
219                            .collect();
220
221                        Value::record(new_record, span)
222                    }
223                    Value::List { vals, .. } => {
224                        let new_vals = vals.iter().map(|v| action(v, arg, head)).collect();
225
226                        Value::list(new_vals, span)
227                    }
228                    _ => input.clone(),
229                },
230                ActionMode::Local => Value::error(
231                    ShellError::UnsupportedInput {
232                        msg: "Only string values are supported".into(),
233                        input: format!("input type: {:?}", other.get_type()),
234                        msg_span: head,
235                        input_span: other.span(),
236                    },
237                    head,
238                ),
239            }
240        }
241    }
242}
243
244fn trim(s: &str, char_: Option<char>, trim_side: &TrimSide) -> String {
245    let delimiters = match char_ {
246        Some(c) => vec![c],
247        // Trying to make this trim work like rust default trim()
248        // which uses is_whitespace() as a default
249        None => vec![
250            ' ',    // space
251            '\x09', // horizontal tab
252            '\x0A', // new line, line feed
253            '\x0B', // vertical tab
254            '\x0C', // form feed, new page
255            '\x0D', // carriage return
256        ], //whitespace
257    };
258
259    match trim_side {
260        TrimSide::Left => s.trim_start_matches(&delimiters[..]).to_string(),
261        TrimSide::Right => s.trim_end_matches(&delimiters[..]).to_string(),
262        TrimSide::Both => s.trim_matches(&delimiters[..]).to_string(),
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use crate::strings::str_::trim::trim_::*;
269    use nu_protocol::{Span, Value};
270
271    #[test]
272    fn test_examples() {
273        use crate::test_examples;
274
275        test_examples(StrTrim {})
276    }
277
278    fn make_record(cols: Vec<&str>, vals: Vec<&str>) -> Value {
279        Value::test_record(
280            cols.into_iter()
281                .zip(vals)
282                .map(|(col, val)| (col.to_owned(), Value::test_string(val)))
283                .collect(),
284        )
285    }
286
287    fn make_list(vals: Vec<&str>) -> Value {
288        Value::list(
289            vals.iter()
290                .map(|x| Value::test_string(x.to_string()))
291                .collect(),
292            Span::test_data(),
293        )
294    }
295
296    #[test]
297    fn trims() {
298        let word = Value::test_string("andres ");
299        let expected = Value::test_string("andres");
300
301        let args = Arguments {
302            to_trim: None,
303            trim_side: TrimSide::Both,
304            cell_paths: None,
305            mode: ActionMode::Local,
306        };
307        let actual = action(&word, &args, Span::test_data());
308        assert_eq!(actual, expected);
309    }
310
311    #[test]
312    fn trims_global() {
313        let word = Value::test_string(" global   ");
314        let expected = Value::test_string("global");
315        let args = Arguments {
316            to_trim: None,
317            trim_side: TrimSide::Both,
318            cell_paths: None,
319            mode: ActionMode::Global,
320        };
321        let actual = action(&word, &args, Span::test_data());
322        assert_eq!(actual, expected);
323    }
324
325    #[test]
326    fn global_trim_ignores_numbers() {
327        let number = Value::test_int(2020);
328        let expected = Value::test_int(2020);
329        let args = Arguments {
330            to_trim: None,
331            trim_side: TrimSide::Both,
332            cell_paths: None,
333            mode: ActionMode::Global,
334        };
335
336        let actual = action(&number, &args, Span::test_data());
337        assert_eq!(actual, expected);
338    }
339
340    #[test]
341    fn global_trim_row() {
342        let row = make_record(vec!["a", "b"], vec!["    c ", "  d   "]);
343        // ["a".to_string() => string("    c "), " b ".to_string() => string("  d   ")];
344        let expected = make_record(vec!["a", "b"], vec!["c", "d"]);
345
346        let args = Arguments {
347            to_trim: None,
348            trim_side: TrimSide::Both,
349            cell_paths: None,
350            mode: ActionMode::Global,
351        };
352        let actual = action(&row, &args, Span::test_data());
353        assert_eq!(actual, expected);
354    }
355
356    #[test]
357    fn global_trim_table() {
358        let row = make_list(vec!["  a  ", "d"]);
359        let expected = make_list(vec!["a", "d"]);
360
361        let args = Arguments {
362            to_trim: None,
363            trim_side: TrimSide::Both,
364            cell_paths: None,
365            mode: ActionMode::Global,
366        };
367        let actual = action(&row, &args, Span::test_data());
368        assert_eq!(actual, expected);
369    }
370
371    #[test]
372    fn trims_custom_character_both_ends() {
373        let word = Value::test_string("!#andres#!");
374        let expected = Value::test_string("#andres#");
375
376        let args = Arguments {
377            to_trim: Some('!'),
378            trim_side: TrimSide::Both,
379            cell_paths: None,
380            mode: ActionMode::Local,
381        };
382        let actual = action(&word, &args, Span::test_data());
383        assert_eq!(actual, expected);
384    }
385
386    #[test]
387    fn trims_whitespace_from_left() {
388        let word = Value::test_string(" andres ");
389        let expected = Value::test_string("andres ");
390
391        let args = Arguments {
392            to_trim: None,
393            trim_side: TrimSide::Left,
394            cell_paths: None,
395            mode: ActionMode::Local,
396        };
397        let actual = action(&word, &args, Span::test_data());
398        assert_eq!(actual, expected);
399    }
400
401    #[test]
402    fn global_trim_left_ignores_numbers() {
403        let number = Value::test_int(2020);
404        let expected = Value::test_int(2020);
405
406        let args = Arguments {
407            to_trim: None,
408            trim_side: TrimSide::Left,
409            cell_paths: None,
410            mode: ActionMode::Global,
411        };
412        let actual = action(&number, &args, Span::test_data());
413        assert_eq!(actual, expected);
414    }
415
416    #[test]
417    fn trims_left_global() {
418        let word = Value::test_string(" global   ");
419        let expected = Value::test_string("global   ");
420
421        let args = Arguments {
422            to_trim: None,
423            trim_side: TrimSide::Left,
424            cell_paths: None,
425            mode: ActionMode::Global,
426        };
427        let actual = action(&word, &args, Span::test_data());
428        assert_eq!(actual, expected);
429    }
430
431    #[test]
432    fn global_trim_left_row() {
433        let row = make_record(vec!["a", "b"], vec!["    c ", "  d   "]);
434        let expected = make_record(vec!["a", "b"], vec!["c ", "d   "]);
435
436        let args = Arguments {
437            to_trim: None,
438            trim_side: TrimSide::Left,
439            cell_paths: None,
440            mode: ActionMode::Global,
441        };
442        let actual = action(&row, &args, Span::test_data());
443        assert_eq!(actual, expected);
444    }
445
446    #[test]
447    fn global_trim_left_table() {
448        let row = Value::list(
449            vec![
450                Value::test_string("  a  "),
451                Value::test_int(65),
452                Value::test_string(" d"),
453            ],
454            Span::test_data(),
455        );
456        let expected = Value::list(
457            vec![
458                Value::test_string("a  "),
459                Value::test_int(65),
460                Value::test_string("d"),
461            ],
462            Span::test_data(),
463        );
464
465        let args = Arguments {
466            to_trim: None,
467            trim_side: TrimSide::Left,
468            cell_paths: None,
469            mode: ActionMode::Global,
470        };
471        let actual = action(&row, &args, Span::test_data());
472        assert_eq!(actual, expected);
473    }
474
475    #[test]
476    fn trims_custom_chars_from_left() {
477        let word = Value::test_string("!!! andres !!!");
478        let expected = Value::test_string(" andres !!!");
479
480        let args = Arguments {
481            to_trim: Some('!'),
482            trim_side: TrimSide::Left,
483            cell_paths: None,
484            mode: ActionMode::Local,
485        };
486        let actual = action(&word, &args, Span::test_data());
487        assert_eq!(actual, expected);
488    }
489    #[test]
490    fn trims_whitespace_from_right() {
491        let word = Value::test_string(" andres ");
492        let expected = Value::test_string(" andres");
493
494        let args = Arguments {
495            to_trim: None,
496            trim_side: TrimSide::Right,
497            cell_paths: None,
498            mode: ActionMode::Local,
499        };
500        let actual = action(&word, &args, Span::test_data());
501        assert_eq!(actual, expected);
502    }
503
504    #[test]
505    fn trims_right_global() {
506        let word = Value::test_string(" global   ");
507        let expected = Value::test_string(" global");
508        let args = Arguments {
509            to_trim: None,
510            trim_side: TrimSide::Right,
511            cell_paths: None,
512            mode: ActionMode::Global,
513        };
514        let actual = action(&word, &args, Span::test_data());
515        assert_eq!(actual, expected);
516    }
517
518    #[test]
519    fn global_trim_right_ignores_numbers() {
520        let number = Value::test_int(2020);
521        let expected = Value::test_int(2020);
522        let args = Arguments {
523            to_trim: None,
524            trim_side: TrimSide::Right,
525            cell_paths: None,
526            mode: ActionMode::Global,
527        };
528        let actual = action(&number, &args, Span::test_data());
529        assert_eq!(actual, expected);
530    }
531
532    #[test]
533    fn global_trim_right_row() {
534        let row = make_record(vec!["a", "b"], vec!["    c ", "  d   "]);
535        let expected = make_record(vec!["a", "b"], vec!["    c", "  d"]);
536        let args = Arguments {
537            to_trim: None,
538            trim_side: TrimSide::Right,
539            cell_paths: None,
540            mode: ActionMode::Global,
541        };
542        let actual = action(&row, &args, Span::test_data());
543        assert_eq!(actual, expected);
544    }
545
546    #[test]
547    fn global_trim_right_table() {
548        let row = Value::list(
549            vec![
550                Value::test_string("  a  "),
551                Value::test_int(65),
552                Value::test_string(" d"),
553            ],
554            Span::test_data(),
555        );
556        let expected = Value::list(
557            vec![
558                Value::test_string("  a"),
559                Value::test_int(65),
560                Value::test_string(" d"),
561            ],
562            Span::test_data(),
563        );
564        let args = Arguments {
565            to_trim: None,
566            trim_side: TrimSide::Right,
567            cell_paths: None,
568            mode: ActionMode::Global,
569        };
570        let actual = action(&row, &args, Span::test_data());
571        assert_eq!(actual, expected);
572    }
573
574    #[test]
575    fn trims_custom_chars_from_right() {
576        let word = Value::test_string("#@! andres !@#");
577        let expected = Value::test_string("#@! andres !@");
578
579        let args = Arguments {
580            to_trim: Some('#'),
581            trim_side: TrimSide::Right,
582            cell_paths: None,
583            mode: ActionMode::Local,
584        };
585        let actual = action(&word, &args, Span::test_data());
586        assert_eq!(actual, expected);
587    }
588}