From 3b2ed7631fdca3791b574f79eca0928e26bc1995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BD=C3=A1dn=C3=ADk?= Date: Tue, 20 Apr 2021 09:45:28 +0300 Subject: [PATCH] Path Enhancement Project #2: parse, join and split (#3256) * Add new path parse subcommand This includes a slight refactor to all the path subcommand `action()` functions. * Remove filestem and extension; Fix example * Add additional description to path parse * Put join arg behind flag; Fix missing import (Win) * Fix error when column path is passed as arg * Add structured path joining Structured path is implicitly joined at every patch subcommand call. * Fix existing path join tests; Fix rustfmt * Remove redundant 'static lifetime (clippy) * Add initial impl of path split subcommand * Add ability to join path from parts * Fix wrong results in path split examples * Fix remaining asyncs after engine change * Do not wrap split path parts into table When the input is just a list of values, the `path split` command will split each value directly into the output stream, similar to `split-row`. Column path--specified values are still wrapped into a table so they can still be used to replace table fields. * Join list of values instead of going one-by-one When `path join` encounters a list of values, it attempts to join them, instead of going one-by-one like the rest of the path commands. You can still `each { echo $it | path join }` to join them one-by-one, if the values are, e.g., tables. Now, the behavior of `path split` and `path join` should match the `split-row` and `str collect` counterparts and should hopefully align better with user's expectations. * Make sure path join detects structured path * Fix panic on empty input stream Also, doesn't collect input into vector unnecessarily. * Fix path join not appending value * Remove argument serialization * Make better errors; Misc refactor * OsStr -> String encoding is now lossy, instead of throwing an error * The consequence is action() now always returns Value instead of Result * Removed redundant handle_value() call in `path join` * Fix possible incorrect error detection in `path split` * Applied rustfmt + clippy * Add more usage, examples & test; Fix type error The 'parent' column was required to be a path but didn't work with string. * Add more help & examples; Maybe fix Windows error * Refactor operate function Reducing code repetition * Review usages and examples * Add the option to manually specify the extension * Add more tests; Fix failures on Windows * Move path commands to engine-p * Small refactor --- crates/nu-command/src/commands.rs | 4 +- .../src/commands/default_context.rs | 4 +- .../nu-command/src/commands/path/basename.rs | 27 ++- .../nu-command/src/commands/path/command.rs | 23 +- .../nu-command/src/commands/path/dirname.rs | 48 ++-- crates/nu-command/src/commands/path/exists.rs | 22 +- crates/nu-command/src/commands/path/expand.rs | 20 +- .../nu-command/src/commands/path/extension.rs | 97 -------- .../nu-command/src/commands/path/filestem.rs | 176 --------------- crates/nu-command/src/commands/path/join.rs | 144 +++++++++--- crates/nu-command/src/commands/path/mod.rs | 209 ++++++++++++++---- crates/nu-command/src/commands/path/parse.rs | 177 +++++++++++++++ crates/nu-command/src/commands/path/split.rs | 146 ++++++++++++ crates/nu-command/src/commands/path/type.rs | 22 +- .../tests/commands/path/extension.rs | 37 ---- .../tests/commands/path/filestem.rs | 95 -------- crates/nu-command/tests/commands/path/join.rs | 20 +- crates/nu-command/tests/commands/path/mod.rs | 4 +- .../nu-command/tests/commands/path/parse.rs | 136 ++++++++++++ .../nu-command/tests/commands/path/split.rs | 47 ++++ 20 files changed, 910 insertions(+), 548 deletions(-) delete mode 100644 crates/nu-command/src/commands/path/extension.rs delete mode 100644 crates/nu-command/src/commands/path/filestem.rs create mode 100644 crates/nu-command/src/commands/path/parse.rs create mode 100644 crates/nu-command/src/commands/path/split.rs delete mode 100644 crates/nu-command/tests/commands/path/extension.rs delete mode 100644 crates/nu-command/tests/commands/path/filestem.rs create mode 100644 crates/nu-command/tests/commands/path/parse.rs create mode 100644 crates/nu-command/tests/commands/path/split.rs diff --git a/crates/nu-command/src/commands.rs b/crates/nu-command/src/commands.rs index a1f0cd031b..d4860b60d7 100644 --- a/crates/nu-command/src/commands.rs +++ b/crates/nu-command/src/commands.rs @@ -234,8 +234,8 @@ pub(crate) use nth::Nth; pub(crate) use open::Open; pub(crate) use parse::Parse; pub(crate) use path::{ - PathBasename, PathCommand, PathDirname, PathExists, PathExpand, PathExtension, PathFilestem, - PathJoin, PathType, + PathBasename, PathCommand, PathDirname, PathExists, PathExpand, PathJoin, PathParse, PathSplit, + PathType, }; pub(crate) use pivot::Pivot; pub(crate) use prepend::Prepend; diff --git a/crates/nu-command/src/commands/default_context.rs b/crates/nu-command/src/commands/default_context.rs index bcaf5f363e..4a63a61312 100644 --- a/crates/nu-command/src/commands/default_context.rs +++ b/crates/nu-command/src/commands/default_context.rs @@ -237,9 +237,9 @@ pub fn create_default_context(interactive: bool) -> Result>, rest: Vec, + replace: Option>, } impl PathSubcommandArguments for PathBasenameArguments { @@ -27,24 +26,28 @@ impl WholeStreamCommand for PathBasename { fn signature(&self) -> Signature { Signature::build("path basename") + .rest(SyntaxShape::ColumnPath, "Optionally operate by column 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 final component of a path" + "Get the final component of a path" } - fn run_with_actions(&self, args: CommandArgs) -> Result { + fn run(&self, args: CommandArgs) -> Result { let tag = args.call_info.name_tag.clone(); - let (PathBasenameArguments { replace, rest }, input) = args.process()?; - let args = Arc::new(PathBasenameArguments { replace, rest }); - Ok(operate(input, &action, tag.span, args)) + let args = args.evaluate_once()?; + let cmd_args = Arc::new(PathBasenameArguments { + rest: args.rest_args()?, + replace: args.get_flag("replace")?, + }); + + Ok(operate(args.input, &action, tag.span, cmd_args)) } #[cfg(windows)] @@ -84,14 +87,16 @@ impl WholeStreamCommand for PathBasename { } } -fn action(path: &Path, args: &PathBasenameArguments) -> UntaggedValue { - match args.replace { +fn action(path: &Path, tag: Tag, args: &PathBasenameArguments) -> Value { + let untagged = match args.replace { Some(ref basename) => UntaggedValue::filepath(path.with_file_name(&basename.item)), None => UntaggedValue::string(match path.file_name() { Some(filename) => filename.to_string_lossy(), None => "".into(), }), - } + }; + + untagged.into_value(tag) } #[cfg(test)] diff --git a/crates/nu-command/src/commands/path/command.rs b/crates/nu-command/src/commands/path/command.rs index 53d1448298..58270691d3 100644 --- a/crates/nu-command/src/commands/path/command.rs +++ b/crates/nu-command/src/commands/path/command.rs @@ -1,7 +1,7 @@ use crate::prelude::*; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; -use nu_protocol::{ReturnSuccess, Signature, UntaggedValue}; +use nu_protocol::{Signature, UntaggedValue}; pub struct Path; @@ -18,10 +18,25 @@ impl WholeStreamCommand for Path { "Explore and manipulate paths." } - fn run_with_actions(&self, args: CommandArgs) -> Result { - Ok(ActionStream::one(ReturnSuccess::value( + fn extra_usage(&self) -> &str { + r#"There are three ways to represent a path: + +* As a path literal, e.g., '/home/viking/spam.txt' +* As a structured path: a table with 'parent', 'stem', and 'extension' (and +* 'prefix' on Windows) columns. This format is produced by the 'path parse' + subcommand. +* As an inner list of path parts, e.g., '[[ / home viking spam.txt ]]'. + Splitting into parts is done by the `path split` command. + +All subcommands accept all three variants as an input. Furthermore, the 'path +join' subcommand can be used to join the structured path or path parts back into +the path literal."# + } + + fn run(&self, args: CommandArgs) -> Result { + Ok(OutputStream::one( UntaggedValue::string(get_full_help(&Path, &args.scope)).into_value(Tag::unknown()), - ))) + )) } } diff --git a/crates/nu-command/src/commands/path/dirname.rs b/crates/nu-command/src/commands/path/dirname.rs index d5d05fe249..4b625d6c08 100644 --- a/crates/nu-command/src/commands/path/dirname.rs +++ b/crates/nu-command/src/commands/path/dirname.rs @@ -8,12 +8,10 @@ use std::path::Path; pub struct PathDirname; -#[derive(Deserialize)] struct PathDirnameArguments { - replace: Option>, - #[serde(rename = "num-levels")] - num_levels: Option>, rest: Vec, + replace: Option>, + num_levels: Option>, } impl PathSubcommandArguments for PathDirnameArguments { @@ -29,6 +27,7 @@ impl WholeStreamCommand for PathDirname { fn signature(&self) -> Signature { Signature::build("path dirname") + .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") .named( "replace", SyntaxShape::String, @@ -41,29 +40,22 @@ impl WholeStreamCommand for PathDirname { "Number of directories to walk up", Some('n'), ) - .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") } fn usage(&self) -> &str { - "Gets the parent directory of a path" + "Get the parent directory of a path" } - fn run_with_actions(&self, args: CommandArgs) -> Result { + fn run(&self, args: CommandArgs) -> Result { let tag = args.call_info.name_tag.clone(); - let ( - PathDirnameArguments { - replace, - num_levels, - rest, - }, - input, - ) = args.process()?; - let args = Arc::new(PathDirnameArguments { - replace, - num_levels, - rest, + let args = args.evaluate_once()?; + let cmd_args = Arc::new(PathDirnameArguments { + rest: args.rest_args()?, + replace: args.get_flag("replace")?, + num_levels: args.get_flag("num-levels")?, }); - Ok(operate(input, &action, tag.span, args)) + + Ok(operate(args.input, &action, tag.span, cmd_args)) } #[cfg(windows)] @@ -77,12 +69,12 @@ impl WholeStreamCommand for PathDirname { ))]), }, Example { - description: "Set how many levels up to skip", + description: "Walk up two levels", example: "echo 'C:\\Users\\joe\\code\\test.txt' | path dirname -n 2", result: Some(vec![Value::from(UntaggedValue::filepath("C:\\Users\\joe"))]), }, Example { - description: "Replace the part that would be returned with custom string", + description: "Replace the part that would be returned with a custom path", example: "echo 'C:\\Users\\joe\\code\\test.txt' | path dirname -n 2 -r C:\\Users\\viking", result: Some(vec![Value::from(UntaggedValue::filepath( @@ -101,12 +93,12 @@ impl WholeStreamCommand for PathDirname { result: Some(vec![Value::from(UntaggedValue::filepath("/home/joe/code"))]), }, Example { - description: "Set how many levels up to skip", + description: "Walk up two levels", example: "echo '/home/joe/code/test.txt' | path dirname -n 2", result: Some(vec![Value::from(UntaggedValue::filepath("/home/joe"))]), }, Example { - description: "Replace the part that would be returned with custom string", + description: "Replace the part that would be returned with a custom path", example: "echo '/home/joe/code/test.txt' | path dirname -n 2 -r /home/viking", result: Some(vec![Value::from(UntaggedValue::filepath( "/home/viking/code/test.txt", @@ -116,7 +108,7 @@ impl WholeStreamCommand for PathDirname { } } -fn action(path: &Path, args: &PathDirnameArguments) -> UntaggedValue { +fn action(path: &Path, tag: Tag, args: &PathDirnameArguments) -> Value { let num_levels = args.num_levels.as_ref().map_or(1, |tagged| tagged.item); let mut dirname = path; @@ -131,7 +123,7 @@ fn action(path: &Path, args: &PathDirnameArguments) -> UntaggedValue { } } - match args.replace { + let untagged = match args.replace { Some(ref newdir) => { let remainder = path.strip_prefix(dirname).unwrap_or(dirname); if !remainder.as_os_str().is_empty() { @@ -141,7 +133,9 @@ fn action(path: &Path, args: &PathDirnameArguments) -> UntaggedValue { } } None => UntaggedValue::filepath(dirname), - } + }; + + untagged.into_value(tag) } #[cfg(test)] diff --git a/crates/nu-command/src/commands/path/exists.rs b/crates/nu-command/src/commands/path/exists.rs index b8e49e9c79..d44198045f 100644 --- a/crates/nu-command/src/commands/path/exists.rs +++ b/crates/nu-command/src/commands/path/exists.rs @@ -7,7 +7,6 @@ use std::path::Path; pub struct PathExists; -#[derive(Deserialize)] struct PathExistsArguments { rest: Vec, } @@ -29,20 +28,23 @@ impl WholeStreamCommand for PathExists { } fn usage(&self) -> &str { - "Checks whether a path exists" + "Check whether a path exists" } - fn run_with_actions(&self, args: CommandArgs) -> Result { + fn run(&self, args: CommandArgs) -> Result { let tag = args.call_info.name_tag.clone(); - let (PathExistsArguments { rest }, input) = args.process()?; - let args = Arc::new(PathExistsArguments { rest }); - Ok(operate(input, &action, tag.span, args)) + let args = args.evaluate_once()?; + let cmd_args = Arc::new(PathExistsArguments { + rest: args.rest_args()?, + }); + + Ok(operate(args.input, &action, tag.span, cmd_args)) } #[cfg(windows)] fn examples(&self) -> Vec { vec![Example { - description: "Check if file exists", + description: "Check if a file exists", example: "echo 'C:\\Users\\joe\\todo.txt' | path exists", result: Some(vec![Value::from(UntaggedValue::boolean(false))]), }] @@ -51,15 +53,15 @@ impl WholeStreamCommand for PathExists { #[cfg(not(windows))] fn examples(&self) -> Vec { vec![Example { - description: "Check if file exists", + description: "Check if a file exists", example: "echo '/home/joe/todo.txt' | path exists", result: Some(vec![Value::from(UntaggedValue::boolean(false))]), }] } } -fn action(path: &Path, _args: &PathExistsArguments) -> UntaggedValue { - UntaggedValue::boolean(path.exists()) +fn action(path: &Path, tag: Tag, _args: &PathExistsArguments) -> Value { + UntaggedValue::boolean(path.exists()).into_value(tag) } #[cfg(test)] diff --git a/crates/nu-command/src/commands/path/expand.rs b/crates/nu-command/src/commands/path/expand.rs index 1f9d35de23..7e9d92f79a 100644 --- a/crates/nu-command/src/commands/path/expand.rs +++ b/crates/nu-command/src/commands/path/expand.rs @@ -2,12 +2,11 @@ use super::{operate, PathSubcommandArguments}; use crate::prelude::*; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue}; +use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; use std::path::{Path, PathBuf}; pub struct PathExpand; -#[derive(Deserialize)] struct PathExpandArguments { rest: Vec, } @@ -29,14 +28,17 @@ impl WholeStreamCommand for PathExpand { } fn usage(&self) -> &str { - "Expands a path to its absolute form" + "Expand a path to its absolute form" } - fn run_with_actions(&self, args: CommandArgs) -> Result { + fn run(&self, args: CommandArgs) -> Result { let tag = args.call_info.name_tag.clone(); - let (PathExpandArguments { rest }, input) = args.process()?; - let args = Arc::new(PathExpandArguments { rest }); - Ok(operate(input, &action, tag.span, args)) + let args = args.evaluate_once()?; + let cmd_args = Arc::new(PathExpandArguments { + rest: args.rest_args()?, + }); + + Ok(operate(args.input, &action, tag.span, cmd_args)) } #[cfg(windows)] @@ -60,11 +62,13 @@ impl WholeStreamCommand for PathExpand { } } -fn action(path: &Path, _args: &PathExpandArguments) -> UntaggedValue { +fn action(path: &Path, tag: Tag, _args: &PathExpandArguments) -> Value { let ps = path.to_string_lossy(); let expanded = shellexpand::tilde(&ps); let path: &Path = expanded.as_ref().as_ref(); + UntaggedValue::filepath(dunce::canonicalize(path).unwrap_or_else(|_| PathBuf::from(path))) + .into_value(tag) } #[cfg(test)] diff --git a/crates/nu-command/src/commands/path/extension.rs b/crates/nu-command/src/commands/path/extension.rs deleted file mode 100644 index 3970692443..0000000000 --- a/crates/nu-command/src/commands/path/extension.rs +++ /dev/null @@ -1,97 +0,0 @@ -use super::{operate, PathSubcommandArguments}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -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, -} - -impl PathSubcommandArguments for PathExtensionArguments { - fn get_column_paths(&self) -> &Vec { - &self.rest - } -} - -impl WholeStreamCommand for PathExtension { - fn name(&self) -> &str { - "path extension" - } - - fn signature(&self) -> Signature { - Signature::build("path extension") - .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" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let (PathExtensionArguments { replace, rest }, input) = args.process()?; - let args = Arc::new(PathExtensionArguments { replace, rest }); - Ok(operate(input, &action, tag.span, args)) - } - - 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("")]), - }, - Example { - description: "Replace an extension with a custom string", - example: "echo 'test.txt' | path extension -r md", - result: Some(vec![Value::from(UntaggedValue::filepath("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::filepath("test.txt"))]), - }, - ] - } -} - -fn action(path: &Path, args: &PathExtensionArguments) -> UntaggedValue { - match args.replace { - Some(ref extension) => UntaggedValue::filepath(path.with_extension(&extension.item)), - None => UntaggedValue::string(match path.extension() { - Some(extension) => extension.to_string_lossy(), - None => "".into(), - }), - } -} - -#[cfg(test)] -mod tests { - use super::PathExtension; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(PathExtension {}) - } -} diff --git a/crates/nu-command/src/commands/path/filestem.rs b/crates/nu-command/src/commands/path/filestem.rs deleted file mode 100644 index f62d1d8d63..0000000000 --- a/crates/nu-command/src/commands/path/filestem.rs +++ /dev/null @@ -1,176 +0,0 @@ -use super::{operate, PathSubcommandArguments}; -use crate::prelude::*; -use nu_engine::WholeStreamCommand; -use nu_errors::ShellError; -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, -} - -impl PathSubcommandArguments for PathFilestemArguments { - fn get_column_paths(&self) -> &Vec { - &self.rest - } -} - -impl WholeStreamCommand for PathFilestem { - fn name(&self) -> &str { - "path filestem" - } - - fn signature(&self) -> Signature { - Signature::build("path filestem") - .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 file stem of a path" - } - - fn run_with_actions(&self, args: CommandArgs) -> Result { - let tag = args.call_info.name_tag.clone(); - let ( - PathFilestemArguments { - prefix, - suffix, - replace, - rest, - }, - input, - ) = args.process()?; - let args = Arc::new(PathFilestemArguments { - prefix, - suffix, - replace, - rest, - }); - Ok(operate(input, &action, tag.span, args)) - } - - #[cfg(windows)] - fn examples(&self) -> Vec { - 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::filepath("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::filepath("/home/joe/bacon_spam.egg.gz"))]), - }, - ] - } -} - -fn action(path: &Path, args: &PathFilestemArguments) -> 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.item).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.item).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.item + &suffix; - UntaggedValue::filepath(path.with_file_name(&new_name)) - } - None => UntaggedValue::string(stem), - } -} - -#[cfg(test)] -mod tests { - use super::PathFilestem; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - test_examples(PathFilestem {}) - } -} diff --git a/crates/nu-command/src/commands/path/join.rs b/crates/nu-command/src/commands/path/join.rs index a2322ae759..e0c5c3839c 100644 --- a/crates/nu-command/src/commands/path/join.rs +++ b/crates/nu-command/src/commands/path/join.rs @@ -1,4 +1,4 @@ -use super::{operate, PathSubcommandArguments}; +use super::{handle_value, join_path, operate_column_paths, PathSubcommandArguments}; use crate::prelude::*; use nu_engine::WholeStreamCommand; use nu_errors::ShellError; @@ -8,10 +8,9 @@ use std::path::Path; pub struct PathJoin; -#[derive(Deserialize)] struct PathJoinArguments { - path: Tagged, rest: Vec, + append: Option>, } impl PathSubcommandArguments for PathJoinArguments { @@ -27,46 +26,141 @@ impl WholeStreamCommand for PathJoin { fn signature(&self) -> Signature { Signature::build("path join") - .required("path", SyntaxShape::String, "Path to join the input path") .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") + .named( + "append", + SyntaxShape::String, + "Path to append to the input", + Some('a'), + ) } fn usage(&self) -> &str { - "Joins an input path with another path" + "Join a structured path or a list of path parts." } - fn run_with_actions(&self, args: CommandArgs) -> Result { + fn extra_usage(&self) -> &str { + r#"Optionally, append an additional path to the result. It is designed to accept +the output of 'path parse' and 'path split' subdommands."# + } + + fn run(&self, args: CommandArgs) -> Result { let tag = args.call_info.name_tag.clone(); - let (PathJoinArguments { path, rest }, input) = args.process()?; - let args = Arc::new(PathJoinArguments { path, rest }); - Ok(operate(input, &action, tag.span, args)) + let args = args.evaluate_once()?; + let cmd_args = Arc::new(PathJoinArguments { + rest: args.rest_args()?, + append: args.get_flag("append")?, + }); + + Ok(operate_join(args.input, &action, tag, cmd_args)) } #[cfg(windows)] fn examples(&self) -> Vec { - vec![Example { - description: "Append a filename to a path", - example: "echo 'C:\\Users\\viking' | path join spam.txt", - result: Some(vec![Value::from(UntaggedValue::filepath( - "C:\\Users\\viking\\spam.txt", - ))]), - }] + vec![ + Example { + description: "Append a filename to a path", + example: r"echo 'C:\Users\viking' | path join -a spam.txt", + result: Some(vec![Value::from(UntaggedValue::filepath( + r"C:\Users\viking\spam.txt", + ))]), + }, + Example { + description: "Join a list of parts into a path", + example: r"echo [ 'C:' '\' 'Users' 'viking' 'spam.txt' ] | path join", + result: Some(vec![Value::from(UntaggedValue::filepath( + r"C:\Users\viking\spam.txt", + ))]), + }, + Example { + description: "Join a structured path into a path", + example: r"echo [ [parent stem extension]; ['C:\Users\viking' 'spam' 'txt']] | path join", + result: Some(vec![Value::from(UntaggedValue::filepath( + r"C:\Users\viking\spam.txt", + ))]), + }, + ] } #[cfg(not(windows))] fn examples(&self) -> Vec { - vec![Example { - description: "Append a filename to a path", - example: "echo '/home/viking' | path join spam.txt", - result: Some(vec![Value::from(UntaggedValue::filepath( - "/home/viking/spam.txt", - ))]), - }] + vec![ + Example { + description: "Append a filename to a path", + example: r"echo '/home/viking' | path join -a spam.txt", + result: Some(vec![Value::from(UntaggedValue::filepath( + r"/home/viking/spam.txt", + ))]), + }, + Example { + description: "Join a list of parts into a path", + example: r"echo [ '/' 'home' 'viking' 'spam.txt' ] | path join", + result: Some(vec![Value::from(UntaggedValue::filepath( + r"/home/viking/spam.txt", + ))]), + }, + Example { + description: "Join a structured path into a path", + example: r"echo [[ parent stem extension ]; [ '/home/viking' 'spam' 'txt' ]] | path join", + result: Some(vec![Value::from(UntaggedValue::filepath( + r"/home/viking/spam.txt", + ))]), + }, + ] } } -fn action(path: &Path, args: &PathJoinArguments) -> UntaggedValue { - UntaggedValue::filepath(path.join(&args.path.item)) +fn operate_join( + input: crate::InputStream, + action: &'static F, + tag: Tag, + args: Arc, +) -> OutputStream +where + T: PathSubcommandArguments + Send + Sync + 'static, + F: Fn(&Path, Tag, &T) -> Value + Send + Sync + 'static, +{ + let span = tag.span; + + if args.get_column_paths().is_empty() { + let mut parts = input.peekable(); + let has_rows = matches!( + parts.peek(), + Some(&Value { + value: UntaggedValue::Row(_), + .. + }) + ); + + if has_rows { + // operate one-by-one like the other path subcommands + parts + .into_iter() + .map( + move |v| match handle_value(&action, &v, span, Arc::clone(&args)) { + Ok(v) => v, + Err(e) => Value::error(e), + }, + ) + .to_output_stream() + } else { + // join the whole input stream + match join_path(&parts.collect_vec(), &span) { + Ok(path_buf) => OutputStream::one(action(&path_buf, tag, &args)), + Err(e) => OutputStream::one(Value::error(e)), + } + } + } else { + operate_column_paths(input, action, span, args) + } +} + +fn action(path: &Path, tag: Tag, args: &PathJoinArguments) -> Value { + if let Some(ref append) = args.append { + UntaggedValue::filepath(path.join(&append.item)).into_value(tag) + } else { + UntaggedValue::filepath(path).into_value(tag) + } } #[cfg(test)] diff --git a/crates/nu-command/src/commands/path/mod.rs b/crates/nu-command/src/commands/path/mod.rs index d42baa1394..60ac8a72d8 100644 --- a/crates/nu-command/src/commands/path/mod.rs +++ b/crates/nu-command/src/commands/path/mod.rs @@ -3,16 +3,18 @@ mod command; mod dirname; mod exists; mod expand; -mod extension; -mod filestem; mod join; +mod parse; +mod split; mod r#type; use crate::prelude::*; use nu_errors::ShellError; -use nu_protocol::{ColumnPath, Primitive, ReturnSuccess, ShellTypeName, UntaggedValue, Value}; +use nu_protocol::{ + ColumnPath, Dictionary, MaybeOwned, Primitive, ShellTypeName, UntaggedValue, Value, +}; use nu_source::Span; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; pub use basename::PathBasename; @@ -20,39 +22,173 @@ pub use command::Path as PathCommand; pub use dirname::PathDirname; pub use exists::PathExists; pub use expand::PathExpand; -pub use extension::PathExtension; -pub use filestem::PathFilestem; pub use join::PathJoin; +pub use parse::PathParse; pub use r#type::PathType; +pub use split::PathSplit; + +#[cfg(windows)] +const ALLOWED_COLUMNS: [&str; 4] = ["prefix", "parent", "stem", "extension"]; +#[cfg(not(windows))] +const ALLOWED_COLUMNS: [&str; 3] = ["parent", "stem", "extension"]; trait PathSubcommandArguments { fn get_column_paths(&self) -> &Vec; } +fn encode_path( + entries: &Dictionary, + orig_span: Span, + new_span: Span, +) -> Result { + if entries.length() == 0 { + return Err(ShellError::labeled_error_with_secondary( + "Empty table cannot be encoded as a path", + "got empty table", + new_span, + "originates from here", + orig_span, + )); + } + + for col in entries.keys() { + if !ALLOWED_COLUMNS.contains(&col.as_str()) { + let msg = format!( + "Column '{}' is not valid for a structured path. Allowed columns are: {}", + col, + ALLOWED_COLUMNS.join(", ") + ); + return Err(ShellError::labeled_error_with_secondary( + "Invalid column name", + msg, + new_span, + "originates from here", + orig_span, + )); + } + } + + // At this point, the row is known to have >0 columns, all of them allowed + let mut result = PathBuf::new(); + + #[cfg(windows)] + if let MaybeOwned::Borrowed(val) = entries.get_data("prefix") { + let s = val.as_string()?; + if !s.is_empty() { + result.push(&s); + } + }; + + if let MaybeOwned::Borrowed(val) = entries.get_data("parent") { + let p = val.as_string()?; + if !p.is_empty() { + result.push(p); + } + }; + + let mut basename = String::new(); + + if let MaybeOwned::Borrowed(val) = entries.get_data("stem") { + let s = val.as_string()?; + if !s.is_empty() { + basename.push_str(&s); + } + }; + + if let MaybeOwned::Borrowed(val) = entries.get_data("extension") { + let s = val.as_string()?; + if !s.is_empty() { + basename.push('.'); + basename.push_str(&s); + } + }; + + if !basename.is_empty() { + result.push(basename); + } + + Ok(result) +} + +fn join_path(parts: &[Value], new_span: &Span) -> Result { + parts + .iter() + .map(|part| match &part.value { + UntaggedValue::Primitive(Primitive::String(s)) => Ok(Path::new(s)), + UntaggedValue::Primitive(Primitive::FilePath(pb)) => Ok(pb.as_path()), + _ => { + let got = format!("got {}", part.type_name()); + Err(ShellError::labeled_error_with_secondary( + "Cannot join values that are not paths or strings.", + got, + new_span, + "originates from here", + part.tag.span, + )) + } + }) + .collect() +} + fn handle_value(action: &F, v: &Value, span: Span, args: Arc) -> Result where - T: PathSubcommandArguments + Send + 'static, - F: Fn(&Path, &T) -> UntaggedValue + Send + 'static, + T: PathSubcommandArguments, + F: Fn(&Path, Tag, &T) -> Value, { - let v = match &v.value { - UntaggedValue::Primitive(Primitive::FilePath(buf)) => { - action(buf, &args).into_value(v.tag()) + match &v.value { + UntaggedValue::Primitive(Primitive::FilePath(buf)) => Ok(action(buf, v.tag(), &args)), + UntaggedValue::Primitive(Primitive::String(s)) => Ok(action(s.as_ref(), v.tag(), &args)), + UntaggedValue::Row(entries) => { + // implicit path join makes all subcommands understand the structured path + let path_buf = encode_path(entries, v.tag().span, span)?; + Ok(action(&path_buf, v.tag(), &args)) } - UntaggedValue::Primitive(Primitive::String(s)) => { - action(s.as_ref(), &args).into_value(v.tag()) + UntaggedValue::Table(parts) => { + // implicit path join makes all subcommands understand path split into parts + let path_buf = join_path(parts, &span)?; + Ok(action(&path_buf, v.tag(), &args)) } other => { let got = format!("got {}", other.type_name()); - return Err(ShellError::labeled_error_with_secondary( - "value is not string or path", + Err(ShellError::labeled_error_with_secondary( + "Value is a not string, path, row, or table", got, span, - "originates from here".to_string(), + "originates from here", v.tag().span, - )); + )) } - }; - Ok(v) + } +} + +fn operate_column_paths( + input: crate::InputStream, + action: &'static F, + span: Span, + args: Arc, +) -> OutputStream +where + T: PathSubcommandArguments + Send + Sync + 'static, + F: Fn(&Path, Tag, &T) -> Value + Send + Sync + 'static, +{ + input + .map(move |v| { + let mut ret = v; + + for path in args.get_column_paths() { + let cloned_args = Arc::clone(&args); + ret = match ret.swap_data_by_column_path( + path, + Box::new(move |old| handle_value(&action, &old, span, cloned_args)), + ) { + Ok(v) => v, + Err(e) => Value::error(e), + }; + } + + ret + }) + .to_output_stream() } fn operate( @@ -60,28 +196,21 @@ fn operate( action: &'static F, span: Span, args: Arc, -) -> ActionStream +) -> OutputStream where T: PathSubcommandArguments + Send + Sync + 'static, - F: Fn(&Path, &T) -> UntaggedValue + Send + Sync + 'static, + F: Fn(&Path, Tag, &T) -> Value + Send + Sync + 'static, { - input - .map(move |v| { - if args.get_column_paths().is_empty() { - ReturnSuccess::value(handle_value(&action, &v, span, Arc::clone(&args))?) - } else { - let mut ret = v; - - for path in args.get_column_paths() { - let cloned_args = Arc::clone(&args); - ret = ret.swap_data_by_column_path( - path, - Box::new(move |old| handle_value(&action, &old, span, cloned_args)), - )?; - } - - ReturnSuccess::value(ret) - } - }) - .to_action_stream() + if args.get_column_paths().is_empty() { + input + .map( + move |v| match handle_value(&action, &v, span, Arc::clone(&args)) { + Ok(v) => v, + Err(e) => Value::error(e), + }, + ) + .to_output_stream() + } else { + operate_column_paths(input, action, span, args) + } } diff --git a/crates/nu-command/src/commands/path/parse.rs b/crates/nu-command/src/commands/path/parse.rs new file mode 100644 index 0000000000..87b212090e --- /dev/null +++ b/crates/nu-command/src/commands/path/parse.rs @@ -0,0 +1,177 @@ +use super::{operate, PathSubcommandArguments}; +use crate::prelude::*; +use nu_engine::WholeStreamCommand; +use nu_errors::ShellError; +use nu_protocol::{ColumnPath, Signature, SyntaxShape, TaggedDictBuilder, UntaggedValue, Value}; +use nu_source::Tagged; +#[cfg(windows)] +use std::path::Component; +use std::path::Path; + +pub struct PathParse; + +struct PathParseArguments { + rest: Vec, + extension: Option>, +} + +impl PathSubcommandArguments for PathParseArguments { + fn get_column_paths(&self) -> &Vec { + &self.rest + } +} + +impl WholeStreamCommand for PathParse { + fn name(&self) -> &str { + "path parse" + } + + fn signature(&self) -> Signature { + Signature::build("path parse") + .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") + .named( + "extension", + SyntaxShape::String, + "Manually supply the extension (without the dot)", + Some('e'), + ) + } + + fn usage(&self) -> &str { + "Convert a path into structured data." + } + + fn extra_usage(&self) -> &str { + r#"Each path is split into a table with 'parent', 'stem' and 'extension' fields. +On Windows, an extra 'prefix' column is added."# + } + + fn run(&self, args: CommandArgs) -> Result { + let tag = args.call_info.name_tag.clone(); + let args = args.evaluate_once()?; + let cmd_args = Arc::new(PathParseArguments { + rest: args.rest_args()?, + extension: args.get_flag("extension")?, + }); + + Ok(operate(args.input, &action, tag.span, cmd_args)) + } + + #[cfg(windows)] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Parse a single path", + example: r"echo 'C:\Users\viking\spam.txt' | path parse", + result: None, + }, + Example { + description: "Replace a complex extension", + example: r"echo 'C:\Users\viking\spam.tar.gz' | path parse -e tar.gz | update extension { = txt }", + result: None, + }, + Example { + description: "Ignore the extension", + example: r"echo 'C:\Users\viking.d' | path parse -e ''", + result: None, + }, + Example { + description: "Parse all paths under the 'name' column", + example: r"ls | path parse name", + result: None, + }, + ] + } + + #[cfg(not(windows))] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Parse a path", + example: r"echo '/home/viking/spam.txt' | path parse", + result: None, + }, + Example { + description: "Replace a complex extension", + example: r"echo '/home/viking/spam.tar.gz' | path parse -e tar.gz | update extension { = txt }", + result: None, + }, + Example { + description: "Ignore the extension", + example: r"echo '/etc/conf.d' | path parse -e ''", + result: None, + }, + Example { + description: "Parse all paths under the 'name' column", + example: r"ls | path parse name", + result: None, + }, + ] + } +} + +fn action(path: &Path, tag: Tag, args: &PathParseArguments) -> Value { + let mut dict = TaggedDictBuilder::new(&tag); + + #[cfg(windows)] + { + // The prefix is only valid on Windows. On non-Windows, it's always empty. + let prefix = match path.components().next() { + Some(Component::Prefix(prefix_component)) => { + prefix_component.as_os_str().to_string_lossy() + } + _ => "".into(), + }; + dict.insert_untagged("prefix", UntaggedValue::string(prefix)); + } + + let parent = path.parent().unwrap_or_else(|| "".as_ref()); + dict.insert_untagged("parent", UntaggedValue::filepath(parent)); + + let basename = path + .file_name() + .unwrap_or_else(|| "".as_ref()) + .to_string_lossy(); + + match &args.extension { + Some(Tagged { item: ext, .. }) => { + let ext_with_dot = [".", ext].concat(); + if basename.ends_with(&ext_with_dot) && !ext.is_empty() { + let stem = basename.trim_end_matches(&ext_with_dot); + dict.insert_untagged("stem", UntaggedValue::string(stem)); + dict.insert_untagged("extension", UntaggedValue::string(ext)); + } else { + dict.insert_untagged("stem", UntaggedValue::string(basename)); + dict.insert_untagged("extension", UntaggedValue::string("")); + } + } + None => { + let stem = path + .file_stem() + .unwrap_or_else(|| "".as_ref()) + .to_string_lossy(); + let extension = path + .extension() + .unwrap_or_else(|| "".as_ref()) + .to_string_lossy(); + + dict.insert_untagged("stem", UntaggedValue::string(stem)); + dict.insert_untagged("extension", UntaggedValue::string(extension)); + } + } + + dict.into_value() +} + +#[cfg(test)] +mod tests { + use super::PathParse; + use super::ShellError; + + #[test] + fn examples_work_as_expected() -> Result<(), ShellError> { + use crate::examples::test as test_examples; + + test_examples(PathParse {}) + } +} diff --git a/crates/nu-command/src/commands/path/split.rs b/crates/nu-command/src/commands/path/split.rs new file mode 100644 index 0000000000..b6e6f42a2c --- /dev/null +++ b/crates/nu-command/src/commands/path/split.rs @@ -0,0 +1,146 @@ +use super::{handle_value, operate_column_paths, PathSubcommandArguments}; +use crate::prelude::*; +use nu_engine::WholeStreamCommand; +use nu_errors::ShellError; +use nu_protocol::{ColumnPath, Signature, SyntaxShape, UntaggedValue, Value}; +use std::path::Path; + +pub struct PathSplit; + +struct PathSplitArguments { + rest: Vec, +} + +impl PathSubcommandArguments for PathSplitArguments { + fn get_column_paths(&self) -> &Vec { + &self.rest + } +} + +impl WholeStreamCommand for PathSplit { + fn name(&self) -> &str { + "path split" + } + + fn signature(&self) -> Signature { + Signature::build("path split") + .rest(SyntaxShape::ColumnPath, "Optionally operate by column path") + } + + fn usage(&self) -> &str { + "Split a path into parts by a separator." + } + + fn run(&self, args: CommandArgs) -> Result { + let tag = args.call_info.name_tag.clone(); + let args = args.evaluate_once()?; + let cmd_args = Arc::new(PathSplitArguments { + rest: args.rest_args()?, + }); + + Ok(operate_split(args.input, &action, tag.span, cmd_args)) + } + + #[cfg(windows)] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Split a path into parts", + example: r"echo 'C:\Users\viking\spam.txt' | path split", + result: Some(vec![ + Value::from(UntaggedValue::string("C:")), + Value::from(UntaggedValue::string(r"\")), + Value::from(UntaggedValue::string("Users")), + Value::from(UntaggedValue::string("viking")), + Value::from(UntaggedValue::string("spam.txt")), + ]), + }, + Example { + description: "Split all paths under the 'name' column", + example: r"ls | path split name", + result: None, + }, + ] + } + + #[cfg(not(windows))] + fn examples(&self) -> Vec { + vec![ + Example { + description: "Split a path into parts", + example: r"echo '/home/viking/spam.txt' | path split", + result: Some(vec![ + Value::from(UntaggedValue::string("/")), + Value::from(UntaggedValue::string("home")), + Value::from(UntaggedValue::string("viking")), + Value::from(UntaggedValue::string("spam.txt")), + ]), + }, + Example { + description: "Split all paths under the 'name' column", + example: r"ls | path split name", + result: None, + }, + ] + } +} + +fn operate_split( + input: crate::InputStream, + action: &'static F, + span: Span, + args: Arc, +) -> OutputStream +where + T: PathSubcommandArguments + Send + Sync + 'static, + F: Fn(&Path, Tag, &T) -> Value + Send + Sync + 'static, +{ + if args.get_column_paths().is_empty() { + // Do not wrap result into a table + input + .flat_map(move |v| { + let split_result = handle_value(&action, &v, span, Arc::clone(&args)); + + match split_result { + Ok(Value { + value: UntaggedValue::Table(parts), + .. + }) => parts.into_iter().to_output_stream(), + Err(e) => OutputStream::one(Value::error(e)), + _ => OutputStream::one(Value::error(ShellError::labeled_error( + "Internal Error", + "unexpected result from the split function", + span, + ))), + } + }) + .to_output_stream() + } else { + operate_column_paths(input, action, span, args) + } +} + +fn action(path: &Path, tag: Tag, _args: &PathSplitArguments) -> Value { + let parts: Vec = path + .components() + .map(|comp| { + let s = comp.as_os_str().to_string_lossy(); + UntaggedValue::string(s).into_value(&tag) + }) + .collect(); + + UntaggedValue::table(&parts).into_value(tag) +} + +#[cfg(test)] +mod tests { + use super::PathSplit; + use super::ShellError; + + #[test] + fn examples_work_as_expected() -> Result<(), ShellError> { + use crate::examples::test as test_examples; + + test_examples(PathSplit {}) + } +} diff --git a/crates/nu-command/src/commands/path/type.rs b/crates/nu-command/src/commands/path/type.rs index 3f481ecbc3..ed23a80673 100644 --- a/crates/nu-command/src/commands/path/type.rs +++ b/crates/nu-command/src/commands/path/type.rs @@ -8,7 +8,6 @@ use std::path::Path; pub struct PathType; -#[derive(Deserialize)] struct PathTypeArguments { rest: Vec, } @@ -30,14 +29,17 @@ impl WholeStreamCommand for PathType { } fn usage(&self) -> &str { - "Gives the type of the object a path refers to (e.g., file, dir, symlink)" + "Get the type of the object a path refers to (e.g., file, dir, symlink)" } - fn run_with_actions(&self, args: CommandArgs) -> Result { + fn run(&self, args: CommandArgs) -> Result { let tag = args.call_info.name_tag.clone(); - let (PathTypeArguments { rest }, input) = args.process()?; - let args = Arc::new(PathTypeArguments { rest }); - Ok(operate(input, &action, tag.span, args)) + let args = args.evaluate_once()?; + let cmd_args = Arc::new(PathTypeArguments { + rest: args.rest_args()?, + }); + + Ok(operate(args.input, &action, tag.span, cmd_args)) } fn examples(&self) -> Vec { @@ -49,12 +51,14 @@ impl WholeStreamCommand for PathType { } } -fn action(path: &Path, _args: &PathTypeArguments) -> UntaggedValue { +fn action(path: &Path, tag: Tag, _args: &PathTypeArguments) -> Value { let meta = std::fs::symlink_metadata(path); - UntaggedValue::string(match &meta { + let untagged = UntaggedValue::string(match &meta { Ok(md) => get_file_type(md), Err(_) => "", - }) + }); + + untagged.into_value(tag) } #[cfg(test)] diff --git a/crates/nu-command/tests/commands/path/extension.rs b/crates/nu-command/tests/commands/path/extension.rs deleted file mode 100644 index bbf6767b23..0000000000 --- a/crates/nu-command/tests/commands/path/extension.rs +++ /dev/null @@ -1,37 +0,0 @@ -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-command/tests/commands/path/filestem.rs b/crates/nu-command/tests/commands/path/filestem.rs deleted file mode 100644 index fe18085059..0000000000 --- a/crates/nu-command/tests/commands/path/filestem.rs +++ /dev/null @@ -1,95 +0,0 @@ -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-command/tests/commands/path/join.rs b/crates/nu-command/tests/commands/path/join.rs index 9a0eb9014b..cc818706ef 100644 --- a/crates/nu-command/tests/commands/path/join.rs +++ b/crates/nu-command/tests/commands/path/join.rs @@ -8,7 +8,7 @@ fn returns_path_joined_with_column_path() { cwd: "tests", pipeline( r#" echo [ [name]; [eggs] ] - | path join spam.txt name + | path join -a spam.txt name | get name "# )); @@ -17,13 +17,27 @@ fn returns_path_joined_with_column_path() { assert_eq!(actual.out, expected); } +#[test] +fn returns_path_joined_from_list() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo [ home viking spam.txt ] + | path join + "# + )); + + let expected = join_path_sep(&["home", "viking", "spam.txt"]); + assert_eq!(actual.out, expected); +} + #[test] fn appends_slash_when_joined_with_empty_path() { let actual = nu!( cwd: "tests", pipeline( r#" echo "/some/dir" - | path join '' + | path join -a '' "# )); @@ -37,7 +51,7 @@ fn returns_joined_path_when_joining_empty_path() { cwd: "tests", pipeline( r#" echo "" - | path join foo.txt + | path join -a foo.txt "# )); diff --git a/crates/nu-command/tests/commands/path/mod.rs b/crates/nu-command/tests/commands/path/mod.rs index e9c3211cb0..c836c5691d 100644 --- a/crates/nu-command/tests/commands/path/mod.rs +++ b/crates/nu-command/tests/commands/path/mod.rs @@ -2,9 +2,9 @@ mod basename; mod dirname; mod exists; mod expand; -mod extension; -mod filestem; mod join; +mod parse; +mod split; mod type_; use std::path::MAIN_SEPARATOR; diff --git a/crates/nu-command/tests/commands/path/parse.rs b/crates/nu-command/tests/commands/path/parse.rs new file mode 100644 index 0000000000..42a365d48a --- /dev/null +++ b/crates/nu-command/tests/commands/path/parse.rs @@ -0,0 +1,136 @@ +use nu_test_support::{nu, pipeline}; + +#[cfg(windows)] +#[test] +fn parses_single_path_prefix() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'C:\users\viking\spam.txt' + | path parse + | get prefix + "# + )); + + assert_eq!(actual.out, "C:"); +} + +#[test] +fn parses_single_path_parent() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.txt' + | path parse + | get parent + "# + )); + + assert_eq!(actual.out, "home/viking"); +} + +#[test] +fn parses_single_path_stem() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.txt' + | path parse + | get stem + "# + )); + + assert_eq!(actual.out, "spam"); +} + +#[test] +fn parses_custom_extension_gets_extension() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.tar.gz' + | path parse -e tar.gz + | get extension + "# + )); + + assert_eq!(actual.out, "tar.gz"); +} + +#[test] +fn parses_custom_extension_gets_stem() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.tar.gz' + | path parse -e tar.gz + | get stem + "# + )); + + assert_eq!(actual.out, "spam"); +} + +#[test] +fn parses_ignoring_extension_gets_extension() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.tar.gz' + | path parse -e '' + | get extension + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn parses_ignoring_extension_gets_stem() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.tar.gz' + | path parse -e "" + | get stem + "# + )); + + assert_eq!(actual.out, "spam.tar.gz"); +} + +#[test] +fn parses_column_path_extension() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo [[home, barn]; ['home/viking/spam.txt', 'barn/cow/moo.png']] + | path parse home barn + | get barn + | get extension + "# + )); + + assert_eq!(actual.out, "png"); +} + +#[test] +fn parses_into_correct_number_of_columns() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo 'home/viking/spam.txt' + | path parse + | pivot + | get Column0 + | length + "# + )); + + #[cfg(windows)] + let expected = "4"; + #[cfg(not(windows))] + let expected = "3"; + + assert_eq!(actual.out, expected); +} diff --git a/crates/nu-command/tests/commands/path/split.rs b/crates/nu-command/tests/commands/path/split.rs new file mode 100644 index 0000000000..ba70d442f6 --- /dev/null +++ b/crates/nu-command/tests/commands/path/split.rs @@ -0,0 +1,47 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn splits_empty_path() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo '' | path split + "# + )); + + assert_eq!(actual.out, ""); +} + +#[test] +fn splits_correctly_single_path() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo ['home/viking/spam.txt'] + | path split + | last + "# + )); + + assert_eq!(actual.out, "spam.txt"); +} + +#[test] +fn splits_correctly_with_column_path() { + let actual = nu!( + cwd: "tests", pipeline( + r#" + echo [ + [home, barn]; + + ['home/viking/spam.txt', 'barn/cow/moo.png'] + ['home/viking/eggs.txt', 'barn/goat/cheese.png'] + ] + | path split home barn + | get barn + | length + "# + )); + + assert_eq!(actual.out, "6"); +}