mirror of
https://github.com/nushell/nushell.git
synced 2025-05-01 08:34:26 +02:00
# Description This PR tights input/output type-checking a bit more. There are a lot of commands that don't have correct input/output types, so part of the effort is updating them. This PR now contains updates to commands that had wrong input/output signatures. It doesn't add examples for these new signatures, but that can be follow-up work. # User-Facing Changes BREAKING CHANGE BREAKING CHANGE This work enforces many more checks on pipeline type correctness than previous nushell versions. This strictness may uncover incompatibilities in existing scripts or shortcomings in the type information for internal commands. # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect -A clippy::result_large_err` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
305 lines
9.6 KiB
Rust
305 lines
9.6 KiB
Rust
use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use nu_engine::CallExt;
|
|
use nu_protocol::ast::Call;
|
|
use nu_protocol::engine::{EngineState, Stack};
|
|
use nu_protocol::{
|
|
engine::Command, Example, PipelineData, ShellError, Signature, Span, Spanned, SyntaxShape,
|
|
Type, Value,
|
|
};
|
|
|
|
use super::PathSubcommandArguments;
|
|
|
|
struct Arguments {
|
|
columns: Option<Vec<String>>,
|
|
append: Vec<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")
|
|
.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))),
|
|
])
|
|
.named(
|
|
"columns",
|
|
SyntaxShape::Table(vec![]),
|
|
"For a record or table input, join strings at the given columns",
|
|
Some('c'),
|
|
)
|
|
.allow_variants_without_examples(true)
|
|
.rest("append", SyntaxShape::String, "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: &EngineState,
|
|
stack: &mut Stack,
|
|
call: &Call,
|
|
input: PipelineData,
|
|
) -> Result<PipelineData, ShellError> {
|
|
let head = call.head;
|
|
let args = Arguments {
|
|
columns: call.get_flag(engine_state, stack, "columns")?,
|
|
append: call.rest(engine_state, stack, 0)?,
|
|
};
|
|
|
|
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(
|
|
"Input value cannot be joined".to_string(),
|
|
"value originates from here".into(),
|
|
head,
|
|
input
|
|
.span()
|
|
.expect("non-Empty non-ListStream PipelineData had no span"),
|
|
)),
|
|
}
|
|
}
|
|
|
|
#[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: "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",
|
|
example: r"'/home/viking' | path join spams this_spam.txt",
|
|
result: Some(Value::test_string(r"/home/viking/spams/this_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, .. } => join_single(Path::new(val), head, args),
|
|
Value::Record { cols, vals, span } => join_record(&cols, &vals, head, span, args),
|
|
Value::List { vals, span } => 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::as_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(|(k, v)| join_record(k, v, head, span, args))
|
|
.collect();
|
|
|
|
Value::List { vals, span }
|
|
}
|
|
Err(_) => Value::Error {
|
|
error: Box::new(ShellError::PipelineMismatch {
|
|
exp_input_type: "string or record".into(),
|
|
dst_span: head,
|
|
src_span: span,
|
|
}),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn join_record(cols: &[String], vals: &[Value], head: Span, 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, head, span) {
|
|
Ok(p) => join_single(p.as_path(), head, args),
|
|
Err(error) => Value::Error {
|
|
error: Box::new(error),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
fn merge_record(
|
|
cols: &[String],
|
|
vals: &[Value],
|
|
head: Span,
|
|
span: Span,
|
|
) -> Result<PathBuf, ShellError> {
|
|
for key in cols {
|
|
if !super::ALLOWED_COLUMNS.contains(&key.as_str()) {
|
|
let allowed_cols = super::ALLOWED_COLUMNS.join(", ");
|
|
return Err(ShellError::UnsupportedInput(
|
|
format!(
|
|
"Column '{key}' is not valid for a structured path. Allowed columns on this platform are: {allowed_cols}"
|
|
),
|
|
"value originates from here".into(),
|
|
head,
|
|
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 {})
|
|
}
|
|
}
|