diff --git a/Cargo.lock b/Cargo.lock index 4de7a3a6c..d20d4ea6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2804,10 +2804,11 @@ dependencies = [ "terminal_size 0.2.1", "thiserror", "titlecase", - "toml 0.7.1", + "toml 0.7.2", "trash", "umask", "unicode-segmentation", + "unicode-width", "url", "users 0.11.0", "uuid", @@ -5288,9 +5289,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "772c1426ab886e7362aedf4abc9c0d1348a979517efedfc25862944d10137af0" +checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6" dependencies = [ "serde", "serde_spanned", @@ -5309,9 +5310,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.1" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90a238ee2e6ede22fb95350acc78e21dc40da00bb66c0334bde83de4ed89424e" +checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5" dependencies = [ "indexmap", "nom8", diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 61db47f9c..164e2097f 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -86,8 +86,8 @@ sysinfo = "0.27.7" terminal_size = "0.2.1" thiserror = "1.0.31" titlecase = "2.0.0" +unicode-segmentation = "1.10.0" toml = "0.7.1" -unicode-segmentation = "1.8.0" url = "2.2.1" percent-encoding = "2.2.0" uuid = { version = "1.2.2", features = ["v4"] } @@ -96,6 +96,7 @@ reedline = { version = "0.15.0", features = ["bashisms", "sqlite"] } wax = { version = "0.5.0" } rusqlite = { version = "0.28.0", features = ["bundled"], optional = true } sqlparser = { version = "0.30.0", features = ["serde"], optional = true } +unicode-width = "0.1.10" [target.'cfg(windows)'.dependencies] winreg = "0.10.1" diff --git a/crates/nu-command/src/conversions/fill.rs b/crates/nu-command/src/conversions/fill.rs new file mode 100644 index 000000000..6ea2af5cc --- /dev/null +++ b/crates/nu-command/src/conversions/fill.rs @@ -0,0 +1,268 @@ +use crate::input_handler::{operate, CmdArgument}; +use nu_engine::CallExt; +use nu_protocol::{ + ast::{Call, CellPath}, + engine::{Command, EngineState, Stack}, + Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value, +}; +use unicode_width::UnicodeWidthStr; + +#[derive(Clone)] +pub struct Fill; + +struct Arguments { + width: usize, + alignment: FillAlignment, + character: String, + cell_paths: Option>, +} + +impl CmdArgument for Arguments { + fn take_cell_paths(&mut self) -> Option> { + self.cell_paths.take() + } +} + +#[derive(Clone, Copy)] +enum FillAlignment { + Left, + Right, + Middle, + MiddleRight, +} + +impl Command for Fill { + fn name(&self) -> &str { + "fill" + } + + fn usage(&self) -> &str { + "Fill and Align" + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("fill") + .input_output_types(vec![ + (Type::Int, Type::String), + (Type::Float, Type::String), + (Type::String, Type::String), + (Type::Filesize, Type::String), + ]) + .vectorizes_over_list(true) + .named( + "width", + SyntaxShape::Int, + "The width of the output. Defaults to 1", + Some('w'), + ) + .named( + "alignment", + SyntaxShape::String, + "The alignment of the output. Defaults to Left (Left(l), Right(r), Center(c/m), MiddleRight(cr/mr))", + Some('a'), + ) + .named( + "character", + SyntaxShape::String, + "The character to fill with. Defaults to ' ' (space)", + Some('c'), + ) + .category(Category::Conversions) + } + + fn search_terms(&self) -> Vec<&str> { + vec!["display", "render", "format", "pad", "align"] + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: + "Fill a string on the left side to a width of 15 with the character '─'", + example: "'nushell' | fill -a l -c '─' -w 15", + result: Some(Value::String { + val: "nushell────────".into(), + span: Span::test_data(), + }), + }, + Example { + description: + "Fill a string on the right side to a width of 15 with the character '─'", + example: "'nushell' | fill -a r -c '─' -w 15", + result: Some(Value::String { + val: "────────nushell".into(), + span: Span::test_data(), + }), + }, + Example { + description: "Fill a string on both sides to a width of 15 with the character '─'", + example: "'nushell' | fill -a m -c '─' -w 15", + result: Some(Value::String { + val: "────nushell────".into(), + span: Span::test_data(), + }), + }, + Example { + description: + "Fill a number on the left side to a width of 5 with the character '0'", + example: "1 | fill --alignment right --character 0 --width 5", + result: Some(Value::String { + val: "00001".into(), + span: Span::test_data(), + }), + }, + Example { + description: "Fill a number on both sides to a width of 5 with the character '0'", + example: "1.1 | fill --alignment center --character 0 --width 5", + result: Some(Value::String { + val: "01.10".into(), + span: Span::test_data(), + }), + }, + Example { + description: + "Fill a filesize on the left side to a width of 5 with the character '0'", + example: "1kib | fill --alignment middle --character 0 --width 10", + result: Some(Value::String { + val: "0001024000".into(), + span: Span::test_data(), + }), + }, + ] + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, + ) -> Result { + fill(engine_state, stack, call, input) + } +} + +fn fill( + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + input: PipelineData, +) -> Result { + let width_arg: Option = call.get_flag(engine_state, stack, "width")?; + let alignment_arg: Option = call.get_flag(engine_state, stack, "alignment")?; + let character_arg: Option = call.get_flag(engine_state, stack, "character")?; + let cell_paths: Vec = call.rest(engine_state, stack, 0)?; + let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); + + let alignment = if let Some(arg) = alignment_arg { + match arg.to_lowercase().as_str() { + "l" | "left" => FillAlignment::Left, + "r" | "right" => FillAlignment::Right, + "c" | "center" | "m" | "middle" => FillAlignment::Middle, + "cr" | "centerright" | "mr" | "middleright" => FillAlignment::MiddleRight, + _ => FillAlignment::Left, + } + } else { + FillAlignment::Left + }; + + let width = if let Some(arg) = width_arg { arg } else { 1 }; + + let character = if let Some(arg) = character_arg { + arg + } else { + " ".to_string() + }; + + let arg = Arguments { + width, + alignment, + character, + cell_paths, + }; + + operate(action, arg, input, call.head, engine_state.ctrlc.clone()) +} + +fn action(input: &Value, args: &Arguments, span: Span) -> Value { + match input { + Value::Int { val, .. } => fill_int(*val, args, span), + Value::Filesize { val, .. } => fill_int(*val, args, span), + Value::Float { val, .. } => fill_float(*val, args, span), + Value::String { val, .. } => fill_string(val, args, span), + // Propagate errors by explicitly matching them before the final case. + Value::Error { .. } => input.clone(), + other => Value::Error { + error: ShellError::OnlySupportsThisInputType( + "int, filesize, float, string".into(), + other.get_type().to_string(), + span, + // This line requires the Value::Error match above. + other.expect_span(), + ), + }, + } +} + +fn fill_float(num: f64, args: &Arguments, span: Span) -> Value { + let s = num.to_string(); + let out_str = pad(&s, args.width, &args.character, args.alignment, false); + + Value::String { val: out_str, span } +} +fn fill_int(num: i64, args: &Arguments, span: Span) -> Value { + let s = num.to_string(); + let out_str = pad(&s, args.width, &args.character, args.alignment, false); + + Value::String { val: out_str, span } +} +fn fill_string(s: &str, args: &Arguments, span: Span) -> Value { + let out_str = pad(s, args.width, &args.character, args.alignment, false); + + Value::String { val: out_str, span } +} + +fn pad(s: &str, width: usize, pad_char: &str, alignment: FillAlignment, truncate: bool) -> String { + // Attribution: Most of this function was taken from https://github.com/ogham/rust-pad and tweaked. Thank you! + // Use width instead of len for graphical display + let cols = UnicodeWidthStr::width(s); + + if cols >= width { + if truncate { + return s[..width].to_string(); + } else { + return s.to_string(); + } + } + + let diff = width - cols; + + let (left_pad, right_pad) = match alignment { + FillAlignment::Left => (0, diff), + FillAlignment::Right => (diff, 0), + FillAlignment::Middle => (diff / 2, diff - diff / 2), + FillAlignment::MiddleRight => (diff - diff / 2, diff / 2), + }; + + let mut new_str = String::new(); + for _ in 0..left_pad { + new_str.push_str(pad_char) + } + new_str.push_str(s); + for _ in 0..right_pad { + new_str.push_str(pad_char) + } + new_str +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_examples() { + use crate::test_examples; + + test_examples(Fill {}) + } +} diff --git a/crates/nu-command/src/conversions/mod.rs b/crates/nu-command/src/conversions/mod.rs index 38570d51b..8f29788e7 100644 --- a/crates/nu-command/src/conversions/mod.rs +++ b/crates/nu-command/src/conversions/mod.rs @@ -1,5 +1,7 @@ +mod fill; mod fmt; pub(crate) mod into; +pub use fill::Fill; pub use fmt::Fmt; pub use into::*; diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index ced738c17..6ffadef4a 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -227,10 +227,8 @@ pub fn create_default_context() -> EngineState { StrIndexOf, StrKebabCase, StrLength, - StrLpad, StrPascalCase, StrReverse, - StrRpad, StrScreamingSnakeCase, StrSnakeCase, StrStartsWith, @@ -370,6 +368,7 @@ pub fn create_default_context() -> EngineState { // Conversions bind_command! { + Fill, Fmt, Into, IntoBool, @@ -484,6 +483,8 @@ pub fn create_default_context() -> EngineState { // Deprecated bind_command! { HashBase64, + LPadDeprecated, + RPadDeprecated, Source, StrDatetimeDeprecated, StrDecimalDeprecated, diff --git a/crates/nu-command/src/deprecated/deprecated_commands.rs b/crates/nu-command/src/deprecated/deprecated_commands.rs index 28fc07914..a7d526d93 100644 --- a/crates/nu-command/src/deprecated/deprecated_commands.rs +++ b/crates/nu-command/src/deprecated/deprecated_commands.rs @@ -20,5 +20,7 @@ pub fn deprecated_commands() -> HashMap { ), ("fetch".to_string(), "http get".to_string()), ("post".to_string(), "http post".to_string()), + ("str lpad".to_string(), "fill".to_string()), + ("str rpad".to_string(), "fill".to_string()), ]) } diff --git a/crates/nu-command/src/deprecated/lpad.rs b/crates/nu-command/src/deprecated/lpad.rs new file mode 100644 index 000000000..b2d5f8a0e --- /dev/null +++ b/crates/nu-command/src/deprecated/lpad.rs @@ -0,0 +1,35 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::{PipelineData, ShellError, Signature}; + +#[derive(Clone)] +pub struct LPadDeprecated; + +impl Command for LPadDeprecated { + fn name(&self) -> &str { + "str lpad" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Deprecated) + } + + fn usage(&self) -> &str { + "Deprecated command" + } + + fn run( + &self, + _: &EngineState, + _: &mut Stack, + call: &Call, + _: PipelineData, + ) -> Result { + Err(nu_protocol::ShellError::DeprecatedCommand( + self.name().to_string(), + "fill".to_owned(), + call.head, + )) + } +} diff --git a/crates/nu-command/src/deprecated/mod.rs b/crates/nu-command/src/deprecated/mod.rs index 4f9373b91..c5759db63 100644 --- a/crates/nu-command/src/deprecated/mod.rs +++ b/crates/nu-command/src/deprecated/mod.rs @@ -1,6 +1,8 @@ mod deprecated_commands; mod hash_base64; +mod lpad; mod math_eval; +mod rpad; mod source; mod str_datetime; mod str_decimal; @@ -9,7 +11,9 @@ mod str_int; pub use deprecated_commands::*; pub use hash_base64::HashBase64; +pub use lpad::LPadDeprecated; pub use math_eval::SubCommand as MathEvalDeprecated; +pub use rpad::RPadDeprecated; pub use source::Source; pub use str_datetime::StrDatetimeDeprecated; pub use str_decimal::StrDecimalDeprecated; diff --git a/crates/nu-command/src/deprecated/rpad.rs b/crates/nu-command/src/deprecated/rpad.rs new file mode 100644 index 000000000..957b51d99 --- /dev/null +++ b/crates/nu-command/src/deprecated/rpad.rs @@ -0,0 +1,35 @@ +use nu_protocol::ast::Call; +use nu_protocol::engine::{Command, EngineState, Stack}; +use nu_protocol::Category; +use nu_protocol::{PipelineData, ShellError, Signature}; + +#[derive(Clone)] +pub struct RPadDeprecated; + +impl Command for RPadDeprecated { + fn name(&self) -> &str { + "str rpad" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).category(Category::Deprecated) + } + + fn usage(&self) -> &str { + "Deprecated command" + } + + fn run( + &self, + _: &EngineState, + _: &mut Stack, + call: &Call, + _: PipelineData, + ) -> Result { + Err(nu_protocol::ShellError::DeprecatedCommand( + self.name().to_string(), + "fill".to_owned(), + call.head, + )) + } +} diff --git a/crates/nu-command/src/strings/str_/lpad.rs b/crates/nu-command/src/strings/str_/lpad.rs deleted file mode 100644 index a4d65fa40..000000000 --- a/crates/nu-command/src/strings/str_/lpad.rs +++ /dev/null @@ -1,165 +0,0 @@ -use crate::input_handler::{operate, CmdArgument}; -use nu_engine::CallExt; -use nu_protocol::ast::Call; -use nu_protocol::ast::CellPath; -use nu_protocol::engine::{Command, EngineState, Stack}; -use nu_protocol::Category; -use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value}; - -struct Arguments { - length: Option, - character: Option, - cell_paths: Option>, -} - -impl CmdArgument for Arguments { - fn take_cell_paths(&mut self) -> Option> { - self.cell_paths.take() - } -} - -#[derive(Clone)] -pub struct SubCommand; - -impl Command for SubCommand { - fn name(&self) -> &str { - "str lpad" - } - - fn signature(&self) -> Signature { - Signature::build("str lpad") - .input_output_types(vec![(Type::String, Type::String)]) - .vectorizes_over_list(true) - .required_named("length", SyntaxShape::Int, "length to pad to", Some('l')) - .required_named( - "character", - SyntaxShape::String, - "character to pad with", - Some('c'), - ) - .rest( - "rest", - SyntaxShape::CellPath, - "For a data structure input, pad strings at the given cell paths", - ) - .category(Category::Strings) - } - - fn usage(&self) -> &str { - "Left-pad a string to a specific length" - } - - fn search_terms(&self) -> Vec<&str> { - vec!["append", "truncate", "padding"] - } - - fn run( - &self, - engine_state: &EngineState, - stack: &mut Stack, - call: &Call, - input: PipelineData, - ) -> Result { - let cell_paths: Vec = call.rest(engine_state, stack, 0)?; - let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); - let args = Arguments { - length: call.get_flag(engine_state, stack, "length")?, - character: call.get_flag(engine_state, stack, "character")?, - cell_paths, - }; - - if args.length.expect("this exists") < 0 { - return Err(ShellError::TypeMismatch( - String::from("The length of the string cannot be negative"), - call.head, - )); - } - operate(action, args, input, call.head, engine_state.ctrlc.clone()) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Left-pad a string with asterisks until it's 10 characters wide", - example: "'nushell' | str lpad -l 10 -c '*'", - result: Some(Value::test_string("***nushell")), - }, - Example { - description: "Left-pad a string with zeroes until it's 10 character wide", - example: "'123' | str lpad -l 10 -c '0'", - result: Some(Value::test_string("0000000123")), - }, - Example { - description: "Use lpad to truncate a string to its last three characters", - example: "'123456789' | str lpad -l 3 -c '0'", - result: Some(Value::test_string("789")), - }, - Example { - description: "Use lpad to pad Unicode", - example: "'▉' | str lpad -l 10 -c '▉'", - result: Some(Value::test_string("▉▉▉▉▉▉▉▉▉▉")), - }, - ] - } -} - -fn action( - input: &Value, - Arguments { - character, length, .. - }: &Arguments, - head: Span, -) -> Value { - match &input { - Value::String { val, .. } => match length { - Some(x) => { - let s = *x as usize; - if s < val.len() { - Value::String { - val: val - .chars() - .rev() - .take(s) - .collect::() - .chars() - .rev() - .collect::(), - span: head, - } - } else { - let c = character.as_ref().expect("we already know this flag needs to exist because the command is type checked before we call the action function"); - let mut res = c.repeat(s - val.chars().count()); - res += val; - Value::String { - val: res, - span: head, - } - } - } - None => Value::Error { - error: ShellError::TypeMismatch(String::from("Length argument is missing"), head), - }, - }, - Value::Error { .. } => input.clone(), - _ => Value::Error { - error: ShellError::OnlySupportsThisInputType( - "string".into(), - input.get_type().to_string(), - head, - input.expect_span(), - ), - }, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_examples() { - use crate::test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/crates/nu-command/src/strings/str_/mod.rs b/crates/nu-command/src/strings/str_/mod.rs index 26744205b..b97557e9e 100644 --- a/crates/nu-command/src/strings/str_/mod.rs +++ b/crates/nu-command/src/strings/str_/mod.rs @@ -6,10 +6,8 @@ mod ends_with; mod index_of; mod join; mod length; -mod lpad; mod replace; mod reverse; -mod rpad; mod starts_with; mod substring; mod trim; @@ -22,10 +20,8 @@ pub use ends_with::SubCommand as StrEndswith; pub use index_of::SubCommand as StrIndexOf; pub use join::*; pub use length::SubCommand as StrLength; -pub use lpad::SubCommand as StrLpad; pub use replace::SubCommand as StrReplace; pub use reverse::SubCommand as StrReverse; -pub use rpad::SubCommand as StrRpad; pub use starts_with::SubCommand as StrStartsWith; pub use substring::SubCommand as StrSubstring; pub use trim::Trim as StrTrim; diff --git a/crates/nu-command/src/strings/str_/rpad.rs b/crates/nu-command/src/strings/str_/rpad.rs deleted file mode 100644 index 1631b908b..000000000 --- a/crates/nu-command/src/strings/str_/rpad.rs +++ /dev/null @@ -1,157 +0,0 @@ -use crate::input_handler::{operate, CmdArgument}; -use nu_engine::CallExt; -use nu_protocol::ast::Call; -use nu_protocol::ast::CellPath; -use nu_protocol::engine::{Command, EngineState, Stack}; -use nu_protocol::Category; -use nu_protocol::{Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value}; - -struct Arguments { - length: Option, - character: Option, - cell_paths: Option>, -} - -impl CmdArgument for Arguments { - fn take_cell_paths(&mut self) -> Option> { - self.cell_paths.take() - } -} - -#[derive(Clone)] -pub struct SubCommand; - -impl Command for SubCommand { - fn name(&self) -> &str { - "str rpad" - } - - fn signature(&self) -> Signature { - Signature::build("str rpad") - .input_output_types(vec![(Type::String, Type::String)]) - .vectorizes_over_list(true) - .required_named("length", SyntaxShape::Int, "length to pad to", Some('l')) - .required_named( - "character", - SyntaxShape::String, - "character to pad with", - Some('c'), - ) - .rest( - "rest", - SyntaxShape::CellPath, - "For a data structure input, pad strings at the given cell paths", - ) - .category(Category::Strings) - } - - fn usage(&self) -> &str { - "Right-pad a string to a specific length" - } - - fn search_terms(&self) -> Vec<&str> { - vec!["append", "truncate", "padding"] - } - - fn run( - &self, - engine_state: &EngineState, - stack: &mut Stack, - call: &Call, - input: PipelineData, - ) -> Result { - let cell_paths: Vec = call.rest(engine_state, stack, 0)?; - let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); - let args = Arguments { - length: call.get_flag(engine_state, stack, "length")?, - character: call.get_flag(engine_state, stack, "character")?, - cell_paths, - }; - - if args.length.expect("this exists") < 0 { - return Err(ShellError::TypeMismatch( - String::from("The length of the string cannot be negative"), - call.head, - )); - } - operate(action, args, input, call.head, engine_state.ctrlc.clone()) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Right-pad a string with asterisks until it's 10 characters wide", - example: "'nushell' | str rpad -l 10 -c '*'", - result: Some(Value::test_string("nushell***")), - }, - Example { - description: "Right-pad a string with zeroes until it's 10 characters wide", - example: "'123' | str rpad -l 10 -c '0'", - result: Some(Value::test_string("1230000000")), - }, - Example { - description: "Use rpad to truncate a string to its first three characters", - example: "'123456789' | str rpad -l 3 -c '0'", - result: Some(Value::test_string("123")), - }, - Example { - description: "Use rpad to pad Unicode", - example: "'▉' | str rpad -l 10 -c '▉'", - result: Some(Value::test_string("▉▉▉▉▉▉▉▉▉▉")), - }, - ] - } -} - -fn action( - input: &Value, - Arguments { - character, length, .. - }: &Arguments, - head: Span, -) -> Value { - match &input { - Value::String { val, .. } => match length { - Some(x) => { - let s = *x as usize; - if s < val.len() { - Value::String { - val: val.chars().take(s).collect::(), - span: head, - } - } else { - let mut res = val.to_string(); - res += &character.as_ref().expect("we already know this flag needs to exist because the command is type checked before we call the action function").repeat(s - val.chars().count()); - Value::String { - val: res, - span: head, - } - } - } - None => Value::Error { - error: ShellError::TypeMismatch(String::from("Length argument is missing"), head), - }, - }, - Value::Error { .. } => input.clone(), - _ => Value::Error { - error: ShellError::OnlySupportsThisInputType( - "string".into(), - input.get_type().to_string(), - head, - input.expect_span(), - ), - }, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_examples() { - use crate::test_examples; - - test_examples(SubCommand {}) - } -} diff --git a/src/tests/test_parser.rs b/src/tests/test_parser.rs index 8edf3a5fa..334c6c66d 100644 --- a/src/tests/test_parser.rs +++ b/src/tests/test_parser.rs @@ -224,7 +224,7 @@ fn commands_have_usage() -> TestResult { #[test] fn equals_separates_long_flag() -> TestResult { run_test( - r#"'nushell' | str lpad --length=10 --character='-'"#, + r#"'nushell' | fill --alignment right --width=10 --character='-'"#, "---nushell", ) }