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
This commit is contained in:
Jakub Žádník 2021-04-20 09:45:28 +03:00 committed by GitHub
parent 1a46e70dfb
commit 3b2ed7631f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 910 additions and 548 deletions

View File

@ -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;

View File

@ -237,9 +237,9 @@ pub fn create_default_context(interactive: bool) -> Result<EvaluationContext, Bo
whole_stream_command(PathDirname),
whole_stream_command(PathExists),
whole_stream_command(PathExpand),
whole_stream_command(PathExtension),
whole_stream_command(PathFilestem),
whole_stream_command(PathJoin),
whole_stream_command(PathParse),
whole_stream_command(PathSplit),
whole_stream_command(PathType),
// Url
whole_stream_command(UrlCommand),

View File

@ -8,10 +8,9 @@ use std::path::Path;
pub struct PathBasename;
#[derive(Deserialize)]
struct PathBasenameArguments {
replace: Option<Tagged<String>>,
rest: Vec<ColumnPath>,
replace: Option<Tagged<String>>,
}
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<ActionStream, ShellError> {
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
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)]

View File

@ -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<ActionStream, ShellError> {
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<OutputStream, ShellError> {
Ok(OutputStream::one(
UntaggedValue::string(get_full_help(&Path, &args.scope)).into_value(Tag::unknown()),
)))
))
}
}

View File

@ -8,12 +8,10 @@ use std::path::Path;
pub struct PathDirname;
#[derive(Deserialize)]
struct PathDirnameArguments {
replace: Option<Tagged<String>>,
#[serde(rename = "num-levels")]
num_levels: Option<Tagged<u32>>,
rest: Vec<ColumnPath>,
replace: Option<Tagged<String>>,
num_levels: Option<Tagged<u32>>,
}
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<ActionStream, ShellError> {
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
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)]

View File

@ -7,7 +7,6 @@ use std::path::Path;
pub struct PathExists;
#[derive(Deserialize)]
struct PathExistsArguments {
rest: Vec<ColumnPath>,
}
@ -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<ActionStream, ShellError> {
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
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<Example> {
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<Example> {
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)]

View File

@ -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<ColumnPath>,
}
@ -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<ActionStream, ShellError> {
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
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)]

View File

@ -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<Tagged<String>>,
rest: Vec<ColumnPath>,
}
impl PathSubcommandArguments for PathExtensionArguments {
fn get_column_paths(&self) -> &Vec<ColumnPath> {
&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<ActionStream, ShellError> {
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<Example> {
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 {})
}
}

View File

@ -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<Tagged<String>>,
suffix: Option<Tagged<String>>,
replace: Option<Tagged<String>>,
rest: Vec<ColumnPath>,
}
impl PathSubcommandArguments for PathFilestemArguments {
fn get_column_paths(&self) -> &Vec<ColumnPath> {
&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<ActionStream, ShellError> {
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<Example> {
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<Example> {
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 {})
}
}

View File

@ -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<String>,
rest: Vec<ColumnPath>,
append: Option<Tagged<String>>,
}
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<ActionStream, ShellError> {
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<OutputStream, ShellError> {
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<Example> {
vec![Example {
vec![
Example {
description: "Append a filename to a path",
example: "echo 'C:\\Users\\viking' | path join spam.txt",
example: r"echo 'C:\Users\viking' | path join -a spam.txt",
result: Some(vec![Value::from(UntaggedValue::filepath(
"C:\\Users\\viking\\spam.txt",
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<Example> {
vec![Example {
vec![
Example {
description: "Append a filename to a path",
example: "echo '/home/viking' | path join spam.txt",
example: r"echo '/home/viking' | path join -a spam.txt",
result: Some(vec![Value::from(UntaggedValue::filepath(
"/home/viking/spam.txt",
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<F, T>(
input: crate::InputStream,
action: &'static F,
tag: Tag,
args: Arc<T>,
) -> 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)]

View File

@ -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<ColumnPath>;
}
fn encode_path(
entries: &Dictionary,
orig_span: Span,
new_span: Span,
) -> Result<PathBuf, ShellError> {
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<PathBuf, ShellError> {
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<F, T>(action: &F, v: &Value, span: Span, args: Arc<T>) -> Result<Value, ShellError>
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,
));
))
}
}
}
fn operate_column_paths<F, T>(
input: crate::InputStream,
action: &'static F,
span: Span,
args: Arc<T>,
) -> 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),
};
Ok(v)
}
ret
})
.to_output_stream()
}
fn operate<F, T>(
@ -60,28 +196,21 @@ fn operate<F, T>(
action: &'static F,
span: Span,
args: Arc<T>,
) -> 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))?)
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 {
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)),
)?;
operate_column_paths(input, action, span, args)
}
ReturnSuccess::value(ret)
}
})
.to_action_stream()
}

View File

@ -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<ColumnPath>,
extension: Option<Tagged<String>>,
}
impl PathSubcommandArguments for PathParseArguments {
fn get_column_paths(&self) -> &Vec<ColumnPath> {
&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<OutputStream, ShellError> {
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<Example> {
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<Example> {
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 {})
}
}

View File

@ -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<ColumnPath>,
}
impl PathSubcommandArguments for PathSplitArguments {
fn get_column_paths(&self) -> &Vec<ColumnPath> {
&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<OutputStream, ShellError> {
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<Example> {
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<Example> {
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<F, T>(
input: crate::InputStream,
action: &'static F,
span: Span,
args: Arc<T>,
) -> 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<Value> = 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 {})
}
}

View File

@ -8,7 +8,6 @@ use std::path::Path;
pub struct PathType;
#[derive(Deserialize)]
struct PathTypeArguments {
rest: Vec<ColumnPath>,
}
@ -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<ActionStream, ShellError> {
fn run(&self, args: CommandArgs) -> Result<OutputStream, ShellError> {
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<Example> {
@ -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)]

View File

@ -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, "");
}

View File

@ -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);
}

View File

@ -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
"#
));

View File

@ -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;

View File

@ -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);
}

View File

@ -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");
}