nushell/crates/nu-command/src/commands/path/mod.rs
Jakub Žádník 3b2ed7631f
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
2021-04-20 18:45:28 +12:00

217 lines
6.1 KiB
Rust

mod basename;
mod command;
mod dirname;
mod exists;
mod expand;
mod join;
mod parse;
mod split;
mod r#type;
use crate::prelude::*;
use nu_errors::ShellError;
use nu_protocol::{
ColumnPath, Dictionary, MaybeOwned, Primitive, ShellTypeName, UntaggedValue, Value,
};
use nu_source::Span;
use std::path::{Path, PathBuf};
use std::sync::Arc;
pub use basename::PathBasename;
pub use command::Path as PathCommand;
pub use dirname::PathDirname;
pub use exists::PathExists;
pub use expand::PathExpand;
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,
F: Fn(&Path, Tag, &T) -> Value,
{
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::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());
Err(ShellError::labeled_error_with_secondary(
"Value is a not string, path, row, or table",
got,
span,
"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),
};
}
ret
})
.to_output_stream()
}
fn operate<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() {
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)
}
}