nushell/crates/nu-command/src/path/join.rs
JT 6c730def4b
revert: move to ahash (#9464)
This PR reverts https://github.com/nushell/nushell/pull/9391

We try not to revert PRs like this, though after discussion with the
Nushell team, we decided to revert this one.

The main reason is that Nushell, as a codebase, isn't ready for these
kinds of optimisations. It's in the part of the development cycle where
our main focus should be on improving the algorithms inside of Nushell
itself. Once we have matured our algorithms, then we can look for
opportunities to switch out technologies we're using for alternate
forms.

Much of Nushell still has lots of opportunities for tuning the codebase,
paying down technical debt, and making the codebase generally cleaner
and more robust. This should be the focus. Performance improvements
should flow out of that work.

Said another, optimisation that isn't part of tuning the codebase is
premature at this stage. We need to focus on doing the hard work of
making the engine, parser, etc better.

# User-Facing Changes

Reverts the HashMap -> ahash change.

cc @FilipAndersson245
2023-06-18 15:27:57 +12:00

303 lines
9.5 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::Table(vec![]), Type::List(Box::new(Type::String))),
])
.named(
"columns",
SyntaxShape::Table,
"For a record or table input, join strings at the given columns",
Some('c'),
)
.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 {})
}
}