From d90420ac4c68ff5221e8f747f98c49b04e2c7a5f Mon Sep 17 00:00:00 2001 From: Lily Mara <6109875+lily-mara@users.noreply.github.com> Date: Thu, 2 Sep 2021 16:19:54 -0700 Subject: [PATCH] Add subcommand `into filesize` (#3987) * Add subcommand `into filesize` It's currently not possible to convert a number or a string containing a number into a filesize. The only way to create an instance of filesize type today is with a literal in nushell syntax. This commit adds the `into filesize` subcommand so that file sizes can be created from the outputs of programs producing numbers or strings, like standard unix tools. There is a limitation with this - it doesn't currently parse values like `10 MB` or `10 MiB`, it can only look at the number itself. If the desire is there, more flexible parsing can be added. * fixup! Add subcommand `into filesize` * fixup! Add subcommand `into filesize` --- .../src/commands/conversions/into/filesize.rs | 182 ++++++++++++++++++ .../src/commands/conversions/into/mod.rs | 2 + crates/nu-command/src/default_context.rs | 1 + .../tests/commands/into_filesize.rs | 76 ++++++++ crates/nu-command/tests/commands/mod.rs | 1 + 5 files changed, 262 insertions(+) create mode 100644 crates/nu-command/src/commands/conversions/into/filesize.rs create mode 100644 crates/nu-command/tests/commands/into_filesize.rs diff --git a/crates/nu-command/src/commands/conversions/into/filesize.rs b/crates/nu-command/src/commands/conversions/into/filesize.rs new file mode 100644 index 0000000000..7a3a648341 --- /dev/null +++ b/crates/nu-command/src/commands/conversions/into/filesize.rs @@ -0,0 +1,182 @@ +use std::convert::TryInto; + +use crate::prelude::*; +use nu_engine::WholeStreamCommand; +use nu_errors::ShellError; +use nu_protocol::{ColumnPath, Primitive, Signature, SyntaxShape, UntaggedValue, Value}; +use num_bigint::ToBigInt; + +pub struct SubCommand; + +impl WholeStreamCommand for SubCommand { + fn name(&self) -> &str { + "into filesize" + } + + fn signature(&self) -> Signature { + Signature::build("into filesize").rest( + "rest", + SyntaxShape::ColumnPath, + "column paths to convert to filesize (for table input)", + ) + } + + fn usage(&self) -> &str { + "Convert value to filesize" + } + + fn run(&self, args: CommandArgs) -> Result { + into_filesize(args) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Convert string to filesize in table", + example: "echo [[bytes]; ['5'] [3.2] [4] [2kb]] | into filesize bytes", + result: Some(vec![ + UntaggedValue::row(indexmap! { + "bytes".to_string() => UntaggedValue::filesize(5).into(), + }) + .into(), + UntaggedValue::row(indexmap! { + "bytes".to_string() => UntaggedValue::filesize(3).into(), + }) + .into(), + UntaggedValue::row(indexmap! { + "bytes".to_string() => UntaggedValue::filesize(4).into(), + }) + .into(), + UntaggedValue::row(indexmap! { + "bytes".to_string() => UntaggedValue::filesize(2000).into(), + }) + .into(), + ]), + }, + Example { + description: "Convert string to filesize", + example: "echo '2' | into filesize", + result: Some(vec![UntaggedValue::filesize(2).into()]), + }, + Example { + description: "Convert decimal to filesize", + example: "echo 8.3 | into filesize", + result: Some(vec![UntaggedValue::filesize(8).into()]), + }, + Example { + description: "Convert int to filesize", + example: "echo 5 | into filesize", + result: Some(vec![UntaggedValue::filesize(5).into()]), + }, + Example { + description: "Convert file size to filesize", + example: "echo 4KB | into filesize", + result: Some(vec![UntaggedValue::filesize(4000).into()]), + }, + ] + } +} + +fn into_filesize(args: CommandArgs) -> Result { + let column_paths: Vec = args.rest(0)?; + + Ok(args + .input + .map(move |v| { + if column_paths.is_empty() { + action(&v, v.tag()) + } else { + let mut ret = v; + for path in &column_paths { + ret = ret.swap_data_by_column_path( + path, + Box::new(move |old| action(old, old.tag())), + )?; + } + + Ok(ret) + } + }) + .into_input_stream()) +} + +pub fn action(input: &Value, tag: impl Into) -> Result { + let tag = tag.into(); + match &input.value { + UntaggedValue::Primitive(prim) => Ok(UntaggedValue::filesize(match prim { + Primitive::String(a_string) => match int_from_string(a_string.trim(), &tag) { + Ok(n) => n, + Err(e) => { + return Err(e); + } + }, + Primitive::Decimal(dec) => match dec.to_bigint() { + Some(n) => match n.to_u64() { + Some(i) => i, + None => { + return Err(ShellError::unimplemented( + "failed to convert decimal to filesize", + )); + } + }, + None => { + return Err(ShellError::unimplemented( + "failed to convert decimal to filesize", + )); + } + }, + Primitive::Int(n_ref) => (*n_ref).try_into().map_err(|_| { + ShellError::unimplemented("cannot convert negative integer to filesize") + })?, + Primitive::Filesize(a_filesize) => *a_filesize, + _ => { + return Err(ShellError::unimplemented( + "'into filesize' for non-numeric primitives", + )) + } + }) + .into_value(&tag)), + UntaggedValue::Row(_) => Err(ShellError::labeled_error( + "specify column name to use, with 'into filesize COLUMN'", + "found table", + tag, + )), + _ => Err(ShellError::unimplemented( + "'into filesize' for unsupported type", + )), + } +} + +fn int_from_string(a_string: &str, tag: &Tag) -> Result { + match a_string.parse::() { + Ok(n) => Ok(n), + Err(_) => match a_string.parse::() { + Ok(f) => match f.to_u64() { + Some(i) => Ok(i), + None => Err(ShellError::labeled_error( + "Could not convert string value to filesize", + "original value", + tag.clone(), + )), + }, + Err(_) => Err(ShellError::labeled_error( + "Could not convert string value to filesize", + "original value", + tag.clone(), + )), + }, + } +} + +#[cfg(test)] +mod tests { + use super::ShellError; + use super::SubCommand; + + #[test] + fn examples_work_as_expected() -> Result<(), ShellError> { + use crate::examples::test as test_examples; + + test_examples(SubCommand {}) + } +} diff --git a/crates/nu-command/src/commands/conversions/into/mod.rs b/crates/nu-command/src/commands/conversions/into/mod.rs index efd01476cf..949c36a7b5 100644 --- a/crates/nu-command/src/commands/conversions/into/mod.rs +++ b/crates/nu-command/src/commands/conversions/into/mod.rs @@ -1,9 +1,11 @@ mod binary; mod command; mod filepath; +mod filesize; mod int; pub mod string; +pub use self::filesize::SubCommand as IntoFilesize; pub use binary::SubCommand as IntoBinary; pub use command::Command as Into; pub use filepath::SubCommand as IntoFilepath; diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index e86c404b07..0aa0995b4e 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -137,6 +137,7 @@ pub fn create_default_context(interactive: bool) -> Result