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 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 None => vec![
250 ' ', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', ], };
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 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}