diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 9f8152343a..dd74893f73 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -411,6 +411,11 @@ pub fn create_default_context( whole_stream_command(RandomDice), #[cfg(feature = "uuid_crate")] whole_stream_command(RandomUUID), + // Path + whole_stream_command(PathCommand), + whole_stream_command(PathExtension), + whole_stream_command(PathBasename), + whole_stream_command(PathExpand), ]); #[cfg(feature = "clipboard")] diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index 9d6fb49dd5..9bf4c647f3 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -76,6 +76,7 @@ pub(crate) mod next; pub(crate) mod nth; pub(crate) mod open; pub(crate) mod parse; +pub(crate) mod path; pub(crate) mod pivot; pub(crate) mod plugin; pub(crate) mod prepend; @@ -203,6 +204,7 @@ pub(crate) use next::Next; pub(crate) use nth::Nth; pub(crate) use open::Open; pub(crate) use parse::Parse; +pub(crate) use path::{PathBasename, PathCommand, PathExpand, PathExtension}; pub(crate) use pivot::Pivot; pub(crate) use prepend::Prepend; pub(crate) use prev::Previous; diff --git a/crates/nu-cli/src/commands/path/basename.rs b/crates/nu-cli/src/commands/path/basename.rs new file mode 100644 index 0000000000..59210d41d7 --- /dev/null +++ b/crates/nu-cli/src/commands/path/basename.rs @@ -0,0 +1,60 @@ +use super::{operate, DefaultArguments}; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; +use std::path::Path; + +pub struct PathBasename; + +#[async_trait] +impl WholeStreamCommand for PathBasename { + fn name(&self) -> &str { + "path basename" + } + + fn signature(&self) -> Signature { + Signature::build("path basename") + .rest(SyntaxShape::ColumnPath, "optionally operate by path") + } + + fn usage(&self) -> &str { + "gets the filename of a path" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let (DefaultArguments { rest }, input) = args.process(®istry).await?; + operate(input, rest, &action).await + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Get basename of a path", + example: "echo '/home/joe/test.txt' | path basename", + result: Some(vec![Value::from("test.txt")]), + }] + } +} + +fn action(path: &Path) -> UntaggedValue { + UntaggedValue::string(match path.file_name() { + Some(filename) => filename.to_string_lossy().to_string(), + _ => "".to_string(), + }) +} + +#[cfg(test)] +mod tests { + use super::PathBasename; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(PathBasename {}) + } +} diff --git a/crates/nu-cli/src/commands/path/command.rs b/crates/nu-cli/src/commands/path/command.rs new file mode 100644 index 0000000000..17d3452e09 --- /dev/null +++ b/crates/nu-cli/src/commands/path/command.rs @@ -0,0 +1,46 @@ +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; + +pub struct Path; + +#[async_trait] +impl WholeStreamCommand for Path { + fn name(&self) -> &str { + "path" + } + + fn signature(&self) -> Signature { + Signature::build("path") + } + + fn usage(&self) -> &str { + "Apply path function" + } + + async fn run( + &self, + _args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let registry = registry.clone(); + + Ok(OutputStream::one(ReturnSuccess::value( + UntaggedValue::string(crate::commands::help::get_help(&Path, ®istry)) + .into_value(Tag::unknown()), + ))) + } +} + +#[cfg(test)] +mod tests { + use super::Path; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(Path {}) + } +} diff --git a/crates/nu-cli/src/commands/path/expand.rs b/crates/nu-cli/src/commands/path/expand.rs new file mode 100644 index 0000000000..7d75bcfb24 --- /dev/null +++ b/crates/nu-cli/src/commands/path/expand.rs @@ -0,0 +1,50 @@ +use super::{operate, DefaultArguments}; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; +use std::path::Path; + +pub struct PathExpand; + +#[async_trait] +impl WholeStreamCommand for PathExpand { + fn name(&self) -> &str { + "path expand" + } + + fn signature(&self) -> Signature { + Signature::build("path expand").rest(SyntaxShape::ColumnPath, "optionally operate by path") + } + + fn usage(&self) -> &str { + "expands the path to its absolute form" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let (DefaultArguments { rest }, input) = args.process(®istry).await?; + operate(input, rest, &action).await + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Expand relative directories", + example: "echo '/home/joe/foo/../bar' | path expand", + result: Some(vec![Value::from("/home/joe/bar")]), + }] + } +} + +fn action(path: &Path) -> UntaggedValue { + let ps = path.to_string_lossy(); + let expanded = shellexpand::tilde(&ps); + let path: &Path = expanded.as_ref().as_ref(); + UntaggedValue::string(match path.canonicalize() { + Ok(p) => p.to_string_lossy().to_string(), + Err(_) => ps.to_string(), + }) +} diff --git a/crates/nu-cli/src/commands/path/extension.rs b/crates/nu-cli/src/commands/path/extension.rs new file mode 100644 index 0000000000..0a43144cce --- /dev/null +++ b/crates/nu-cli/src/commands/path/extension.rs @@ -0,0 +1,67 @@ +use super::{operate, DefaultArguments}; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; +use std::path::Path; + +pub struct PathExtension; + +#[async_trait] +impl WholeStreamCommand for PathExtension { + fn name(&self) -> &str { + "path extension" + } + + fn signature(&self) -> Signature { + Signature::build("path extension") + .rest(SyntaxShape::ColumnPath, "optionally operate by path") + } + + fn usage(&self) -> &str { + "gets the extension of a path" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + let (DefaultArguments { rest }, input) = args.process(®istry).await?; + operate(input, rest, &action).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get extension of a path", + example: "echo 'test.txt' | path extension", + result: Some(vec![Value::from("txt")]), + }, + Example { + description: "You get an empty string if there is no extension", + example: "echo 'test' | path extension", + result: Some(vec![Value::from("")]), + }, + ] + } +} + +fn action(path: &Path) -> UntaggedValue { + UntaggedValue::string(match path.extension() { + Some(ext) => ext.to_string_lossy().to_string(), + _ => "".to_string(), + }) +} + +#[cfg(test)] +mod tests { + use super::PathExtension; + + #[test] + fn examples_work_as_expected() { + use crate::examples::test as test_examples; + + test_examples(PathExtension {}) + } +} diff --git a/crates/nu-cli/src/commands/path/mod.rs b/crates/nu-cli/src/commands/path/mod.rs new file mode 100644 index 0000000000..2285e2a4e6 --- /dev/null +++ b/crates/nu-cli/src/commands/path/mod.rs @@ -0,0 +1,67 @@ +mod basename; +mod command; +mod expand; +mod extension; + +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ColumnPath, Primitive, ReturnSuccess, ShellTypeName, UntaggedValue, Value}; +use std::path::Path; + +pub use basename::PathBasename; +pub use command::Path as PathCommand; +pub use expand::PathExpand; +pub use extension::PathExtension; + +#[derive(Deserialize)] +struct DefaultArguments { + rest: Vec, +} + +fn handle_value(action: &F, v: &Value) -> Result +where + F: Fn(&Path) -> UntaggedValue + Send + 'static, +{ + let v = match &v.value { + UntaggedValue::Primitive(Primitive::Path(buf)) => action(buf).into_value(v.tag()), + UntaggedValue::Primitive(Primitive::String(s)) + | UntaggedValue::Primitive(Primitive::Line(s)) => action(s.as_ref()).into_value(v.tag()), + other => { + let got = format!("got {}", other.type_name()); + return Err(ShellError::labeled_error( + "value is not string or path", + got, + v.tag().span, + )); + } + }; + Ok(v) +} + +async fn operate( + input: crate::InputStream, + paths: Vec, + action: &'static F, +) -> Result +where + F: Fn(&Path) -> UntaggedValue + Send + Sync + 'static, +{ + Ok(input + .map(move |v| { + if paths.is_empty() { + ReturnSuccess::value(handle_value(&action, &v)?) + } else { + let mut ret = v; + + for path in &paths { + ret = ret.swap_data_by_column_path( + path, + Box::new(move |old| handle_value(&action, &old)), + )?; + } + + ReturnSuccess::value(ret) + } + }) + .to_output_stream()) +}