Add Path commands (#280)

* Add Path command

* Add `path basename`

* Refactor operate into `mod`

* Add `path dirname`

* Add `path exists`

* Add `path expand`

* Remove Arc wrapper for args

* Add `path type`

* Add `path relative`

* Add `path parse`

* Add `path split`

* Add `path join`

* Fix errors after rebase

* Convert to Path in `operate`

* Fix table behavior in `path join`

* Use conditional import in `path parse`

* Fix missing cases for `path join`

* Update default_context.rs

* clippy

* Fix tests

* Fix tests

Co-authored-by: JT <547158+jntrnr@users.noreply.github.com>
Co-authored-by: JT <jonathan.d.turner@gmail.com>
This commit is contained in:
Hilmar Gústafsson 2021-12-13 02:47:14 +01:00 committed by GitHub
parent bee7ef21eb
commit 90ddb23492
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1594 additions and 2 deletions

View file

@ -74,6 +74,20 @@ pub fn create_default_context() -> EngineState {
Zip,
};
// Path
bind_command! {
Path,
PathBasename,
PathDirname,
PathExists,
PathExpand,
PathJoin,
PathParse,
PathRelativeTo,
PathSplit,
PathType,
};
// System
bind_command! {
Benchmark,

View file

@ -7,7 +7,7 @@ use nu_protocol::{
use crate::To;
use super::{Date, From, Into, Math, Random, Split, Str, Url};
use super::{Date, From, Into, Math, Path, Random, Split, Str, Url};
pub fn test_examples(cmd: impl Command + 'static) {
let examples = cmd.examples();
@ -24,6 +24,7 @@ pub fn test_examples(cmd: impl Command + 'static) {
working_set.add_decl(Box::new(Random));
working_set.add_decl(Box::new(Split));
working_set.add_decl(Box::new(Math));
working_set.add_decl(Box::new(Path));
working_set.add_decl(Box::new(Date));
working_set.add_decl(Box::new(Url));

View file

@ -430,7 +430,7 @@ mod tests {
);
assert_eq!(
table(value.clone().into_pipeline_data(), true, &Config::default()),
table(value.into_pipeline_data(), true, &Config::default()),
one(r#"
| country |
| ----------- |

View file

@ -12,6 +12,7 @@ mod formats;
mod hash;
mod math;
mod network;
mod path;
mod platform;
mod random;
mod shells;
@ -33,6 +34,7 @@ pub use formats::*;
pub use hash::*;
pub use math::*;
pub use network::*;
pub use path::*;
pub use platform::*;
pub use random::*;
pub use shells::*;

View file

@ -0,0 +1,151 @@
use std::path::Path;
use nu_engine::CallExt;
use nu_protocol::{engine::Command, Example, Signature, Span, Spanned, SyntaxShape, Value};
use super::PathSubcommandArguments;
struct Arguments {
columns: Option<Vec<String>>,
replace: Option<Spanned<String>>,
}
impl PathSubcommandArguments for Arguments {
fn get_columns(&self) -> Option<Vec<String>> {
self.columns.clone()
}
}
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"path basename"
}
fn signature(&self) -> Signature {
Signature::build("path basename")
.named(
"columns",
SyntaxShape::Table,
"Optionally operate by column path",
Some('c'),
)
.named(
"replace",
SyntaxShape::String,
"Return original path with basename replaced by this string",
Some('r'),
)
}
fn usage(&self) -> &str {
"Get the final component of a path"
}
fn run(
&self,
engine_state: &nu_protocol::engine::EngineState,
stack: &mut nu_protocol::engine::Stack,
call: &nu_protocol::ast::Call,
input: nu_protocol::PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let head = call.head;
let args = Arguments {
columns: call.get_flag(engine_state, stack, "columns")?,
replace: call.get_flag(engine_state, stack, "replace")?,
};
input.map(
move |value| super::operate(&get_basename, &args, value, head),
engine_state.ctrlc.clone(),
)
}
#[cfg(windows)]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Get basename of a path",
example: "'C:\\Users\\joe\\test.txt' | path basename",
result: Some(Value::test_string("test.txt")),
},
Example {
description: "Get basename of a path in a column",
example: "ls .. | path basename -c [ name ]",
result: None,
},
Example {
description: "Get basename of a path in a column",
example: "[[name];[C:\\Users\\Joe]] | path basename -c [ name ]",
result: Some(Value::List {
vals: vec![Value::Record {
cols: vec!["name".to_string()],
vals: vec![Value::test_string("Joe")],
span: Span::unknown(),
}],
span: Span::unknown(),
}),
},
Example {
description: "Replace basename of a path",
example: "'C:\\Users\\joe\\test.txt' | path basename -r 'spam.png'",
result: Some(Value::test_string("C:\\Users\\joe\\spam.png")),
},
]
}
#[cfg(not(windows))]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Get basename of a path",
example: "'/home/joe/test.txt' | path basename",
result: Some(Value::test_string("test.txt")),
},
Example {
description: "Get basename of a path by column",
example: "[[name];[/home/joe]] | path basename -c [ name ]",
result: Some(Value::List {
vals: vec![Value::Record {
cols: vec!["name".to_string()],
vals: vec![Value::test_string("joe")],
span: Span::unknown(),
}],
span: Span::unknown(),
}),
},
Example {
description: "Replace basename of a path",
example: "'/home/joe/test.txt' | path basename -r 'spam.png'",
result: Some(Value::test_string("/home/joe/spam.png")),
},
]
}
}
fn get_basename(path: &Path, span: Span, args: &Arguments) -> Value {
match &args.replace {
Some(r) => Value::string(path.with_file_name(r.item.clone()).to_string_lossy(), span),
None => Value::string(
match path.file_name() {
Some(n) => n.to_string_lossy(),
None => "".into(),
},
span,
),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}

View file

@ -0,0 +1,56 @@
use nu_engine::get_full_help;
use nu_protocol::{
ast::Call,
engine::{Command, EngineState, Stack},
IntoPipelineData, PipelineData, Signature, Value,
};
#[derive(Clone)]
pub struct PathCommand;
impl Command for PathCommand {
fn name(&self) -> &str {
"path"
}
fn signature(&self) -> Signature {
Signature::build("path")
}
fn usage(&self) -> &str {
"Explore and manipulate paths."
}
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,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, nu_protocol::ShellError> {
Ok(Value::String {
val: get_full_help(
&PathCommand.signature(),
&PathCommand.examples(),
engine_state,
),
span: call.head,
}
.into_pipeline_data())
}
}

View file

@ -0,0 +1,168 @@
use std::path::Path;
use nu_engine::CallExt;
use nu_protocol::{engine::Command, Example, Signature, Span, Spanned, SyntaxShape, Value};
use super::PathSubcommandArguments;
struct Arguments {
columns: Option<Vec<String>>,
replace: Option<Spanned<String>>,
num_levels: Option<i64>,
}
impl PathSubcommandArguments for Arguments {
fn get_columns(&self) -> Option<Vec<String>> {
self.columns.clone()
}
}
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"path dirname"
}
fn signature(&self) -> Signature {
Signature::build("path dirname")
.named(
"columns",
SyntaxShape::Table,
"Optionally operate by column path",
Some('c'),
)
.named(
"replace",
SyntaxShape::String,
"Return original path with dirname replaced by this string",
Some('r'),
)
.named(
"num-levels",
SyntaxShape::Int,
"Number of directories to walk up",
Some('n'),
)
}
fn usage(&self) -> &str {
"Get the parent directory of a path"
}
fn run(
&self,
engine_state: &nu_protocol::engine::EngineState,
stack: &mut nu_protocol::engine::Stack,
call: &nu_protocol::ast::Call,
input: nu_protocol::PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let head = call.head;
let args = Arguments {
columns: call.get_flag(engine_state, stack, "columns")?,
replace: call.get_flag(engine_state, stack, "replace")?,
num_levels: call.get_flag(engine_state, stack, "num-levels")?,
};
input.map(
move |value| super::operate(&get_dirname, &args, value, head),
engine_state.ctrlc.clone(),
)
}
#[cfg(windows)]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Get dirname of a path",
example: "'C:\\Users\\joe\\code\\test.txt' | path dirname",
result: Some(Value::test_string("C:\\Users\\joe\\code")),
},
Example {
description: "Get dirname of a path in a column",
example: "ls ('.' | path expand) | path dirname -c [ name ]",
result: None,
},
Example {
description: "Walk up two levels",
example: "'C:\\Users\\joe\\code\\test.txt' | path dirname -n 2",
result: Some(Value::test_string("C:\\Users\\joe")),
},
Example {
description: "Replace the part that would be returned with a custom path",
example:
"'C:\\Users\\joe\\code\\test.txt' | path dirname -n 2 -r C:\\Users\\viking",
result: Some(Value::test_string("C:\\Users\\viking\\code\\test.txt")),
},
]
}
#[cfg(not(windows))]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Get dirname of a path",
example: "'/home/joe/code/test.txt' | path dirname",
result: Some(Value::test_string("/home/joe/code")),
},
Example {
description: "Get dirname of a path in a column",
example: "ls ('.' | path expand) | path dirname -c [ name ]",
result: None,
},
Example {
description: "Walk up two levels",
example: "'/home/joe/code/test.txt' | path dirname -n 2",
result: Some(Value::test_string("/home/joe")),
},
Example {
description: "Replace the part that would be returned with a custom path",
example: "'/home/joe/code/test.txt' | path dirname -n 2 -r /home/viking",
result: Some(Value::test_string("/home/viking/code/test.txt")),
},
]
}
}
fn get_dirname(path: &Path, span: Span, args: &Arguments) -> Value {
let num_levels = args.num_levels.as_ref().map_or(1, |val| *val);
let mut dirname = path;
let mut reached_top = false;
for _ in 0..num_levels {
dirname = dirname.parent().unwrap_or_else(|| {
reached_top = true;
dirname
});
if reached_top {
break;
}
}
let path = match args.replace {
Some(ref newdir) => {
let remainder = path.strip_prefix(dirname).unwrap_or(dirname);
if !remainder.as_os_str().is_empty() {
Path::new(&newdir.item).join(remainder)
} else {
Path::new(&newdir.item).to_path_buf()
}
}
None => dirname.to_path_buf(),
};
Value::string(path.to_string_lossy(), span)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}

