diff --git a/crates/nu-cli/src/commands/path/basename.rs b/crates/nu-cli/src/commands/path/basename.rs index eaa4b1b7b8..56d279befa 100644 --- a/crates/nu-cli/src/commands/path/basename.rs +++ b/crates/nu-cli/src/commands/path/basename.rs @@ -2,11 +2,18 @@ use super::{operate, DefaultArguments}; use crate::commands::WholeStreamCommand; use crate::prelude::*; use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; +use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; +use nu_source::Tagged; use std::path::Path; pub struct PathBasename; +#[derive(Deserialize)] +struct PathBasenameArguments { + replace: Option>, + rest: Vec, +} + #[async_trait] impl WholeStreamCommand for PathBasename { fn name(&self) -> &str { @@ -15,11 +22,17 @@ impl WholeStreamCommand for PathBasename { fn signature(&self) -> Signature { Signature::build("path basename") - .rest(SyntaxShape::ColumnPath, "optionally operate by path") + .named( + "replace", + SyntaxShape::String, + "Return original path with basename replaced by this string", + Some('r'), + ) + .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") } fn usage(&self) -> &str { - "gets the filename of a path" + "Gets the final component of a path" } async fn run( @@ -28,24 +41,60 @@ impl WholeStreamCommand for PathBasename { registry: &CommandRegistry, ) -> Result { let tag = args.call_info.name_tag.clone(); - let (DefaultArguments { rest }, input) = args.process(®istry).await?; - operate(input, rest, &action, tag.span).await + let (PathBasenameArguments { replace, rest }, input) = args.process(®istry).await?; + let args = Arc::new(DefaultArguments { + replace: replace.map(|v| v.item), + prefix: None, + suffix: None, + num_levels: None, + paths: rest, + }); + operate(input, &action, tag.span, args).await } + #[cfg(windows)] 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")]), - }] + vec![ + Example { + description: "Get basename of a path", + example: "echo 'C:\\Users\\joe\\test.txt' | path basename", + result: Some(vec![Value::from("test.txt")]), + }, + Example { + description: "Replace basename of a path", + example: "echo 'C:\\Users\\joe\\test.txt' | path basename -r 'spam.png'", + result: Some(vec![Value::from(UntaggedValue::path( + "C:\\Users\\joe\\spam.png", + ))]), + }, + ] + } + + #[cfg(not(windows))] + 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")]), + }, + Example { + description: "Replace basename of a path", + example: "echo '/home/joe/test.txt' | path basename -r 'spam.png'", + result: Some(vec![Value::from(UntaggedValue::path("/home/joe/spam.png"))]), + }, + ] } } -fn action(path: &Path) -> UntaggedValue { - UntaggedValue::string(match path.file_name() { - Some(filename) => filename.to_string_lossy().to_string(), - _ => "".to_string(), - }) +fn action(path: &Path, args: Arc) -> UntaggedValue { + match args.replace { + Some(ref basename) => UntaggedValue::path(path.with_file_name(basename)), + None => UntaggedValue::string(match path.file_name() { + Some(filename) => filename.to_string_lossy(), + None => "".into(), + }), + } } #[cfg(test)] diff --git a/crates/nu-cli/src/commands/path/command.rs b/crates/nu-cli/src/commands/path/command.rs index 0e351a3037..5d623c204b 100644 --- a/crates/nu-cli/src/commands/path/command.rs +++ b/crates/nu-cli/src/commands/path/command.rs @@ -16,7 +16,7 @@ impl WholeStreamCommand for Path { } fn usage(&self) -> &str { - "Apply path function" + "Explore and manipulate paths" } async fn run( diff --git a/crates/nu-cli/src/commands/path/dirname.rs b/crates/nu-cli/src/commands/path/dirname.rs index 0788fe2422..6ca8f3bf43 100644 --- a/crates/nu-cli/src/commands/path/dirname.rs +++ b/crates/nu-cli/src/commands/path/dirname.rs @@ -2,11 +2,20 @@ use super::{operate, DefaultArguments}; use crate::commands::WholeStreamCommand; use crate::prelude::*; use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; +use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; +use nu_source::Tagged; use std::path::Path; pub struct PathDirname; +#[derive(Deserialize)] +struct PathDirnameArguments { + replace: Option>, + #[serde(rename = "num-levels")] + num_levels: Option>, + rest: Vec, +} + #[async_trait] impl WholeStreamCommand for PathDirname { fn name(&self) -> &str { @@ -14,11 +23,24 @@ impl WholeStreamCommand for PathDirname { } fn signature(&self) -> Signature { - Signature::build("path dirname").rest(SyntaxShape::ColumnPath, "optionally operate by path") + Signature::build("path dirname") + .named( + "replace", + SyntaxShape::String, + "Return original path with dirname replaced by this string", + Some('r'), + ) + .named( + "num-levels", + SyntaxShape::Int, + "Number of directories to walk up", + Some('n'), + ) + .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") } fn usage(&self) -> &str { - "gets the dirname of a path" + "Gets the parent directory of a path" } async fn run( @@ -27,24 +49,100 @@ impl WholeStreamCommand for PathDirname { registry: &CommandRegistry, ) -> Result { let tag = args.call_info.name_tag.clone(); - let (DefaultArguments { rest }, input) = args.process(®istry).await?; - operate(input, rest, &action, tag.span).await + let ( + PathDirnameArguments { + replace, + num_levels, + rest, + }, + input, + ) = args.process(®istry).await?; + let args = Arc::new(DefaultArguments { + replace: replace.map(|v| v.item), + prefix: None, + suffix: None, + num_levels: num_levels.map(|v| v.item), + paths: rest, + }); + operate(input, &action, tag.span, args).await } + #[cfg(windows)] fn examples(&self) -> Vec { - vec![Example { - description: "Get dirname of a path", - example: "echo '/home/joe/test.txt' | path dirname", - result: Some(vec![Value::from("/home/joe")]), - }] + vec![ + Example { + description: "Get dirname of a path", + example: "echo 'C:\\Users\\joe\\code\\test.txt' | path dirname", + result: Some(vec![Value::from(UntaggedValue::path( + "C:\\Users\\joe\\code", + ))]), + }, + Example { + description: "Set how many levels up to skip", + example: "echo 'C:\\Users\\joe\\code\\test.txt' | path dirname -n 2", + result: Some(vec![Value::from(UntaggedValue::path("C:\\Users\\joe"))]), + }, + Example { + description: "Replace the part that would be returned with custom string", + example: + "echo 'C:\\Users\\joe\\code\\test.txt' | path dirname -n 2 -r C:\\Users\\viking", + result: Some(vec![Value::from(UntaggedValue::path( + "C:\\Users\\viking\\code\\test.txt", + ))]), + }, + ] + } + + #[cfg(not(windows))] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get dirname of a path", + example: "echo '/home/joe/code/test.txt' | path dirname", + result: Some(vec![Value::from(UntaggedValue::path("/home/joe/code"))]), + }, + Example { + description: "Set how many levels up to skip", + example: "echo '/home/joe/code/test.txt' | path dirname -n 2", + result: Some(vec![Value::from(UntaggedValue::path("/home/joe"))]), + }, + Example { + description: "Replace the part that would be returned with custom string", + example: "echo '/home/joe/code/test.txt' | path dirname -n 2 -r /home/viking", + result: Some(vec![Value::from(UntaggedValue::path( + "/home/viking/code/test.txt", + ))]), + }, + ] } } -fn action(path: &Path) -> UntaggedValue { - UntaggedValue::string(match path.parent() { - Some(dirname) => dirname.to_string_lossy().to_string(), - _ => "".to_string(), - }) +fn action(path: &Path, args: Arc) -> UntaggedValue { + let num_levels = args.num_levels.unwrap_or(1); + + let mut dirname = path; + let mut reached_top = false; // end early if somebody passes -n 99999999 + for _ in 0..num_levels { + dirname = dirname.parent().unwrap_or_else(|| { + reached_top = true; + dirname + }); + if reached_top { + break; + } + } + + match args.replace { + Some(ref newdir) => { + let remainder = path.strip_prefix(dirname).unwrap_or(dirname); + if !remainder.as_os_str().is_empty() { + UntaggedValue::path(Path::new(newdir).join(remainder)) + } else { + UntaggedValue::path(Path::new(newdir)) + } + } + None => UntaggedValue::path(dirname), + } } #[cfg(test)] diff --git a/crates/nu-cli/src/commands/path/exists.rs b/crates/nu-cli/src/commands/path/exists.rs index d6926c77fe..c3f62fdba9 100644 --- a/crates/nu-cli/src/commands/path/exists.rs +++ b/crates/nu-cli/src/commands/path/exists.rs @@ -2,11 +2,16 @@ use super::{operate, DefaultArguments}; use crate::commands::WholeStreamCommand; use crate::prelude::*; use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; +use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; use std::path::Path; pub struct PathExists; +#[derive(Deserialize)] +struct PathExistsArguments { + rest: Vec, +} + #[async_trait] impl WholeStreamCommand for PathExists { fn name(&self) -> &str { @@ -14,11 +19,12 @@ impl WholeStreamCommand for PathExists { } fn signature(&self) -> Signature { - Signature::build("path exists").rest(SyntaxShape::ColumnPath, "optionally operate by path") + Signature::build("path exists") + .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") } fn usage(&self) -> &str { - "checks whether the path exists" + "Checks whether a path exists" } async fn run( @@ -27,10 +33,27 @@ impl WholeStreamCommand for PathExists { registry: &CommandRegistry, ) -> Result { let tag = args.call_info.name_tag.clone(); - let (DefaultArguments { rest }, input) = args.process(®istry).await?; - operate(input, rest, &action, tag.span).await + let (PathExistsArguments { rest }, input) = args.process(®istry).await?; + let args = Arc::new(DefaultArguments { + replace: None, + prefix: None, + suffix: None, + num_levels: None, + paths: rest, + }); + operate(input, &action, tag.span, args).await } + #[cfg(windows)] + fn examples(&self) -> Vec { + vec![Example { + description: "Check if file exists", + example: "echo 'C:\\Users\\joe\\todo.txt' | path exists", + result: Some(vec![Value::from(UntaggedValue::boolean(false))]), + }] + } + + #[cfg(not(windows))] fn examples(&self) -> Vec { vec![Example { description: "Check if file exists", @@ -40,7 +63,7 @@ impl WholeStreamCommand for PathExists { } } -fn action(path: &Path) -> UntaggedValue { +fn action(path: &Path, _args: Arc) -> UntaggedValue { UntaggedValue::boolean(path.exists()) } diff --git a/crates/nu-cli/src/commands/path/expand.rs b/crates/nu-cli/src/commands/path/expand.rs index fbcb7b76fa..6c511de8b1 100644 --- a/crates/nu-cli/src/commands/path/expand.rs +++ b/crates/nu-cli/src/commands/path/expand.rs @@ -2,11 +2,16 @@ use super::{operate, DefaultArguments}; use crate::commands::WholeStreamCommand; use crate::prelude::*; use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue}; -use std::path::Path; +use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue}; +use std::path::{Path, PathBuf}; pub struct PathExpand; +#[derive(Deserialize)] +struct PathExpandArguments { + rest: Vec, +} + #[async_trait] impl WholeStreamCommand for PathExpand { fn name(&self) -> &str { @@ -14,11 +19,12 @@ impl WholeStreamCommand for PathExpand { } fn signature(&self) -> Signature { - Signature::build("path expand").rest(SyntaxShape::ColumnPath, "optionally operate by path") + Signature::build("path expand") + .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") } fn usage(&self) -> &str { - "expands the path to its absolute form" + "Expands a path to its absolute form" } async fn run( @@ -27,28 +33,43 @@ impl WholeStreamCommand for PathExpand { registry: &CommandRegistry, ) -> Result { let tag = args.call_info.name_tag.clone(); - let (DefaultArguments { rest }, input) = args.process(®istry).await?; - operate(input, rest, &action, tag.span).await + let (PathExpandArguments { rest }, input) = args.process(®istry).await?; + let args = Arc::new(DefaultArguments { + replace: None, + prefix: None, + suffix: None, + num_levels: None, + paths: rest, + }); + operate(input, &action, tag.span, args).await } + #[cfg(windows)] + fn examples(&self) -> Vec { + vec![Example { + description: "Expand relative directories", + example: "echo 'C:\\Users\\joe\\foo\\..\\bar' | path expand", + result: None, + // fails to canonicalize into Some(vec![Value::from("C:\\Users\\joe\\bar")]) due to non-existing path + }] + } + + #[cfg(not(windows))] fn examples(&self) -> Vec { vec![Example { description: "Expand relative directories", example: "echo '/home/joe/foo/../bar' | path expand", result: None, - //Some(vec![Value::from("/home/joe/bar")]), + // fails to canonicalize into Some(vec![Value::from("/home/joe/bar")]) due to non-existing path }] } } -fn action(path: &Path) -> UntaggedValue { +fn action(path: &Path, _args: Arc) -> 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(), - }) + UntaggedValue::path(dunce::canonicalize(path).unwrap_or_else(|_| PathBuf::from(path))) } #[cfg(test)] diff --git a/crates/nu-cli/src/commands/path/extension.rs b/crates/nu-cli/src/commands/path/extension.rs index fd36abf1e3..5d9517dad5 100644 --- a/crates/nu-cli/src/commands/path/extension.rs +++ b/crates/nu-cli/src/commands/path/extension.rs @@ -2,11 +2,18 @@ use super::{operate, DefaultArguments}; use crate::commands::WholeStreamCommand; use crate::prelude::*; use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; +use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; +use nu_source::Tagged; use std::path::Path; pub struct PathExtension; +#[derive(Deserialize)] +struct PathExtensionArguments { + replace: Option>, + rest: Vec, +} + #[async_trait] impl WholeStreamCommand for PathExtension { fn name(&self) -> &str { @@ -15,11 +22,17 @@ impl WholeStreamCommand for PathExtension { fn signature(&self) -> Signature { Signature::build("path extension") - .rest(SyntaxShape::ColumnPath, "optionally operate by path") + .named( + "replace", + SyntaxShape::String, + "Return original path with extension replaced by this string", + Some('r'), + ) + .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") } fn usage(&self) -> &str { - "gets the extension of a path" + "Gets the extension of a path" } async fn run( @@ -28,8 +41,15 @@ impl WholeStreamCommand for PathExtension { registry: &CommandRegistry, ) -> Result { let tag = args.call_info.name_tag.clone(); - let (DefaultArguments { rest }, input) = args.process(®istry).await?; - operate(input, rest, &action, tag.span).await + let (PathExtensionArguments { replace, rest }, input) = args.process(®istry).await?; + let args = Arc::new(DefaultArguments { + replace: replace.map(|v| v.item), + prefix: None, + suffix: None, + num_levels: None, + paths: rest, + }); + operate(input, &action, tag.span, args).await } fn examples(&self) -> Vec { @@ -44,15 +64,28 @@ impl WholeStreamCommand for PathExtension { example: "echo 'test' | path extension", result: Some(vec![Value::from("")]), }, + Example { + description: "Replace an extension with a custom string", + example: "echo 'test.txt' | path extension -r md", + result: Some(vec![Value::from(UntaggedValue::path("test.md"))]), + }, + Example { + description: "To replace more complex extensions:", + example: "echo 'test.tar.gz' | path extension -r '' | path extension -r txt", + result: Some(vec![Value::from(UntaggedValue::path("test.txt"))]), + }, ] } } -fn action(path: &Path) -> UntaggedValue { - UntaggedValue::string(match path.extension() { - Some(ext) => ext.to_string_lossy().to_string(), - _ => "".to_string(), - }) +fn action(path: &Path, args: Arc) -> UntaggedValue { + match args.replace { + Some(ref extension) => UntaggedValue::path(path.with_extension(extension)), + None => UntaggedValue::string(match path.extension() { + Some(extension) => extension.to_string_lossy(), + None => "".into(), + }), + } } #[cfg(test)] diff --git a/crates/nu-cli/src/commands/path/filestem.rs b/crates/nu-cli/src/commands/path/filestem.rs index 4a0caf1746..7e49a23c57 100644 --- a/crates/nu-cli/src/commands/path/filestem.rs +++ b/crates/nu-cli/src/commands/path/filestem.rs @@ -2,11 +2,20 @@ use super::{operate, DefaultArguments}; use crate::commands::WholeStreamCommand; use crate::prelude::*; use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; +use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; +use nu_source::Tagged; use std::path::Path; pub struct PathFilestem; +#[derive(Deserialize)] +struct PathFilestemArguments { + prefix: Option>, + suffix: Option>, + replace: Option>, + rest: Vec, +} + #[async_trait] impl WholeStreamCommand for PathFilestem { fn name(&self) -> &str { @@ -15,11 +24,29 @@ impl WholeStreamCommand for PathFilestem { fn signature(&self) -> Signature { Signature::build("path filestem") - .rest(SyntaxShape::ColumnPath, "optionally operate by path") + .named( + "replace", + SyntaxShape::String, + "Return original path with filestem replaced by this string", + Some('r'), + ) + .named( + "prefix", + SyntaxShape::String, + "Strip this string from from the beginning of a file name", + Some('p'), + ) + .named( + "suffix", + SyntaxShape::String, + "Strip this string from from the end of a file name", + Some('s'), + ) + .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") } fn usage(&self) -> &str { - "gets the filestem of a path" + "Gets the file stem of a path" } async fn run( @@ -28,24 +55,111 @@ impl WholeStreamCommand for PathFilestem { registry: &CommandRegistry, ) -> Result { let tag = args.call_info.name_tag.clone(); - let (DefaultArguments { rest }, input) = args.process(®istry).await?; - operate(input, rest, &action, tag.span).await + let ( + PathFilestemArguments { + replace, + prefix, + suffix, + rest, + }, + input, + ) = args.process(®istry).await?; + let args = Arc::new(DefaultArguments { + replace: replace.map(|v| v.item), + prefix: prefix.map(|v| v.item), + suffix: suffix.map(|v| v.item), + num_levels: None, + paths: rest, + }); + operate(input, &action, tag.span, args).await } + #[cfg(windows)] fn examples(&self) -> Vec { - vec![Example { - description: "Get filestem of a path", - example: "echo '/home/joe/test.txt' | path filestem", - result: Some(vec![Value::from("test")]), - }] + vec![ + Example { + description: "Get filestem of a path", + example: "echo 'C:\\Users\\joe\\bacon_lettuce.egg' | path filestem", + result: Some(vec![Value::from("bacon_lettuce")]), + }, + Example { + description: "Get filestem of a path, stripped of prefix and suffix", + example: "echo 'C:\\Users\\joe\\bacon_lettuce.egg.gz' | path filestem -p bacon_ -s .egg.gz", + result: Some(vec![Value::from("lettuce")]), + }, + Example { + description: "Replace the filestem that would be returned", + example: "echo 'C:\\Users\\joe\\bacon_lettuce.egg.gz' | path filestem -p bacon_ -s .egg.gz -r spam", + result: Some(vec![Value::from(UntaggedValue::path("C:\\Users\\joe\\bacon_spam.egg.gz"))]), + }, + ] + } + + #[cfg(not(windows))] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Get filestem of a path", + example: "echo '/home/joe/bacon_lettuce.egg' | path filestem", + result: Some(vec![Value::from("bacon_lettuce")]), + }, + Example { + description: "Get filestem of a path, stripped of prefix and suffix", + example: "echo '/home/joe/bacon_lettuce.egg.gz' | path filestem -p bacon_ -s .egg.gz", + result: Some(vec![Value::from("lettuce")]), + }, + Example { + description: "Replace the filestem that would be returned", + example: "echo '/home/joe/bacon_lettuce.egg.gz' | path filestem -p bacon_ -s .egg.gz -r spam", + result: Some(vec![Value::from(UntaggedValue::path("/home/joe/bacon_spam.egg.gz"))]), + }, + ] } } -fn action(path: &Path) -> UntaggedValue { - UntaggedValue::string(match path.file_stem() { - Some(stem) => stem.to_string_lossy().to_string(), - _ => "".to_string(), - }) +fn action(path: &Path, args: Arc) -> UntaggedValue { + let basename = match path.file_name() { + Some(name) => name.to_string_lossy().to_string(), + None => "".to_string(), + }; + + let suffix = match args.suffix { + Some(ref suf) => match basename.rmatch_indices(suf).next() { + Some((i, _)) => basename.split_at(i).1.to_string(), + None => "".to_string(), + }, + None => match path.extension() { + // Prepend '.' since the extension returned comes without it + Some(ext) => ".".to_string() + &ext.to_string_lossy().to_string(), + None => "".to_string(), + }, + }; + + let prefix = match args.prefix { + Some(ref pre) => match basename.matches(pre).next() { + Some(m) => basename.split_at(m.len()).0.to_string(), + None => "".to_string(), + }, + None => "".to_string(), + }; + + let basename_without_prefix = match basename.matches(&prefix).next() { + Some(m) => basename.split_at(m.len()).1.to_string(), + None => basename, + }; + + let stem = match basename_without_prefix.rmatch_indices(&suffix).next() { + Some((i, _)) => basename_without_prefix.split_at(i).0.to_string(), + None => basename_without_prefix, + }; + + match args.replace { + Some(ref replace) => { + let new_name = prefix + replace + &suffix; + UntaggedValue::path(path.with_file_name(&new_name)) + } + None => UntaggedValue::string(stem), + } } #[cfg(test)] diff --git a/crates/nu-cli/src/commands/path/mod.rs b/crates/nu-cli/src/commands/path/mod.rs index 44edc07531..f3423f38b2 100644 --- a/crates/nu-cli/src/commands/path/mod.rs +++ b/crates/nu-cli/src/commands/path/mod.rs @@ -12,6 +12,7 @@ use nu_errors::ShellError; use nu_protocol::{ColumnPath, Primitive, ReturnSuccess, ShellTypeName, UntaggedValue, Value}; use nu_source::Span; use std::path::Path; +use std::sync::Arc; pub use basename::PathBasename; pub use command::Path as PathCommand; @@ -24,17 +25,32 @@ pub use r#type::PathType; #[derive(Deserialize)] struct DefaultArguments { - rest: Vec, + // used by basename, dirname, extension and filestem + replace: Option, + // used by filestem + prefix: Option, + suffix: Option, + // used by dirname + num_levels: Option, + // used by all + paths: Vec, } -fn handle_value(action: &F, v: &Value, span: Span) -> Result +fn handle_value( + action: &F, + v: &Value, + span: Span, + args: Arc, +) -> Result where - F: Fn(&Path) -> UntaggedValue + Send + 'static, + F: Fn(&Path, Arc) -> UntaggedValue + Send + 'static, { let v = match &v.value { - UntaggedValue::Primitive(Primitive::Path(buf)) => action(buf).into_value(v.tag()), + UntaggedValue::Primitive(Primitive::Path(buf)) => action(buf, args).into_value(v.tag()), UntaggedValue::Primitive(Primitive::String(s)) - | UntaggedValue::Primitive(Primitive::Line(s)) => action(s.as_ref()).into_value(v.tag()), + | UntaggedValue::Primitive(Primitive::Line(s)) => { + action(s.as_ref(), args).into_value(v.tag()) + } other => { let got = format!("got {}", other.type_name()); return Err(ShellError::labeled_error_with_secondary( @@ -51,24 +67,25 @@ where async fn operate( input: crate::InputStream, - paths: Vec, action: &'static F, span: Span, + args: Arc, ) -> Result where - F: Fn(&Path) -> UntaggedValue + Send + Sync + 'static, + F: Fn(&Path, Arc) -> UntaggedValue + Send + Sync + 'static, { Ok(input .map(move |v| { - if paths.is_empty() { - ReturnSuccess::value(handle_value(&action, &v, span)?) + if args.paths.is_empty() { + ReturnSuccess::value(handle_value(&action, &v, span, Arc::clone(&args))?) } else { let mut ret = v; - for path in &paths { + for path in &args.paths { + let cloned_args = Arc::clone(&args); ret = ret.swap_data_by_column_path( path, - Box::new(move |old| handle_value(&action, &old, span)), + Box::new(move |old| handle_value(&action, &old, span, cloned_args)), )?; } diff --git a/crates/nu-cli/src/commands/path/type.rs b/crates/nu-cli/src/commands/path/type.rs index aaae3d4fad..140f5efcc4 100644 --- a/crates/nu-cli/src/commands/path/type.rs +++ b/crates/nu-cli/src/commands/path/type.rs @@ -3,11 +3,16 @@ use crate::commands::WholeStreamCommand; use crate::prelude::*; use crate::shell::filesystem_shell::get_file_type; use nu_errors::ShellError; -use nu_protocol::{Signature, SyntaxShape, UntaggedValue, Value}; +use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; use std::path::Path; pub struct PathType; +#[derive(Deserialize)] +struct PathTypeArguments { + rest: Vec, +} + #[async_trait] impl WholeStreamCommand for PathType { fn name(&self) -> &str { @@ -15,11 +20,12 @@ impl WholeStreamCommand for PathType { } fn signature(&self) -> Signature { - Signature::build("path type").rest(SyntaxShape::ColumnPath, "optionally operate by path") + Signature::build("path type") + .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") } fn usage(&self) -> &str { - "gives the type of the object the path refers to (eg file, dir, symlink)" + "Gives the type of the object a path refers to (e.g., file, dir, symlink)" } async fn run( @@ -28,8 +34,15 @@ impl WholeStreamCommand for PathType { registry: &CommandRegistry, ) -> Result { let tag = args.call_info.name_tag.clone(); - let (DefaultArguments { rest }, input) = args.process(®istry).await?; - operate(input, rest, &action, tag.span).await + let (PathTypeArguments { rest }, input) = args.process(®istry).await?; + let args = Arc::new(DefaultArguments { + replace: None, + prefix: None, + suffix: None, + num_levels: None, + paths: rest, + }); + operate(input, &action, tag.span, args).await } fn examples(&self) -> Vec { @@ -41,7 +54,7 @@ impl WholeStreamCommand for PathType { } } -fn action(path: &Path) -> UntaggedValue { +fn action(path: &Path, _args: Arc) -> UntaggedValue { let meta = std::fs::symlink_metadata(path); UntaggedValue::string(match &meta { Ok(md) => get_file_type(md), diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index 2b90267d50..6527c450db 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -34,6 +34,7 @@ mod mkdir; mod move_; mod open; mod parse; +mod path; mod prepend; mod random; mod range; diff --git a/crates/nu-cli/tests/commands/path/basename.rs b/crates/nu-cli/tests/commands/path/basename.rs new file mode 100644 index 0000000000..a16c8202c9 --- /dev/null +++ b/crates/nu-cli/tests/commands/path/basename.rs @@ -0,0 +1,83 @@ +use nu_test_support::{nu, pipeline}; + +use super::join_path_sep; + +#[test] +fn returns_basename_of_empty_input() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "" + | path basename + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn replaces_basename_of_empty_input() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "" + | path basename -r newname.txt + "# + )); + + assert_eq!(actual.out, "newname.txt"); +} + +#[test] +fn returns_basename_of_path_ending_with_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "some/file.txt/." + | path basename + "# + )); + + assert_eq!(actual.out, "file.txt"); +} + +#[test] +fn replaces_basename_of_path_ending_with_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "some/file.txt/." + | path basename -r viking.txt + "# + )); + + let expected = join_path_sep(&["some", "viking.txt"]); + assert_eq!(actual.out, expected); +} + +#[test] +fn returns_basename_of_path_ending_with_double_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "some/file.txt/.." + | path basename + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn replaces_basename_of_path_ending_with_double_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "some/file.txt/.." + | path basename -r eggs + "# + )); + + let expected = join_path_sep(&["some/file.txt/..", "eggs"]); + assert_eq!(actual.out, expected); +} diff --git a/crates/nu-cli/tests/commands/path/dirname.rs b/crates/nu-cli/tests/commands/path/dirname.rs new file mode 100644 index 0000000000..a935c1fb34 --- /dev/null +++ b/crates/nu-cli/tests/commands/path/dirname.rs @@ -0,0 +1,137 @@ +use nu_test_support::{nu, pipeline}; + +use super::join_path_sep; + +#[test] +fn returns_dirname_of_empty_input() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "" + | path dirname + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn replaces_dirname_of_empty_input() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "" + | path dirname -r newdir + "# + )); + + assert_eq!(actual.out, "newdir"); +} + +#[test] +fn returns_dirname_of_path_ending_with_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "some/dir/." + | path dirname + "# + )); + + assert_eq!(actual.out, "some"); +} + +#[test] +fn replaces_dirname_of_path_ending_with_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "some/dir/." + | path dirname -r eggs + "# + )); + + let expected = join_path_sep(&["eggs", "dir"]); + assert_eq!(actual.out, expected); +} + +#[test] +fn returns_dirname_of_path_ending_with_double_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "some/dir/.." + | path dirname + "# + )); + + assert_eq!(actual.out, "some/dir"); +} + +#[test] +fn replaces_dirname_of_path_with_double_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "some/dir/.." + | path dirname -r eggs + "# + )); + + let expected = join_path_sep(&["eggs", ".."]); + assert_eq!(actual.out, expected); +} + +#[test] +fn returns_dirname_of_zero_levels() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "some/dir/with/spam.txt" + | path dirname -n 0 + "# + )); + + assert_eq!(actual.out, "some/dir/with/spam.txt"); +} + +#[test] +fn replaces_dirname_of_zero_levels_with_empty_string() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "some/dir/with/spam.txt" + | path dirname -n 0 -r "" + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn replaces_dirname_of_more_levels() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "some/dir/with/spam.txt" + | path dirname -r eggs -n 2 + "# + )); + + let expected = join_path_sep(&["eggs", "with/spam.txt"]); + assert_eq!(actual.out, expected); +} + +#[test] +fn replaces_dirname_of_way_too_many_levels() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "some/dir/with/spam.txt" + | path dirname -r eggs -n 999 + "# + )); + + let expected = join_path_sep(&["eggs", "some/dir/with/spam.txt"]); + assert_eq!(actual.out, expected); +} diff --git a/crates/nu-cli/tests/commands/path/exists.rs b/crates/nu-cli/tests/commands/path/exists.rs new file mode 100644 index 0000000000..9a1b496e04 --- /dev/null +++ b/crates/nu-cli/tests/commands/path/exists.rs @@ -0,0 +1,53 @@ +use nu_test_support::fs::Stub::EmptyFile; +use nu_test_support::nu; +use nu_test_support::playground::Playground; + +#[test] +fn checks_if_existing_file_exists() { + Playground::setup("path_exists_1", |dirs, sandbox| { + sandbox.with_files(vec![EmptyFile("spam.txt")]); + + let actual = nu!( + cwd: dirs.test(), + "echo spam.txt | path exists" + ); + + assert_eq!(actual.out, "true"); + }) +} + +#[test] +fn checks_if_missing_file_exists() { + Playground::setup("path_exists_2", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + "echo spam.txt | path exists" + ); + + assert_eq!(actual.out, "false"); + }) +} + +#[test] +fn checks_if_dot_exists() { + Playground::setup("path_exists_3", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + "echo '.' | path exists" + ); + + assert_eq!(actual.out, "true"); + }) +} + +#[test] +fn checks_if_double_dot_exists() { + Playground::setup("path_exists_4", |dirs, _| { + let actual = nu!( + cwd: dirs.test(), + "echo '..' | path exists" + ); + + assert_eq!(actual.out, "true"); + }) +} diff --git a/crates/nu-cli/tests/commands/path/expand.rs b/crates/nu-cli/tests/commands/path/expand.rs new file mode 100644 index 0000000000..9ff7b57117 --- /dev/null +++ b/crates/nu-cli/tests/commands/path/expand.rs @@ -0,0 +1,45 @@ +use nu_test_support::fs::Stub::EmptyFile; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +use std::path::PathBuf; + +#[test] +fn expands_path_with_dot() { + Playground::setup("path_expand_1", |dirs, sandbox| { + sandbox + .within("menu") + .with_files(vec![EmptyFile("spam.txt")]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo "menu/./spam.txt" + | path expand + "# + )); + + let expected = dirs.test.join("menu").join("spam.txt"); + assert_eq!(PathBuf::from(actual.out), expected); + }) +} + +#[test] +fn expands_path_with_double_dot() { + Playground::setup("path_expand_2", |dirs, sandbox| { + sandbox + .within("menu") + .with_files(vec![EmptyFile("spam.txt")]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo "menu/../menu/spam.txt" + | path expand + "# + )); + + let expected = dirs.test.join("menu").join("spam.txt"); + assert_eq!(PathBuf::from(actual.out), expected); + }) +} diff --git a/crates/nu-cli/tests/commands/path/extension.rs b/crates/nu-cli/tests/commands/path/extension.rs new file mode 100644 index 0000000000..bbf6767b23 --- /dev/null +++ b/crates/nu-cli/tests/commands/path/extension.rs @@ -0,0 +1,37 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn returns_extension_of_path_ending_with_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "bacon." | path extension + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn replaces_extension_with_dot_of_path_ending_with_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "bacon." | path extension -r .egg + "# + )); + + assert_eq!(actual.out, "bacon..egg"); +} + +#[test] +fn replaces_extension_of_empty_path() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "" | path extension -r egg + "# + )); + + assert_eq!(actual.out, ""); +} diff --git a/crates/nu-cli/tests/commands/path/filestem.rs b/crates/nu-cli/tests/commands/path/filestem.rs new file mode 100644 index 0000000000..fe18085059 --- /dev/null +++ b/crates/nu-cli/tests/commands/path/filestem.rs @@ -0,0 +1,95 @@ +use nu_test_support::{nu, pipeline}; + +use super::join_path_sep; + +#[test] +fn returns_filestem_of_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "menu/eggs/." + | path filestem + "# + )); + + assert_eq!(actual.out, "eggs"); +} + +#[test] +fn returns_filestem_of_double_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "menu/eggs/.." + | path filestem + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn returns_filestem_of_path_with_empty_prefix() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "menu/spam.txt" + | path filestem -p "" + "# + )); + + assert_eq!(actual.out, "spam"); +} + +#[test] +fn returns_filestem_of_path_with_empty_suffix() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "menu/spam.txt" + | path filestem -s "" + "# + )); + + assert_eq!(actual.out, "spam.txt"); +} + +#[test] +fn returns_filestem_of_path_with_empty_prefix_and_suffix() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "menu/spam.txt" + | path filestem -p "" -s "" + "# + )); + + assert_eq!(actual.out, "spam.txt"); +} + +#[test] +fn returns_filestem_with_wrong_prefix_and_suffix() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "menu/spam.txt" + | path filestem -p "bacon" -s "eggs" + "# + )); + + assert_eq!(actual.out, "spam.txt"); +} + +#[test] +fn replaces_filestem_stripped_to_dot() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "menu/spam.txt" + | path filestem -p "spam" -s "txt" -r ".eggs." + "# + )); + + let expected = join_path_sep(&["menu", "spam.eggs.txt"]); + assert_eq!(actual.out, expected); +} diff --git a/crates/nu-cli/tests/commands/path/mod.rs b/crates/nu-cli/tests/commands/path/mod.rs new file mode 100644 index 0000000000..99b6a32339 --- /dev/null +++ b/crates/nu-cli/tests/commands/path/mod.rs @@ -0,0 +1,33 @@ +mod basename; +mod dirname; +mod exists; +mod expand; +mod extension; +mod filestem; +mod type_; + +use std::path::MAIN_SEPARATOR; + +/// Helper function that joins string literals with '/' or '\', based on host OS +fn join_path_sep(pieces: &[&str]) -> String { + let sep_string = String::from(MAIN_SEPARATOR); + pieces.join(&sep_string) +} + +#[cfg(windows)] +#[test] +fn joins_path_on_windows() { + let pieces = ["sausage", "bacon", "spam"]; + let actual = join_path_sep(&pieces); + + assert_eq!(&actual, "sausage\\bacon\\spam"); +} + +#[cfg(not(windows))] +#[test] +fn joins_path_on_other_than_windows() { + let pieces = ["sausage", "bacon", "spam"]; + let actual = join_path_sep(&pieces); + + assert_eq!(&actual, "sausage/bacon/spam"); +} diff --git a/crates/nu-cli/tests/commands/path/type_.rs b/crates/nu-cli/tests/commands/path/type_.rs new file mode 100644 index 0000000000..0699767f89 --- /dev/null +++ b/crates/nu-cli/tests/commands/path/type_.rs @@ -0,0 +1,54 @@ +use nu_test_support::fs::Stub::EmptyFile; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, pipeline}; + +#[test] +fn returns_type_of_missing_file() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo "spam.txt" + | path type + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn returns_type_of_existing_file() { + Playground::setup("path_expand_1", |dirs, sandbox| { + sandbox + .within("menu") + .with_files(vec![EmptyFile("spam.txt")]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo "menu" + | path type + "# + )); + + assert_eq!(actual.out, "Dir"); + }) +} + +#[test] +fn returns_type_of_existing_directory() { + Playground::setup("path_expand_1", |dirs, sandbox| { + sandbox + .within("menu") + .with_files(vec![EmptyFile("spam.txt")]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + echo "menu/spam.txt" + | path type + "# + )); + + assert_eq!(actual.out, "File"); + }) +}