forked from extern/nushell
* 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:
parent
1a46e70dfb
commit
3b2ed7631f
@ -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;
|
||||
|
@ -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),
|
||||
|
@ -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)]
|
||||
|
@ -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()),
|
||||
)))
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)]
|
||||
|
@ -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)]
|
||||
|
@ -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)]
|
||||
|
@ -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 {})
|
||||
}
|
||||
}
|
@ -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 {})
|
||||
}
|
||||
}
|
@ -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 {
|
||||
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<Example> {
|
||||
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<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)]
|
||||
|
@ -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,
|
||||
));
|
||||
))
|
||||
}
|
||||
};
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
};
|
||||
}
|
||||
|
||||
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))?)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
177
crates/nu-command/src/commands/path/parse.rs
Normal file
177
crates/nu-command/src/commands/path/parse.rs
Normal 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 {})
|
||||
}
|
||||
}
|
146
crates/nu-command/src/commands/path/split.rs
Normal file
146
crates/nu-command/src/commands/path/split.rs
Normal 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 {})
|
||||
}
|
||||
}
|
@ -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)]
|
||||
|
@ -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, "");
|
||||
}
|
@ -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);
|
||||
}
|
@ -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
|
||||
"#
|
||||
));
|
||||
|
||||
|
@ -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;
|
||||
|
136
crates/nu-command/tests/commands/path/parse.rs
Normal file
136
crates/nu-command/tests/commands/path/parse.rs
Normal 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);
|
||||
}
|
47
crates/nu-command/tests/commands/path/split.rs
Normal file
47
crates/nu-command/tests/commands/path/split.rs
Normal 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");
|
||||
}
|
Loading…
Reference in New Issue
Block a user