View file

@ -0,0 +1,113 @@
use std::path::Path;
use nu_engine::CallExt;
use nu_protocol::{engine::Command, Example, Signature, Span, SyntaxShape, Value};
use super::PathSubcommandArguments;
struct Arguments {
columns: Option<Vec<String>>,
}
impl PathSubcommandArguments for Arguments {
fn get_columns(&self) -> Option<Vec<String>> {
self.columns.clone()
}
}
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"path exists"
}
fn signature(&self) -> Signature {
Signature::build("path exists").named(
"columns",
SyntaxShape::Table,
"Optionally operate by column path",
Some('c'),
)
}
fn usage(&self) -> &str {
"Check whether a path exists"
}
fn run(
&self,
engine_state: &nu_protocol::engine::EngineState,
stack: &mut nu_protocol::engine::Stack,
call: &nu_protocol::ast::Call,
input: nu_protocol::PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let head = call.head;
let args = Arguments {
columns: call.get_flag(engine_state, stack, "columns")?,
};
input.map(
move |value| super::operate(&exists, &args, value, head),
engine_state.ctrlc.clone(),
)
}
#[cfg(windows)]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Check if a file exists",
example: "'C:\\Users\\joe\\todo.txt' | path exists",
result: Some(Value::Bool {
val: false,
span: Span::unknown(),
}),
},
Example {
description: "Check if a file exists in a column",
example: "ls | path exists -c [ name ]",
result: None,
},
]
}
#[cfg(not(windows))]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Check if a file exists",
example: "'/home/joe/todo.txt' | path exists",
result: Some(Value::Bool {
val: false,
span: Span::unknown(),
}),
},
Example {
description: "Check if a file exists in a column",
example: "ls | path exists -c [ name ]",
result: None,
},
]
}
}
fn exists(path: &Path, span: Span, _args: &Arguments) -> Value {
Value::Bool {
val: path.exists(),
span,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}

View file

@ -0,0 +1,136 @@
use std::path::Path;
use nu_engine::CallExt;
use nu_path::{canonicalize, expand_path};
use nu_protocol::{engine::Command, Example, ShellError, Signature, Span, SyntaxShape, Value};
use super::PathSubcommandArguments;
struct Arguments {
strict: bool,
columns: Option<Vec<String>>,
}
impl PathSubcommandArguments for Arguments {
fn get_columns(&self) -> Option<Vec<String>> {
self.columns.clone()
}
}
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"path expand"
}
fn signature(&self) -> Signature {
Signature::build("path expand")
.switch(
"strict",
"Throw an error if the path could not be expanded",
Some('s'),
)
.named(
"columns",
SyntaxShape::Table,
"Optionally operate by column path",
Some('c'),
)
}
fn usage(&self) -> &str {
"Try to expand a path to its absolute form"
}
fn run(
&self,
engine_state: &nu_protocol::engine::EngineState,
stack: &mut nu_protocol::engine::Stack,
call: &nu_protocol::ast::Call,
input: nu_protocol::PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let head = call.head;
let args = Arguments {
strict: call.has_flag("strict"),
columns: call.get_flag(engine_state, stack, "columns")?,
};
input.map(
move |value| super::operate(&expand, &args, value, head),
engine_state.ctrlc.clone(),
)
}
#[cfg(windows)]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Expand an absolute path",
example: r"'C:\Users\joe\foo\..\bar' | path expand",
result: Some(Value::test_string(r"C:\Users\joe\bar")),
},
Example {
description: "Expand a path in a column",
example: "ls | path expand -c [ name ]",
result: None,
},
Example {
description: "Expand a relative path",
example: r"'foo\..\bar' | path expand",
result: Some(Value::test_string("bar")),
},
]
}
#[cfg(not(windows))]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Expand an absolute path",
example: "'/home/joe/foo/../bar' | path expand",
result: Some(Value::test_string("/home/joe/bar")),
},
Example {
description: "Expand a path in a column",
example: "ls | path expand -c [ name ]",
result: None,
},
Example {
description: "Expand a relative path",
example: "'foo/../bar' | path expand",
result: Some(Value::test_string("bar")),
},
]
}
}
fn expand(path: &Path, span: Span, args: &Arguments) -> Value {
if let Ok(p) = canonicalize(path) {
Value::string(p.to_string_lossy(), span)
} else if args.strict {
Value::Error {
error: ShellError::LabeledError(
"Could not expand path".into(),
"could not be expanded (path might not exist, non-final \
component is not a directory, or other cause)"
.into(),
),
}
} else {
Value::string(expand_path(path).to_string_lossy(), span)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}

View file

@ -0,0 +1,270 @@
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use nu_engine::CallExt;
use nu_protocol::{
engine::Command, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape,
Value, ValueStream,
};
use super::PathSubcommandArguments;
struct Arguments {
columns: Option<Vec<String>>,
append: Option<Spanned<String>>,
}
impl PathSubcommandArguments for Arguments {
fn get_columns(&self) -> Option<Vec<String>> {
self.columns.clone()
}
}
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"path join"
}
fn signature(&self) -> Signature {
Signature::build("path join")
.named(
"columns",
SyntaxShape::Table,
"Optionally operate by column path",
Some('c'),
)
.optional(
"append",
SyntaxShape::Filepath,
"Path to append to the input",
)
}
fn usage(&self) -> &str {
"Join a structured path or a list of path parts."
}
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' subcommands."#
}
fn run(
&self,
engine_state: &nu_protocol::engine::EngineState,
stack: &mut nu_protocol::engine::Stack,
call: &nu_protocol::ast::Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
let args = Arguments {
columns: call.get_flag(engine_state, stack, "columns")?,
append: call.opt(engine_state, stack, 0)?,
};
match input {
PipelineData::Value(val, md) => {
Ok(PipelineData::Value(handle_value(val, &args, head), md))
}
PipelineData::Stream(stream, md) => Ok(PipelineData::Stream(
ValueStream::from_stream(
stream.map(move |val| handle_value(val, &args, head)),
engine_state.ctrlc.clone(),
),
md,
)),
}
}
#[cfg(windows)]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Append a filename to a path",
example: r"'C:\Users\viking' | path join spam.txt",
result: Some(Value::test_string(r"C:\Users\viking\spam.txt")),
},
Example {
description: "Append a filename to a path inside a column",
example: r"ls | path join spam.txt -c [ name ]",
result: None,
},
Example {
description: "Join a list of parts into a path",
example: r"[ 'C:' '\' 'Users' 'viking' 'spam.txt' ] | path join",
result: Some(Value::test_string(r"C:\Users\viking\spam.txt")),
},
Example {
description: "Join a structured path into a path",
example: r"[ [parent stem extension]; ['C:\Users\viking' 'spam' 'txt']] | path join",
result: Some(Value::List {
vals: vec![Value::test_string(r"C:\Users\viking\spam.txt")],
span: Span::unknown(),
}),
},
]
}
#[cfg(not(windows))]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Append a filename to a path",
example: r"'/home/viking' | path join spam.txt",
result: Some(Value::test_string(r"/home/viking/spam.txt")),
},
Example {
description: "Append a filename to a path inside a column",
example: r"ls | path join spam.txt -c [ name ]",
result: None,
},
Example {
description: "Join a list of parts into a path",
example: r"[ '/' 'home' 'viking' 'spam.txt' ] | path join",
result: Some(Value::test_string(r"/home/viking/spam.txt")),
},
Example {
description: "Join a structured path into a path",
example: r"[[ parent stem extension ]; [ '/home/viking' 'spam' 'txt' ]] | path join",
result: Some(Value::List {
vals: vec![Value::test_string(r"/home/viking/spam.txt")],
span: Span::unknown(),
}),
},
]
}
}
fn handle_value(v: Value, args: &Arguments, head: Span) -> Value {
match v {
Value::String { ref val, span } => join_single(Path::new(val), span, args),
Value::Record { cols, vals, span } => join_record(&cols, &vals, span, args),
Value::List { vals, span } => join_list(&vals, span, args),
_ => super::handle_invalid_values(v, head),
}
}
fn join_single(path: &Path, span: Span, args: &Arguments) -> Value {
let path = if let Some(ref append) = args.append {
path.join(Path::new(&append.item))
} else {
path.to_path_buf()
};
Value::string(path.to_string_lossy(), span)
}
fn join_list(parts: &[Value], span: Span, args: &Arguments) -> Value {
let path: Result<PathBuf, ShellError> = parts.iter().map(Value::as_string).collect();
match path {
Ok(ref path) => join_single(path, span, args),
Err(_) => {
let records: Result<Vec<_>, ShellError> = parts.iter().map(Value::as_record).collect();
match records {
Ok(vals) => {
let vals = vals
.iter()
.map(|(k, v)| join_record(k, v, span, args))
.collect();
Value::List { vals, span }
}
Err(_) => Value::Error {
error: ShellError::PipelineMismatch("string or record".into(), span, span),
},
}
}
}
}
fn join_record(cols: &[String], vals: &[Value], span: Span, args: &Arguments) -> Value {
if args.columns.is_some() {
super::operate(
&join_single,
args,
Value::Record {
cols: cols.to_vec(),
vals: vals.to_vec(),
span,
},
span,
)
} else {
match merge_record(cols, vals, span) {
Ok(p) => join_single(p.as_path(), span, args),
Err(error) => Value::Error { error },
}
}
}
fn merge_record(cols: &[String], vals: &[Value], span: Span) -> Result<PathBuf, ShellError> {
for key in cols {
if !super::ALLOWED_COLUMNS.contains(&key.as_str()) {
let allowed_cols = super::ALLOWED_COLUMNS.join(", ");
let msg = format!(
"Column '{}' is not valid for a structured path. Allowed columns are: {}",
key, allowed_cols
);
return Err(ShellError::UnsupportedInput(msg, span));
}
}
let entries: HashMap<&str, &Value> = cols.iter().map(String::as_str).zip(vals).collect();
let mut result = PathBuf::new();
#[cfg(windows)]
if let Some(val) = entries.get("prefix") {
let p = val.as_string()?;
if !p.is_empty() {
result.push(p);
}
}
if let Some(val) = entries.get("parent") {
let p = val.as_string()?;
if !p.is_empty() {
result.push(p);
}
}
let mut basename = String::new();
if let Some(val) = entries.get("stem") {
let p = val.as_string()?;
if !p.is_empty() {
basename.push_str(&p);
}
}
if let Some(val) = entries.get("extension") {
let p = val.as_string()?;
if !p.is_empty() {
basename.push('.');
basename.push_str(&p);
}
}
if !basename.is_empty() {
result.push(basename);
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}

View file

@ -0,0 +1,95 @@
mod basename;
pub mod command;
mod dirname;
mod exists;
mod expand;
mod join;
mod parse;
mod relative_to;
mod split;
mod r#type;
use std::path::Path as StdPath;
pub use basename::SubCommand as PathBasename;
pub use command::PathCommand as Path;
pub use dirname::SubCommand as PathDirname;
pub use exists::SubCommand as PathExists;
pub use expand::SubCommand as PathExpand;
pub use join::SubCommand as PathJoin;
pub use parse::SubCommand as PathParse;
pub use r#type::SubCommand as PathType;
pub use relative_to::SubCommand as PathRelativeTo;
pub use split::SubCommand as PathSplit;
use nu_protocol::{ShellError, Span, Value};
#[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_columns(&self) -> Option<Vec<String>>;
}
fn operate<F, A>(cmd: &F, args: &A, v: Value, name: Span) -> Value
where
F: Fn(&StdPath, Span, &A) -> Value + Send + Sync + 'static,
A: PathSubcommandArguments + Send + Sync + 'static,
{
match v {
Value::String { val, span } => cmd(StdPath::new(&val), span, args),
Value::Record { cols, vals, span } => {
let col = if let Some(col) = args.get_columns() {
col
} else {
vec![]
};
if col.is_empty() {
return Value::Error {
error: ShellError::UnsupportedInput(
String::from("when the input is a table, you must specify the columns"),
name,
),
};
}
let mut output_cols = vec![];
let mut output_vals = vec![];
for (k, v) in cols.iter().zip(vals) {
output_cols.push(k.clone());
if col.contains(k) {
let new_val = match v {
Value::String { val, span } => cmd(StdPath::new(&val), span, args),
_ => return handle_invalid_values(v, name),
};
output_vals.push(new_val);
} else {
output_vals.push(v);
}
}
Value::Record {
cols: output_cols,
vals: output_vals,
span,
}
}
_ => handle_invalid_values(v, name),
}
}
fn handle_invalid_values(rest: Value, name: Span) -> Value {
Value::Error {
error: err_from_value(&rest, name),
}
}
fn err_from_value(rest: &Value, name: Span) -> ShellError {
match rest.span() {
Ok(span) => ShellError::PipelineMismatch("string, row or list".into(), name, span),
Err(error) => error,
}
}

View file

@ -0,0 +1,201 @@
use std::path::Path;
use indexmap::IndexMap;
use nu_engine::CallExt;
use nu_protocol::{
engine::Command, Example, ShellError, Signature, Span, Spanned, SyntaxShape, Value,
};
use super::PathSubcommandArguments;
struct Arguments {
columns: Option<Vec<String>>,
extension: Option<Spanned<String>>,
}
impl PathSubcommandArguments for Arguments {
fn get_columns(&self) -> Option<Vec<String>> {
self.columns.clone()
}
}
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"path parse"
}
fn signature(&self) -> Signature {
Signature::build("path parse")
.named(
"columns",
SyntaxShape::Table,
"Optionally operate by column path",
Some('c'),
)
.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,
engine_state: &nu_protocol::engine::EngineState,
stack: &mut nu_protocol::engine::Stack,
call: &nu_protocol::ast::Call,
input: nu_protocol::PipelineData,
) -> Result<nu_protocol::PipelineData, ShellError> {
let head = call.head;
let args = Arguments {
columns: call.get_flag(engine_state, stack, "columns")?,
extension: call.get_flag(engine_state, stack, "extension")?,
};
input.map(
move |value| super::operate(&parse, &args, value, head),
engine_state.ctrlc.clone(),
)
}
#[cfg(windows)]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Parse a single path",
example: r"'C:\Users\viking\spam.txt' | path parse",
result: None,
},
Example {
description: "Replace a complex extension",
example: r"'C:\Users\viking\spam.tar.gz' | path parse -e tar.gz | update extension { 'txt' }",
result: None,
},
Example {
description: "Ignore the extension",
example: r"'C:\Users\viking.d' | path parse -e ''",
result: None,
},
Example {
description: "Parse all paths under the 'name' column",
example: r"ls | path parse -c [ name ]",
result: None,
},
]
}
#[cfg(not(windows))]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Parse a path",
example: r"'/home/viking/spam.txt' | path parse",
result: None,
},
Example {
description: "Replace a complex extension",
example: r"'/home/viking/spam.tar.gz' | path parse -e tar.gz | update extension { 'txt' }",
result: None,
},
Example {
description: "Ignore the extension",
example: r"'/etc/conf.d' | path parse -e ''",
result: None,
},
Example {
description: "Parse all paths under the 'name' column",
example: r"ls | path parse -c [ name ]",
result: None,
},
]
}
}
fn parse(path: &Path, span: Span, args: &Arguments) -> Value {
let mut map: IndexMap<String, Value> = IndexMap::new();
#[cfg(windows)]
{
use std::path::Component;
let prefix = match path.components().next() {
Some(Component::Prefix(prefix_component)) => {
prefix_component.as_os_str().to_string_lossy()
}
_ => "".into(),
};
map.insert("prefix".into(), Value::string(prefix, span));
}
let parent = path
.parent()
.unwrap_or_else(|| "".as_ref())
.to_string_lossy();
map.insert("parent".into(), Value::string(parent, span));
let basename = path
.file_name()
.unwrap_or_else(|| "".as_ref())
.to_string_lossy();
match &args.extension {
Some(Spanned {
item: extension,
span: extension_span,
}) => {
let ext_with_dot = [".", extension].concat();
if basename.ends_with(&ext_with_dot) && !extension.is_empty() {
let stem = basename.trim_end_matches(&ext_with_dot);
map.insert("stem".into(), Value::string(stem, span));
map.insert(
"extension".into(),
Value::string(extension, *extension_span),
);
} else {
map.insert("stem".into(), Value::string(basename, span));
map.insert("extension".into(), Value::string("", span));
}
}
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();
map.insert("stem".into(), Value::string(stem, span));
map.insert("extension".into(), Value::string(extension, span));
}
}
Value::from(Spanned { item: map, span })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}

View file

@ -0,0 +1,133 @@
use std::path::Path;
use nu_engine::CallExt;
use nu_protocol::{
engine::Command, Example, ShellError, Signature, Span, Spanned, SyntaxShape, Value,
};
use super::PathSubcommandArguments;
struct Arguments {
path: Spanned<String>,
columns: Option<Vec<String>>,
}
impl PathSubcommandArguments for Arguments {
fn get_columns(&self) -> Option<Vec<String>> {
self.columns.clone()
}
}
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"path relative-to"
}
fn signature(&self) -> Signature {
Signature::build("path relative-to")
.required(
"path",
SyntaxShape::Filepath,
"Parent shared with the input path",
)
.named(
"columns",
SyntaxShape::Table,
"Optionally operate by column path",
Some('c'),
)
}
fn usage(&self) -> &str {
"Get a path as relative to another path."
}
fn extra_usage(&self) -> &str {
r#"Can be used only when the input and the argument paths are either both
absolute or both relative. The argument path needs to be a parent of the input
path."#
}
fn run(
&self,
engine_state: &nu_protocol::engine::EngineState,
stack: &mut nu_protocol::engine::Stack,
call: &nu_protocol::ast::Call,
input: nu_protocol::PipelineData,
) -> Result<nu_protocol::PipelineData, ShellError> {
let head = call.head;
let args = Arguments {
path: call.req(engine_state, stack, 0)?,
columns: call.get_flag(engine_state, stack, "columns")?,
};
input.map(
move |value| super::operate(&relative_to, &args, value, head),
engine_state.ctrlc.clone(),
)
}
#[cfg(windows)]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Find a relative path from two absolute paths",
example: r"'C:\Users\viking' | path relative-to 'C:\Users'",
result: Some(Value::test_string(r"viking")),
},
Example {
description: "Find a relative path from two absolute paths in a column",
example: "ls ~ | path relative-to ~ -c [ name ]",
result: None,
},
Example {
description: "Find a relative path from two relative paths",
example: r"'eggs\bacon\sausage\spam' | path relative-to 'eggs\bacon\sausage'",
result: Some(Value::test_string(r"spam")),
},
]
}
#[cfg(not(windows))]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Find a relative path from two absolute paths",
example: r"'/home/viking' | path relative-to '/home'",
result: Some(Value::test_string(r"viking")),
},
Example {
description: "Find a relative path from two absolute paths in a column",
example: "ls ~ | path relative-to ~ -c [ name ]",
result: None,
},
Example {
description: "Find a relative path from two relative paths",
example: r"'eggs/bacon/sausage/spam' | path relative-to 'eggs/bacon/sausage'",
result: Some(Value::test_string(r"spam")),
},
]
}
}
fn relative_to(path: &Path, span: Span, args: &Arguments) -> Value {
match path.strip_prefix(Path::new(&args.path.item)) {
Ok(p) => Value::string(p.to_string_lossy(), span),
Err(_) => todo!(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}

View file

@ -0,0 +1,130 @@
use std::path::Path;
use nu_engine::CallExt;
use nu_protocol::{engine::Command, Example, ShellError, Signature, Span, SyntaxShape, Value};
use super::PathSubcommandArguments;
struct Arguments {
columns: Option<Vec<String>>,
}
impl PathSubcommandArguments for Arguments {
fn get_columns(&self) -> Option<Vec<String>> {
self.columns.clone()
}
}
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"path split"
}
fn signature(&self) -> Signature {
Signature::build("path split").named(
"columns",
SyntaxShape::Table,
"Optionally operate by column path",
Some('c'),
)
}
fn usage(&self) -> &str {
"Split a path into parts by a separator."
}
fn run(
&self,
engine_state: &nu_protocol::engine::EngineState,
stack: &mut nu_protocol::engine::Stack,
call: &nu_protocol::ast::Call,
input: nu_protocol::PipelineData,
) -> Result<nu_protocol::PipelineData, ShellError> {
let head = call.head;
let args = Arguments {
columns: call.get_flag(engine_state, stack, "columns")?,
};
input.map(
move |value| super::operate(&split, &args, value, head),
engine_state.ctrlc.clone(),
)
}
#[cfg(windows)]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Split a path into parts",
example: r"'C:\Users\viking\spam.txt' | path split",
result: Some(Value::List {
vals: vec![
Value::test_string("C:"),
Value::test_string(r"\"),
Value::test_string("Users"),
Value::test_string("viking"),
Value::test_string("spam.txt"),
],
span: Span::unknown(),
}),
},
Example {
description: "Split all paths under the 'name' column",
example: r"ls ('.' | path expand) | path split -c [ name ]",
result: None,
},
]
}
#[cfg(not(windows))]
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Split a path into parts",
example: r"'/home/viking/spam.txt' | path split",
result: Some(Value::List {
vals: vec![
Value::test_string("/"),
Value::test_string("home"),
Value::test_string("viking"),
Value::test_string("spam.txt"),
],
span: Span::unknown(),
}),
},
Example {
description: "Split all paths under the 'name' column",
example: r"ls ('.' | path expand) | path split -c [ name ]",
result: None,
},
]
}
}
fn split(path: &Path, span: Span, _: &Arguments) -> Value {
Value::List {
vals: path
.components()
.map(|comp| {
let s = comp.as_os_str().to_string_lossy();
Value::string(s, span)
})
.collect(),
span,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}

View file

@ -0,0 +1,122 @@
use std::path::Path;
use nu_engine::CallExt;
use nu_protocol::{engine::Command, Example, ShellError, Signature, Span, SyntaxShape, Value};
use super::PathSubcommandArguments;
struct Arguments {
columns: Option<Vec<String>>,
}
impl PathSubcommandArguments for Arguments {
fn get_columns(&self) -> Option<Vec<String>> {
self.columns.clone()
}
}
#[derive(Clone)]
pub struct SubCommand;
impl Command for SubCommand {
fn name(&self) -> &str {
"path type"
}
fn signature(&self) -> Signature {
Signature::build("path type").named(
"columns",
SyntaxShape::Table,
"Optionally operate by column path",
Some('c'),
)
}
fn usage(&self) -> &str {
"Get the type of the object a path refers to (e.g., file, dir, symlink)"
}
fn run(
&self,
engine_state: &nu_protocol::engine::EngineState,
stack: &mut nu_protocol::engine::Stack,
call: &nu_protocol::ast::Call,
input: nu_protocol::PipelineData,
) -> Result<nu_protocol::PipelineData, ShellError> {
let head = call.head;
let args = Arguments {
columns: call.get_flag(engine_state, stack, "columns")?,
};
input.map(
move |value| super::operate(&r#type, &args, value, head),
engine_state.ctrlc.clone(),
)
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Show type of a filepath",
example: "'.' | path type",
result: Some(Value::test_string("Dir")),
},
Example {
description: "Show type of a filepath in a column",
example: "ls | path type -c [ name ]",
result: None,
},
]
}
}
fn r#type(path: &Path, span: Span, _: &Arguments) -> Value {
let meta = std::fs::symlink_metadata(path);
Value::string(
match &meta {
Ok(data) => get_file_type(data),
Err(_) => "",
},
span,
)
}
fn get_file_type(md: &std::fs::Metadata) -> &str {
let ft = md.file_type();
let mut file_type = "Unknown";
if ft.is_dir() {
file_type = "Dir";
} else if ft.is_file() {
file_type = "File";
} else if ft.is_symlink() {
file_type = "Symlink";
} else {
#[cfg(unix)]
{
use std::os::unix::fs::FileTypeExt;
if ft.is_block_device() {
file_type = "Block device";
} else if ft.is_char_device() {
file_type = "Char device";
} else if ft.is_fifo() {
file_type = "Pipe";
} else if ft.is_socket() {
file_type = "Socket";
}
}
}
file_type
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(SubCommand {})
}
}