Skip to main content

nu_command/strings/str_/trim/
trim_.rs

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