use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::CallExt; use nu_protocol::{ ast::{Call, CellPath}, engine::{Command, EngineState, Stack}, Category, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; #[derive(Clone)] pub struct SubCommand; struct Arguments { to_trim: Option, trim_side: TrimSide, cell_paths: Option>, mode: ActionMode, } impl CmdArgument for Arguments { fn take_cell_paths(&mut self) -> Option> { self.cell_paths.take() } } pub enum TrimSide { Left, Right, Both, } impl Command for SubCommand { fn name(&self) -> &str { "str trim" } fn signature(&self) -> Signature { Signature::build("str trim") .input_output_types(vec![ (Type::String, Type::String), ( Type::List(Box::new(Type::String)), Type::List(Box::new(Type::String)), ), (Type::Table(vec![]), Type::Table(vec![])), (Type::Record(vec![]), Type::Record(vec![])), ]) .allow_variants_without_examples(true) .rest( "rest", SyntaxShape::CellPath, "For a data structure input, trim strings at the given cell paths", ) .named( "char", SyntaxShape::String, "character to trim (default: whitespace)", Some('c'), ) .switch( "left", "trims characters only from the beginning of the string", Some('l'), ) .switch( "right", "trims characters only from the end of the string", Some('r'), ) .category(Category::Strings) } fn usage(&self) -> &str { "Trim whitespace or specific character." } fn search_terms(&self) -> Vec<&str> { vec!["whitespace", "strip", "lstrip", "rstrip"] } fn run( &self, engine_state: &EngineState, stack: &mut Stack, call: &Call, input: PipelineData, ) -> Result { let character = call.get_flag::>(engine_state, stack, "char")?; let to_trim = match character.as_ref() { Some(v) => { if v.item.chars().count() > 1 { return Err(ShellError::GenericError( "Trim only works with single character".into(), "needs single character".into(), Some(v.span), None, Vec::new(), )); } v.item.chars().next() } None => None, }; let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); let mode = match cell_paths { None => ActionMode::Global, Some(_) => ActionMode::Local, }; let left = call.has_flag("left"); let right = call.has_flag("right"); let trim_side = match (left, right) { (true, true) => TrimSide::Both, (true, false) => TrimSide::Left, (false, true) => TrimSide::Right, (false, false) => TrimSide::Both, }; let args = Arguments { to_trim, trim_side, cell_paths, mode, }; operate(action, args, input, call.head, engine_state.ctrlc.clone()) } fn examples(&self) -> Vec { vec![ Example { description: "Trim whitespace", example: "'Nu shell ' | str trim", result: Some(Value::test_string("Nu shell")), }, Example { description: "Trim a specific character", example: "'=== Nu shell ===' | str trim --char '=' | str trim", result: Some(Value::test_string("Nu shell")), }, Example { description: "Trim whitespace from the beginning of string", example: "' Nu shell ' | str trim --left", result: Some(Value::test_string("Nu shell ")), }, Example { description: "Trim a specific character", example: "'=== Nu shell ===' | str trim --char '='", result: Some(Value::test_string(" Nu shell ")), }, Example { description: "Trim whitespace from the end of string", example: "' Nu shell ' | str trim --right", result: Some(Value::test_string(" Nu shell")), }, Example { description: "Trim a specific character", example: "'=== Nu shell ===' | str trim --right --char '='", result: Some(Value::test_string("=== Nu shell ")), }, ] } } #[derive(Debug, Copy, Clone)] pub enum ActionMode { Local, Global, } fn action(input: &Value, arg: &Arguments, head: Span) -> Value { let char_ = arg.to_trim; let trim_side = &arg.trim_side; let mode = &arg.mode; match input { Value::String { val: s, .. } => Value::string(trim(s, char_, trim_side), head), // Propagate errors by explicitly matching them before the final case. Value::Error { .. } => input.clone(), other => { let span = other.span(); match mode { ActionMode::Global => match other { Value::Record { val: record, .. } => { let new_record = record .iter() .map(|(k, v)| (k.clone(), action(v, arg, head))) .collect(); Value::record(new_record, span) } Value::List { vals, .. } => { let new_vals = vals.iter().map(|v| action(v, arg, head)).collect(); Value::list(new_vals, span) } _ => input.clone(), }, ActionMode::Local => Value::error( ShellError::UnsupportedInput { msg: "Only string values are supported".into(), input: format!("input type: {:?}", other.get_type()), msg_span: head, input_span: other.span(), }, head, ), } } } } fn trim(s: &str, char_: Option, trim_side: &TrimSide) -> String { let delimiters = match char_ { Some(c) => vec![c], // Trying to make this trim work like rust default trim() // which uses is_whitespace() as a default None => vec![ ' ', // space '\x09', // horizontal tab '\x0A', // new line, line feed '\x0B', // vertical tab '\x0C', // form feed, new page '\x0D', // carriage return ], //whitespace }; match trim_side { TrimSide::Left => s.trim_start_matches(&delimiters[..]).to_string(), TrimSide::Right => s.trim_end_matches(&delimiters[..]).to_string(), TrimSide::Both => s.trim_matches(&delimiters[..]).to_string(), } } #[cfg(test)] mod tests { use crate::strings::str_::trim::trim_::*; use nu_protocol::{Span, Value}; #[test] fn test_examples() { use crate::test_examples; test_examples(SubCommand {}) } fn make_record(cols: Vec<&str>, vals: Vec<&str>) -> Value { Value::test_record( cols.into_iter() .zip(vals) .map(|(col, val)| (col.to_owned(), Value::test_string(val))) .collect(), ) } fn make_list(vals: Vec<&str>) -> Value { Value::list( vals.iter() .map(|x| Value::test_string(x.to_string())) .collect(), Span::test_data(), ) } #[test] fn trims() { let word = Value::test_string("andres "); let expected = Value::test_string("andres"); let args = Arguments { to_trim: None, trim_side: TrimSide::Both, cell_paths: None, mode: ActionMode::Local, }; let actual = action(&word, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn trims_global() { let word = Value::test_string(" global "); let expected = Value::test_string("global"); let args = Arguments { to_trim: None, trim_side: TrimSide::Both, cell_paths: None, mode: ActionMode::Global, }; let actual = action(&word, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn global_trim_ignores_numbers() { let number = Value::test_int(2020); let expected = Value::test_int(2020); let args = Arguments { to_trim: None, trim_side: TrimSide::Both, cell_paths: None, mode: ActionMode::Global, }; let actual = action(&number, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn global_trim_row() { let row = make_record(vec!["a", "b"], vec![" c ", " d "]); // ["a".to_string() => string(" c "), " b ".to_string() => string(" d ")]; let expected = make_record(vec!["a", "b"], vec!["c", "d"]); let args = Arguments { to_trim: None, trim_side: TrimSide::Both, cell_paths: None, mode: ActionMode::Global, }; let actual = action(&row, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn global_trim_table() { let row = make_list(vec![" a ", "d"]); let expected = make_list(vec!["a", "d"]); let args = Arguments { to_trim: None, trim_side: TrimSide::Both, cell_paths: None, mode: ActionMode::Global, }; let actual = action(&row, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn trims_custom_character_both_ends() { let word = Value::test_string("!#andres#!"); let expected = Value::test_string("#andres#"); let args = Arguments { to_trim: Some('!'), trim_side: TrimSide::Both, cell_paths: None, mode: ActionMode::Local, }; let actual = action(&word, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn trims_whitespace_from_left() { let word = Value::test_string(" andres "); let expected = Value::test_string("andres "); let args = Arguments { to_trim: None, trim_side: TrimSide::Left, cell_paths: None, mode: ActionMode::Local, }; let actual = action(&word, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn global_trim_left_ignores_numbers() { let number = Value::test_int(2020); let expected = Value::test_int(2020); let args = Arguments { to_trim: None, trim_side: TrimSide::Left, cell_paths: None, mode: ActionMode::Global, }; let actual = action(&number, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn trims_left_global() { let word = Value::test_string(" global "); let expected = Value::test_string("global "); let args = Arguments { to_trim: None, trim_side: TrimSide::Left, cell_paths: None, mode: ActionMode::Global, }; let actual = action(&word, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn global_trim_left_row() { let row = make_record(vec!["a", "b"], vec![" c ", " d "]); let expected = make_record(vec!["a", "b"], vec!["c ", "d "]); let args = Arguments { to_trim: None, trim_side: TrimSide::Left, cell_paths: None, mode: ActionMode::Global, }; let actual = action(&row, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn global_trim_left_table() { let row = Value::list( vec![ Value::test_string(" a "), Value::test_int(65), Value::test_string(" d"), ], Span::test_data(), ); let expected = Value::list( vec![ Value::test_string("a "), Value::test_int(65), Value::test_string("d"), ], Span::test_data(), ); let args = Arguments { to_trim: None, trim_side: TrimSide::Left, cell_paths: None, mode: ActionMode::Global, }; let actual = action(&row, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn trims_custom_chars_from_left() { let word = Value::test_string("!!! andres !!!"); let expected = Value::test_string(" andres !!!"); let args = Arguments { to_trim: Some('!'), trim_side: TrimSide::Left, cell_paths: None, mode: ActionMode::Local, }; let actual = action(&word, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn trims_whitespace_from_right() { let word = Value::test_string(" andres "); let expected = Value::test_string(" andres"); let args = Arguments { to_trim: None, trim_side: TrimSide::Right, cell_paths: None, mode: ActionMode::Local, }; let actual = action(&word, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn trims_right_global() { let word = Value::test_string(" global "); let expected = Value::test_string(" global"); let args = Arguments { to_trim: None, trim_side: TrimSide::Right, cell_paths: None, mode: ActionMode::Global, }; let actual = action(&word, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn global_trim_right_ignores_numbers() { let number = Value::test_int(2020); let expected = Value::test_int(2020); let args = Arguments { to_trim: None, trim_side: TrimSide::Right, cell_paths: None, mode: ActionMode::Global, }; let actual = action(&number, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn global_trim_right_row() { let row = make_record(vec!["a", "b"], vec![" c ", " d "]); let expected = make_record(vec!["a", "b"], vec![" c", " d"]); let args = Arguments { to_trim: None, trim_side: TrimSide::Right, cell_paths: None, mode: ActionMode::Global, }; let actual = action(&row, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn global_trim_right_table() { let row = Value::list( vec![ Value::test_string(" a "), Value::test_int(65), Value::test_string(" d"), ], Span::test_data(), ); let expected = Value::list( vec![ Value::test_string(" a"), Value::test_int(65), Value::test_string(" d"), ], Span::test_data(), ); let args = Arguments { to_trim: None, trim_side: TrimSide::Right, cell_paths: None, mode: ActionMode::Global, }; let actual = action(&row, &args, Span::test_data()); assert_eq!(actual, expected); } #[test] fn trims_custom_chars_from_right() { let word = Value::test_string("#@! andres !@#"); let expected = Value::test_string("#@! andres !@"); let args = Arguments { to_trim: Some('#'), trim_side: TrimSide::Right, cell_paths: None, mode: ActionMode::Local, }; let actual = action(&word, &args, Span::test_data()); assert_eq!(actual, expected); } }