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