mirror of
https://github.com/nushell/nushell.git
synced 2025-05-16 12:44:34 +00:00
# Description When implementing a `Command`, one must also import all the types present in the function signatures for `Command`. This makes it so that we often import the same set of types in each command implementation file. E.g., something like this: ```rust use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value, }; ``` This PR adds the `nu_engine::command_prelude` module which contains the necessary and commonly used types to implement a `Command`: ```rust // command_prelude.rs pub use crate::CallExt; pub use nu_protocol::{ ast::{Call, CellPath}, engine::{Command, EngineState, Stack}, record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, IntoSpanned, PipelineData, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; ``` This should reduce the boilerplate needed to implement a command and also gives us a place to track the breadth of the `Command` API. I tried to be conservative with what went into the prelude modules, since it might be hard/annoying to remove items from the prelude in the future. Let me know if something should be included or excluded.
305 lines
10 KiB
Rust
305 lines
10 KiB
Rust
use super::PathSubcommandArguments;
|
|
use nu_engine::command_prelude::*;
|
|
use nu_protocol::engine::StateWorkingSet;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
struct Arguments {
|
|
append: Vec<Spanned<String>>,
|
|
}
|
|
|
|
impl PathSubcommandArguments for Arguments {}
|
|
|
|
#[derive(Clone)]
|
|
pub struct SubCommand;
|
|
|
|
impl Command for SubCommand {
|
|
fn name(&self) -> &str {
|
|
"path join"
|
|
}
|
|
|
|
fn signature(&self) -> Signature {
|
|
Signature::build("path join")
|
|
.input_output_types(vec![
|
|
(Type::String, Type::String),
|
|
(Type::List(Box::new(Type::String)), Type::String),
|
|
(Type::Record(vec![]), Type::String),
|
|
(Type::Table(vec![]), Type::List(Box::new(Type::String))),
|
|
])
|
|
.rest(
|
|
"append",
|
|
SyntaxShape::String,
|
|
"Path to append to the input.",
|
|
)
|
|
.category(Category::Path)
|
|
}
|
|
|
|
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 is_const(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn run(
|
|
&self,
|
|
engine_state: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
let args = Arguments {
|
|
append: call.rest(engine_state, stack, 0)?,
|
|
};
|
|
|
|
run(call, &args, input)
|
|
}
|
|
|
|
fn run_const(
|
|
&self,
|
|
working_set: &StateWorkingSet,
|
|
call: &Call,
|
|
input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
let args = Arguments {
|
|
append: call.rest_const(working_set, 0)?,
|
|
};
|
|
|
|
run(call, &args, input)
|
|
}
|
|
|
|
#[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",
|
|
example: r"'C:\Users\viking' | path join spams this_spam.txt",
|
|
result: Some(Value::test_string(r"C:\Users\viking\spams\this_spam.txt")),
|
|
},
|
|
Example {
|
|
description: "Use relative paths, e.g. '..' will go up one directory",
|
|
example: r"'C:\Users\viking' | path join .. folder",
|
|
result: Some(Value::test_string(r"C:\Users\viking\..\folder")),
|
|
},
|
|
Example {
|
|
description:
|
|
"Use absolute paths, e.g. '/' will bring you to the top level directory",
|
|
example: r"'C:\Users\viking' | path join / folder",
|
|
result: Some(Value::test_string(r"C:/folder")),
|
|
},
|
|
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: 'C:\Users\viking', stem: 'spam', extension: 'txt' } | path join",
|
|
result: Some(Value::test_string(r"C:\Users\viking\spam.txt")),
|
|
},
|
|
Example {
|
|
description: "Join a table of structured paths into a list of paths",
|
|
example: r"[ [parent stem extension]; ['C:\Users\viking' 'spam' 'txt']] | path join",
|
|
result: Some(Value::list(
|
|
vec![Value::test_string(r"C:\Users\viking\spam.txt")],
|
|
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",
|
|
example: r"'/home/viking' | path join spams this_spam.txt",
|
|
result: Some(Value::test_string(r"/home/viking/spams/this_spam.txt")),
|
|
},
|
|
Example {
|
|
description: "Use relative paths, e.g. '..' will go up one directory",
|
|
example: r"'/home/viking' | path join .. folder",
|
|
result: Some(Value::test_string(r"/home/viking/../folder")),
|
|
},
|
|
Example {
|
|
description:
|
|
"Use absolute paths, e.g. '/' will bring you to the top level directory",
|
|
example: r"'/home/viking' | path join / folder",
|
|
result: Some(Value::test_string(r"/folder")),
|
|
},
|
|
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: '/home/viking', stem: 'spam', extension: 'txt' } | path join",
|
|
result: Some(Value::test_string(r"/home/viking/spam.txt")),
|
|
},
|
|
Example {
|
|
description: "Join a table of structured paths into a list of paths",
|
|
example: r"[[ parent stem extension ]; [ '/home/viking' 'spam' 'txt' ]] | path join",
|
|
result: Some(Value::list(
|
|
vec![Value::test_string(r"/home/viking/spam.txt")],
|
|
Span::test_data(),
|
|
)),
|
|
},
|
|
]
|
|
}
|
|
}
|
|
|
|
fn run(call: &Call, args: &Arguments, input: PipelineData) -> Result<PipelineData, ShellError> {
|
|
let head = call.head;
|
|
|
|
let metadata = input.metadata();
|
|
|
|
match input {
|
|
PipelineData::Value(val, md) => Ok(PipelineData::Value(handle_value(val, args, head), md)),
|
|
PipelineData::ListStream(..) => Ok(PipelineData::Value(
|
|
handle_value(input.into_value(head), args, head),
|
|
metadata,
|
|
)),
|
|
PipelineData::Empty { .. } => Err(ShellError::PipelineEmpty { dst_span: head }),
|
|
_ => Err(ShellError::UnsupportedInput {
|
|
msg: "Input value cannot be joined".to_string(),
|
|
input: "value originates from here".into(),
|
|
msg_span: head,
|
|
input_span: input.span().unwrap_or(call.head),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn handle_value(v: Value, args: &Arguments, head: Span) -> Value {
|
|
let span = v.span();
|
|
match v {
|
|
Value::String { ref val, .. } => join_single(Path::new(val), head, args),
|
|
Value::Record { val, .. } => join_record(&val, head, span, args),
|
|
Value::List { vals, .. } => join_list(&vals, head, span, args),
|
|
|
|
_ => super::handle_invalid_values(v, head),
|
|
}
|
|
}
|
|
|
|
fn join_single(path: &Path, head: Span, args: &Arguments) -> Value {
|
|
let mut result = path.to_path_buf();
|
|
for path_to_append in &args.append {
|
|
result.push(&path_to_append.item)
|
|
}
|
|
|
|
Value::string(result.to_string_lossy(), head)
|
|
}
|
|
|
|
fn join_list(parts: &[Value], head: Span, span: Span, args: &Arguments) -> Value {
|
|
let path: Result<PathBuf, ShellError> = parts.iter().map(Value::coerce_string).collect();
|
|
|
|
match path {
|
|
Ok(ref path) => join_single(path, head, args),
|
|
Err(_) => {
|
|
let records: Result<Vec<_>, ShellError> = parts.iter().map(Value::as_record).collect();
|
|
match records {
|
|
Ok(vals) => {
|
|
let vals = vals
|
|
.iter()
|
|
.map(|r| join_record(r, head, span, args))
|
|
.collect();
|
|
|
|
Value::list(vals, span)
|
|
}
|
|
Err(_) => Value::error(
|
|
ShellError::PipelineMismatch {
|
|
exp_input_type: "string or record".into(),
|
|
dst_span: head,
|
|
src_span: span,
|
|
},
|
|
span,
|
|
),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn join_record(record: &Record, head: Span, span: Span, args: &Arguments) -> Value {
|
|
match merge_record(record, head, span) {
|
|
Ok(p) => join_single(p.as_path(), head, args),
|
|
Err(error) => Value::error(error, span),
|
|
}
|
|
}
|
|
|
|
fn merge_record(record: &Record, head: Span, span: Span) -> Result<PathBuf, ShellError> {
|
|
for key in record.columns() {
|
|
if !super::ALLOWED_COLUMNS.contains(&key.as_str()) {
|
|
let allowed_cols = super::ALLOWED_COLUMNS.join(", ");
|
|
return Err(ShellError::UnsupportedInput { msg: format!(
|
|
"Column '{key}' is not valid for a structured path. Allowed columns on this platform are: {allowed_cols}"
|
|
), input: "value originates from here".into(), msg_span: head, input_span: span });
|
|
}
|
|
}
|
|
|
|
let mut result = PathBuf::new();
|
|
|
|
#[cfg(windows)]
|
|
if let Some(val) = record.get("prefix") {
|
|
let p = val.coerce_str()?;
|
|
if !p.is_empty() {
|
|
result.push(p.as_ref());
|
|
}
|
|
}
|
|
|
|
if let Some(val) = record.get("parent") {
|
|
let p = val.coerce_str()?;
|
|
if !p.is_empty() {
|
|
result.push(p.as_ref());
|
|
}
|
|
}
|
|
|
|
let mut basename = String::new();
|
|
if let Some(val) = record.get("stem") {
|
|
let p = val.coerce_str()?;
|
|
if !p.is_empty() {
|
|
basename.push_str(&p);
|
|
}
|
|
}
|
|
|
|
if let Some(val) = record.get("extension") {
|
|
let p = val.coerce_str()?;
|
|
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 {})
|
|
}
|
|
}
|