nushell/crates/nu-command/src/path/join.rs
JT 3522bead97
Add string stream and binary stream, add text decoding (#570)
* WIP

* Add binary/string streams and text decoding

* Make string collection fallible

* Oops, forgot pretty hex

* Oops, forgot pretty hex

* clippy
2021-12-24 18:22:11 +11:00

275 lines
8.2 KiB
Rust

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::ListStream(stream, md) => Ok(PipelineData::ListStream(
ValueStream::from_stream(
stream.map(move |val| handle_value(val, &args, head)),
engine_state.ctrlc.clone(),
),
md,
)),
_ => Err(ShellError::UnsupportedInput(
"Input data is not supported by this command.".to_string(),
head,
)),
}
}
#[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::test_data(),
}),
},
]
}
#[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::test_data(),
}),
},
]
}
}
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 {})
}
}