mirror of
https://github.com/nushell/nushell
synced 2024-12-27 21:43:09 +00:00
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:
parent
bee7ef21eb
commit
90ddb23492
15 changed files with 1594 additions and 2 deletions
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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 |
|
||||
| ----------- |
|
||||
|
|
|
@ -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::*;
|
||||
|
|
151
crates/nu-command/src/path/basename.rs
Normal file
151
crates/nu-command/src/path/basename.rs
Normal 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 {})
|
||||
}
|
||||
}
|
56
crates/nu-command/src/path/command.rs
Normal file
56
crates/nu-command/src/path/command.rs
Normal 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())
|
||||
}
|
||||
}
|
168
crates/nu-command/src/path/dirname.rs
Normal file
168
crates/nu-command/src/path/dirname.rs
Normal 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 {})
|
||||
}
|
||||
}
|
113
crates/nu-command/src/path/exists.rs
Normal file
113
crates/nu-command/src/path/exists.rs
Normal 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 {})
|
||||
}
|
||||
}
|
136
crates/nu-command/src/path/expand.rs
Normal file
136
crates/nu-command/src/path/expand.rs
Normal 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 {})
|
||||
}
|
||||
}
|
270
crates/nu-command/src/path/join.rs
Normal file
270
crates/nu-command/src/path/join.rs
Normal 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 {})
|
||||
}
|
||||
}
|
95
crates/nu-command/src/path/mod.rs
Normal file
95
crates/nu-command/src/path/mod.rs
Normal 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,
|
||||
}
|
||||
}
|
201
crates/nu-command/src/path/parse.rs
Normal file
201
crates/nu-command/src/path/parse.rs
Normal 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 {})
|
||||
}
|
||||
}
|
133
crates/nu-command/src/path/relative_to.rs
Normal file
133
crates/nu-command/src/path/relative_to.rs
Normal 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 {})
|
||||
}
|
||||
}
|
130
crates/nu-command/src/path/split.rs
Normal file
130
crates/nu-command/src/path/split.rs
Normal 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 {})
|
||||
}
|
||||
}
|
122
crates/nu-command/src/path/type.rs
Normal file
122
crates/nu-command/src/path/type.rs
Normal 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 {})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue