mirror of
https://github.com/nushell/nushell.git
synced 2025-08-10 07:28:27 +02:00
Merge branch 'nushell:main' into feature-PWD-per-drive
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_parser::parse;
|
||||
use nu_protocol::engine::StateWorkingSet;
|
||||
use nu_parser::{flatten_block, parse};
|
||||
use nu_protocol::{engine::StateWorkingSet, record};
|
||||
use serde_json::{json, Value as JsonValue};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Ast;
|
||||
@ -16,109 +17,23 @@ impl Command for Ast {
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("ast")
|
||||
.input_output_types(vec![(Type::String, Type::record())])
|
||||
.input_output_types(vec![
|
||||
(Type::Nothing, Type::table()),
|
||||
(Type::Nothing, Type::record()),
|
||||
(Type::Nothing, Type::String),
|
||||
])
|
||||
.required(
|
||||
"pipeline",
|
||||
SyntaxShape::String,
|
||||
"The pipeline to print the ast for.",
|
||||
)
|
||||
.switch("json", "serialize to json", Some('j'))
|
||||
.switch("minify", "minify the nuon or json output", Some('m'))
|
||||
.switch("json", "Serialize to json", Some('j'))
|
||||
.switch("minify", "Minify the nuon or json output", Some('m'))
|
||||
.switch("flatten", "An easier to read version of the ast", Some('f'))
|
||||
.allow_variants_without_examples(true)
|
||||
.category(Category::Debug)
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let pipeline: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||
let to_json = call.has_flag(engine_state, stack, "json")?;
|
||||
let minify = call.has_flag(engine_state, stack, "minify")?;
|
||||
let mut working_set = StateWorkingSet::new(engine_state);
|
||||
let block_output = parse(&mut working_set, None, pipeline.item.as_bytes(), false);
|
||||
let error_output = working_set.parse_errors.first();
|
||||
let block_span = match &block_output.span {
|
||||
Some(span) => span,
|
||||
None => &pipeline.span,
|
||||
};
|
||||
if to_json {
|
||||
// Get the block as json
|
||||
let serde_block_str = if minify {
|
||||
serde_json::to_string(&*block_output)
|
||||
} else {
|
||||
serde_json::to_string_pretty(&*block_output)
|
||||
};
|
||||
let block_json = match serde_block_str {
|
||||
Ok(json) => json,
|
||||
Err(e) => Err(ShellError::CantConvert {
|
||||
to_type: "string".to_string(),
|
||||
from_type: "block".to_string(),
|
||||
span: *block_span,
|
||||
help: Some(format!(
|
||||
"Error: {e}\nCan't convert {block_output:?} to string"
|
||||
)),
|
||||
})?,
|
||||
};
|
||||
// Get the error as json
|
||||
let serde_error_str = if minify {
|
||||
serde_json::to_string(&error_output)
|
||||
} else {
|
||||
serde_json::to_string_pretty(&error_output)
|
||||
};
|
||||
|
||||
let error_json = match serde_error_str {
|
||||
Ok(json) => json,
|
||||
Err(e) => Err(ShellError::CantConvert {
|
||||
to_type: "string".to_string(),
|
||||
from_type: "error".to_string(),
|
||||
span: *block_span,
|
||||
help: Some(format!(
|
||||
"Error: {e}\nCan't convert {error_output:?} to string"
|
||||
)),
|
||||
})?,
|
||||
};
|
||||
|
||||
// Create a new output record, merging the block and error
|
||||
let output_record = Value::record(
|
||||
record! {
|
||||
"block" => Value::string(block_json, *block_span),
|
||||
"error" => Value::string(error_json, Span::test_data()),
|
||||
},
|
||||
pipeline.span,
|
||||
);
|
||||
Ok(output_record.into_pipeline_data())
|
||||
} else {
|
||||
let block_value = Value::string(
|
||||
if minify {
|
||||
format!("{block_output:?}")
|
||||
} else {
|
||||
format!("{block_output:#?}")
|
||||
},
|
||||
pipeline.span,
|
||||
);
|
||||
let error_value = Value::string(
|
||||
if minify {
|
||||
format!("{error_output:?}")
|
||||
} else {
|
||||
format!("{error_output:#?}")
|
||||
},
|
||||
pipeline.span,
|
||||
);
|
||||
let output_record = Value::record(
|
||||
record! {
|
||||
"block" => block_value,
|
||||
"error" => error_value
|
||||
},
|
||||
pipeline.span,
|
||||
);
|
||||
Ok(output_record.into_pipeline_data())
|
||||
}
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
@ -147,8 +62,247 @@ impl Command for Ast {
|
||||
example: "ast 'for x in 1..10 { echo $x ' --json --minify",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Print the ast of a string flattened",
|
||||
example: r#"ast "'hello'" --flatten"#,
|
||||
result: Some(Value::test_list(vec![Value::test_record(record! {
|
||||
"content" => Value::test_string("'hello'"),
|
||||
"shape" => Value::test_string("shape_string"),
|
||||
"span" => Value::test_record(record! {
|
||||
"start" => Value::test_int(0),
|
||||
"end" => Value::test_int(7),}),
|
||||
})])),
|
||||
},
|
||||
Example {
|
||||
description: "Print the ast of a string flattened, as json, minified",
|
||||
example: r#"ast "'hello'" --flatten --json --minify"#,
|
||||
result: Some(Value::test_string(
|
||||
r#"[{"content":"'hello'","shape":"shape_string","span":{"start":0,"end":7}}]"#,
|
||||
)),
|
||||
},
|
||||
Example {
|
||||
description: "Print the ast of a pipeline flattened",
|
||||
example: r#"ast 'ls | sort-by type name -i' --flatten"#,
|
||||
result: Some(Value::test_list(vec![
|
||||
Value::test_record(record! {
|
||||
"content" => Value::test_string("ls"),
|
||||
"shape" => Value::test_string("shape_external"),
|
||||
"span" => Value::test_record(record! {
|
||||
"start" => Value::test_int(0),
|
||||
"end" => Value::test_int(2),}),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"content" => Value::test_string("|"),
|
||||
"shape" => Value::test_string("shape_pipe"),
|
||||
"span" => Value::test_record(record! {
|
||||
"start" => Value::test_int(3),
|
||||
"end" => Value::test_int(4),}),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"content" => Value::test_string("sort-by"),
|
||||
"shape" => Value::test_string("shape_internalcall"),
|
||||
"span" => Value::test_record(record! {
|
||||
"start" => Value::test_int(5),
|
||||
"end" => Value::test_int(12),}),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"content" => Value::test_string("type"),
|
||||
"shape" => Value::test_string("shape_string"),
|
||||
"span" => Value::test_record(record! {
|
||||
"start" => Value::test_int(13),
|
||||
"end" => Value::test_int(17),}),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"content" => Value::test_string("name"),
|
||||
"shape" => Value::test_string("shape_string"),
|
||||
"span" => Value::test_record(record! {
|
||||
"start" => Value::test_int(18),
|
||||
"end" => Value::test_int(22),}),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"content" => Value::test_string("-i"),
|
||||
"shape" => Value::test_string("shape_flag"),
|
||||
"span" => Value::test_record(record! {
|
||||
"start" => Value::test_int(23),
|
||||
"end" => Value::test_int(25),}),
|
||||
}),
|
||||
])),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let pipeline: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||
let to_json = call.has_flag(engine_state, stack, "json")?;
|
||||
let minify = call.has_flag(engine_state, stack, "minify")?;
|
||||
let flatten = call.has_flag(engine_state, stack, "flatten")?;
|
||||
|
||||
let mut working_set = StateWorkingSet::new(engine_state);
|
||||
let offset = working_set.next_span_start();
|
||||
let parsed_block = parse(&mut working_set, None, pipeline.item.as_bytes(), false);
|
||||
|
||||
if flatten {
|
||||
let flat = flatten_block(&working_set, &parsed_block);
|
||||
if to_json {
|
||||
let mut json_val: JsonValue = json!([]);
|
||||
for (span, shape) in flat {
|
||||
let content =
|
||||
String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
|
||||
|
||||
let json = json!(
|
||||
{
|
||||
"content": content,
|
||||
"shape": shape.to_string(),
|
||||
"span": {
|
||||
"start": span.start.checked_sub(offset),
|
||||
"end": span.end.checked_sub(offset),
|
||||
},
|
||||
}
|
||||
);
|
||||
json_merge(&mut json_val, &json);
|
||||
}
|
||||
let json_string = if minify {
|
||||
if let Ok(json_str) = serde_json::to_string(&json_val) {
|
||||
json_str
|
||||
} else {
|
||||
"{}".to_string()
|
||||
}
|
||||
} else if let Ok(json_str) = serde_json::to_string_pretty(&json_val) {
|
||||
json_str
|
||||
} else {
|
||||
"{}".to_string()
|
||||
};
|
||||
|
||||
Ok(Value::string(json_string, pipeline.span).into_pipeline_data())
|
||||
} else {
|
||||
// let mut rec: Record = Record::new();
|
||||
let mut rec = vec![];
|
||||
for (span, shape) in flat {
|
||||
let content =
|
||||
String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
|
||||
let each_rec = record! {
|
||||
"content" => Value::test_string(content),
|
||||
"shape" => Value::test_string(shape.to_string()),
|
||||
"span" => Value::test_record(record!{
|
||||
"start" => Value::test_int(match span.start.checked_sub(offset) {
|
||||
Some(start) => start as i64,
|
||||
None => 0
|
||||
}),
|
||||
"end" => Value::test_int(match span.end.checked_sub(offset) {
|
||||
Some(end) => end as i64,
|
||||
None => 0
|
||||
}),
|
||||
}),
|
||||
};
|
||||
rec.push(Value::test_record(each_rec));
|
||||
}
|
||||
Ok(Value::list(rec, pipeline.span).into_pipeline_data())
|
||||
}
|
||||
} else {
|
||||
let error_output = working_set.parse_errors.first();
|
||||
let block_span = match &parsed_block.span {
|
||||
Some(span) => span,
|
||||
None => &pipeline.span,
|
||||
};
|
||||
if to_json {
|
||||
// Get the block as json
|
||||
let serde_block_str = if minify {
|
||||
serde_json::to_string(&*parsed_block)
|
||||
} else {
|
||||
serde_json::to_string_pretty(&*parsed_block)
|
||||
};
|
||||
let block_json = match serde_block_str {
|
||||
Ok(json) => json,
|
||||
Err(e) => Err(ShellError::CantConvert {
|
||||
to_type: "string".to_string(),
|
||||
from_type: "block".to_string(),
|
||||
span: *block_span,
|
||||
help: Some(format!(
|
||||
"Error: {e}\nCan't convert {parsed_block:?} to string"
|
||||
)),
|
||||
})?,
|
||||
};
|
||||
// Get the error as json
|
||||
let serde_error_str = if minify {
|
||||
serde_json::to_string(&error_output)
|
||||
} else {
|
||||
serde_json::to_string_pretty(&error_output)
|
||||
};
|
||||
|
||||
let error_json = match serde_error_str {
|
||||
Ok(json) => json,
|
||||
Err(e) => Err(ShellError::CantConvert {
|
||||
to_type: "string".to_string(),
|
||||
from_type: "error".to_string(),
|
||||
span: *block_span,
|
||||
help: Some(format!(
|
||||
"Error: {e}\nCan't convert {error_output:?} to string"
|
||||
)),
|
||||
})?,
|
||||
};
|
||||
|
||||
// Create a new output record, merging the block and error
|
||||
let output_record = Value::record(
|
||||
record! {
|
||||
"block" => Value::string(block_json, *block_span),
|
||||
"error" => Value::string(error_json, Span::test_data()),
|
||||
},
|
||||
pipeline.span,
|
||||
);
|
||||
Ok(output_record.into_pipeline_data())
|
||||
} else {
|
||||
let block_value = Value::string(
|
||||
if minify {
|
||||
format!("{parsed_block:?}")
|
||||
} else {
|
||||
format!("{parsed_block:#?}")
|
||||
},
|
||||
pipeline.span,
|
||||
);
|
||||
let error_value = Value::string(
|
||||
if minify {
|
||||
format!("{error_output:?}")
|
||||
} else {
|
||||
format!("{error_output:#?}")
|
||||
},
|
||||
pipeline.span,
|
||||
);
|
||||
let output_record = Value::record(
|
||||
record! {
|
||||
"block" => block_value,
|
||||
"error" => error_value
|
||||
},
|
||||
pipeline.span,
|
||||
);
|
||||
Ok(output_record.into_pipeline_data())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn json_merge(a: &mut JsonValue, b: &JsonValue) {
|
||||
match (a, b) {
|
||||
(JsonValue::Object(ref mut a), JsonValue::Object(b)) => {
|
||||
for (k, v) in b {
|
||||
json_merge(a.entry(k).or_insert(JsonValue::Null), v);
|
||||
}
|
||||
}
|
||||
(JsonValue::Array(ref mut a), JsonValue::Array(b)) => {
|
||||
a.extend(b.clone());
|
||||
}
|
||||
(JsonValue::Array(ref mut a), JsonValue::Object(b)) => {
|
||||
a.extend([JsonValue::Object(b.clone())]);
|
||||
}
|
||||
(a, b) => {
|
||||
*a = b.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
37
crates/nu-command/src/env/config/config_env.rs
vendored
37
crates/nu-command/src/env/config/config_env.rs
vendored
@ -15,7 +15,16 @@ impl Command for ConfigEnv {
|
||||
Signature::build(self.name())
|
||||
.category(Category::Env)
|
||||
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
||||
.switch("default", "Print default `env.nu` file instead.", Some('d'))
|
||||
.switch(
|
||||
"default",
|
||||
"Print the internal default `env.nu` file instead.",
|
||||
Some('d'),
|
||||
)
|
||||
.switch(
|
||||
"sample",
|
||||
"Print a commented, sample `env.nu` file instead.",
|
||||
Some('s'),
|
||||
)
|
||||
// TODO: Signature narrower than what run actually supports theoretically
|
||||
}
|
||||
|
||||
@ -26,18 +35,18 @@ impl Command for ConfigEnv {
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "allow user to open and update nu env",
|
||||
description: "open user's env.nu in the default editor",
|
||||
example: "config env",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "allow user to print default `env.nu` file",
|
||||
example: "config env --default,",
|
||||
description: "pretty-print a commented, sample `env.nu` that explains common settings",
|
||||
example: "config env --sample | nu-highlight,",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "allow saving the default `env.nu` locally",
|
||||
example: "config env --default | save -f ~/.config/nushell/default_env.nu",
|
||||
description: "pretty-print the internal `env.nu` file which is loaded before the user's environment",
|
||||
example: "config env --default | nu-highlight,",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
@ -50,12 +59,28 @@ impl Command for ConfigEnv {
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let default_flag = call.has_flag(engine_state, stack, "default")?;
|
||||
let sample_flag = call.has_flag(engine_state, stack, "sample")?;
|
||||
if default_flag && sample_flag {
|
||||
return Err(ShellError::IncompatibleParameters {
|
||||
left_message: "can't use `--default` at the same time".into(),
|
||||
left_span: call.get_flag_span(stack, "default").expect("has flag"),
|
||||
right_message: "because of `--sample`".into(),
|
||||
right_span: call.get_flag_span(stack, "sample").expect("has flag"),
|
||||
});
|
||||
}
|
||||
// `--default` flag handling
|
||||
if call.has_flag(engine_state, stack, "default")? {
|
||||
let head = call.head;
|
||||
return Ok(Value::string(nu_utils::get_default_env(), head).into_pipeline_data());
|
||||
}
|
||||
|
||||
// `--sample` flag handling
|
||||
if sample_flag {
|
||||
let head = call.head;
|
||||
return Ok(Value::string(nu_utils::get_sample_env(), head).into_pipeline_data());
|
||||
}
|
||||
|
||||
// Find the editor executable.
|
||||
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||
|
37
crates/nu-command/src/env/config/config_nu.rs
vendored
37
crates/nu-command/src/env/config/config_nu.rs
vendored
@ -17,9 +17,14 @@ impl Command for ConfigNu {
|
||||
.input_output_types(vec![(Type::Nothing, Type::Any)])
|
||||
.switch(
|
||||
"default",
|
||||
"Print default `config.nu` file instead.",
|
||||
"Print the internal default `config.nu` file instead.",
|
||||
Some('d'),
|
||||
)
|
||||
.switch(
|
||||
"sample",
|
||||
"Print a commented, sample `config.nu` file instead.",
|
||||
Some('s'),
|
||||
)
|
||||
// TODO: Signature narrower than what run actually supports theoretically
|
||||
}
|
||||
|
||||
@ -30,18 +35,19 @@ impl Command for ConfigNu {
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
description: "allow user to open and update nu config",
|
||||
description: "open user's config.nu in the default editor",
|
||||
example: "config nu",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "allow user to print default `config.nu` file",
|
||||
example: "config nu --default,",
|
||||
description: "pretty-print a commented, sample `config.nu` that explains common settings",
|
||||
example: "config nu --sample | nu-highlight",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "allow saving the default `config.nu` locally",
|
||||
example: "config nu --default | save -f ~/.config/nushell/default_config.nu",
|
||||
description:
|
||||
"pretty-print the internal `config.nu` file which is loaded before user's config",
|
||||
example: "config nu --default | nu-highlight",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
@ -54,12 +60,29 @@ impl Command for ConfigNu {
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let default_flag = call.has_flag(engine_state, stack, "default")?;
|
||||
let sample_flag = call.has_flag(engine_state, stack, "sample")?;
|
||||
if default_flag && sample_flag {
|
||||
return Err(ShellError::IncompatibleParameters {
|
||||
left_message: "can't use `--default` at the same time".into(),
|
||||
left_span: call.get_flag_span(stack, "default").expect("has flag"),
|
||||
right_message: "because of `--sample`".into(),
|
||||
right_span: call.get_flag_span(stack, "sample").expect("has flag"),
|
||||
});
|
||||
}
|
||||
|
||||
// `--default` flag handling
|
||||
if call.has_flag(engine_state, stack, "default")? {
|
||||
if default_flag {
|
||||
let head = call.head;
|
||||
return Ok(Value::string(nu_utils::get_default_config(), head).into_pipeline_data());
|
||||
}
|
||||
|
||||
// `--sample` flag handling
|
||||
if sample_flag {
|
||||
let head = call.head;
|
||||
return Ok(Value::string(nu_utils::get_sample_config(), head).into_pipeline_data());
|
||||
}
|
||||
|
||||
// Find the editor executable.
|
||||
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||
|
@ -194,7 +194,7 @@ impl Command for Watch {
|
||||
|
||||
match result {
|
||||
Ok(val) => {
|
||||
val.print(engine_state, stack, false, false)?;
|
||||
val.print_table(engine_state, stack, false, false)?;
|
||||
}
|
||||
Err(err) => {
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
|
@ -1,5 +1,6 @@
|
||||
use indexmap::IndexMap;
|
||||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::report_shell_warning;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SplitBy;
|
||||
@ -27,6 +28,15 @@ impl Command for SplitBy {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
report_shell_warning(
|
||||
engine_state,
|
||||
&ShellError::Deprecated {
|
||||
deprecated: "The `split_by` command",
|
||||
suggestion: "Please use the `group-by` command instead.",
|
||||
span: call.head,
|
||||
help: None,
|
||||
},
|
||||
);
|
||||
split_by(engine_state, stack, call, input)
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,10 @@ impl Command for FromCsv {
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("from csv")
|
||||
.input_output_types(vec![(Type::String, Type::table())])
|
||||
.input_output_types(vec![
|
||||
(Type::String, Type::table()),
|
||||
(Type::String, Type::list(Type::Any)),
|
||||
])
|
||||
.named(
|
||||
"separator",
|
||||
SyntaxShape::String,
|
||||
@ -82,6 +85,26 @@ impl Command for FromCsv {
|
||||
})],
|
||||
))
|
||||
},
|
||||
Example {
|
||||
description: "Convert comma-separated data to a table, allowing variable number of columns per row",
|
||||
example: "\"ColA,ColB\n1,2\n3,4,5\n6\" | from csv --flexible",
|
||||
result: Some(Value::test_list (
|
||||
vec![
|
||||
Value::test_record(record! {
|
||||
"ColA" => Value::test_int(1),
|
||||
"ColB" => Value::test_int(2),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"ColA" => Value::test_int(3),
|
||||
"ColB" => Value::test_int(4),
|
||||
"column2" => Value::test_int(5),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"ColA" => Value::test_int(6),
|
||||
}),
|
||||
],
|
||||
))
|
||||
},
|
||||
Example {
|
||||
description: "Convert comma-separated data to a table, ignoring headers",
|
||||
example: "open data.txt | from csv --noheaders",
|
||||
|
@ -39,12 +39,7 @@ fn from_delimited_stream(
|
||||
.from_reader(input_reader);
|
||||
|
||||
let headers = if noheaders {
|
||||
(0..reader
|
||||
.headers()
|
||||
.map_err(|err| from_csv_error(err, span))?
|
||||
.len())
|
||||
.map(|i| format!("column{i}"))
|
||||
.collect::<Vec<String>>()
|
||||
vec![]
|
||||
} else {
|
||||
reader
|
||||
.headers()
|
||||
@ -54,32 +49,28 @@ fn from_delimited_stream(
|
||||
.collect()
|
||||
};
|
||||
|
||||
let n = headers.len();
|
||||
let columns = headers
|
||||
.into_iter()
|
||||
.chain((n..).map(|i| format!("column{i}")));
|
||||
let iter = reader.into_records().map(move |row| {
|
||||
let row = match row {
|
||||
Ok(row) => row,
|
||||
Err(err) => return Value::error(from_csv_error(err, span), span),
|
||||
};
|
||||
let columns = headers.iter().cloned();
|
||||
let values = row
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
if no_infer {
|
||||
Value::string(s, span)
|
||||
} else if let Ok(i) = s.parse() {
|
||||
Value::int(i, span)
|
||||
} else if let Ok(f) = s.parse() {
|
||||
Value::float(f, span)
|
||||
} else {
|
||||
Value::string(s, span)
|
||||
}
|
||||
})
|
||||
.chain(std::iter::repeat(Value::nothing(span)));
|
||||
let columns = columns.clone();
|
||||
let values = row.into_iter().map(|s| {
|
||||
if no_infer {
|
||||
Value::string(s, span)
|
||||
} else if let Ok(i) = s.parse() {
|
||||
Value::int(i, span)
|
||||
} else if let Ok(f) = s.parse() {
|
||||
Value::float(f, span)
|
||||
} else {
|
||||
Value::string(s, span)
|
||||
}
|
||||
});
|
||||
|
||||
// If there are more values than the number of headers,
|
||||
// then the remaining values are ignored.
|
||||
//
|
||||
// Otherwise, if there are less values than headers,
|
||||
// then `Value::nothing(span)` is used to fill the remaining columns.
|
||||
Value::record(columns.zip(values).collect(), span)
|
||||
});
|
||||
|
||||
|
@ -11,7 +11,10 @@ impl Command for FromTsv {
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("from tsv")
|
||||
.input_output_types(vec![(Type::String, Type::table())])
|
||||
.input_output_types(vec![
|
||||
(Type::String, Type::table()),
|
||||
(Type::String, Type::list(Type::Any)),
|
||||
])
|
||||
.named(
|
||||
"comment",
|
||||
SyntaxShape::String,
|
||||
@ -76,6 +79,21 @@ impl Command for FromTsv {
|
||||
})],
|
||||
))
|
||||
},
|
||||
Example {
|
||||
description: "Convert comma-separated data to a table, allowing variable number of columns per row and ignoring headers",
|
||||
example: "\"value 1\nvalue 2\tdescription 2\" | from tsv --flexible --noheaders",
|
||||
result: Some(Value::test_list (
|
||||
vec![
|
||||
Value::test_record(record! {
|
||||
"column0" => Value::test_string("value 1"),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"column0" => Value::test_string("value 2"),
|
||||
"column1" => Value::test_string("description 2"),
|
||||
}),
|
||||
],
|
||||
))
|
||||
},
|
||||
Example {
|
||||
description: "Create a tsv file with header columns and open it",
|
||||
example: r#"$'c1(char tab)c2(char tab)c3(char nl)1(char tab)2(char tab)3' | save tsv-data | open tsv-data | from tsv"#,
|
||||
|
@ -4,7 +4,7 @@ use nu_engine::command_prelude::*;
|
||||
|
||||
use quick_xml::{
|
||||
escape,
|
||||
events::{BytesEnd, BytesStart, BytesText, Event},
|
||||
events::{BytesEnd, BytesPI, BytesStart, BytesText, Event},
|
||||
};
|
||||
use std::{borrow::Cow, io::Cursor};
|
||||
|
||||
@ -406,7 +406,7 @@ impl Job {
|
||||
let content_text = format!("{} {}", tag, content);
|
||||
// PI content must NOT be escaped
|
||||
// https://www.w3.org/TR/xml/#sec-pi
|
||||
let pi_content = BytesText::from_escaped(content_text.as_str());
|
||||
let pi_content = BytesPI::new(content_text.as_str());
|
||||
|
||||
self.writer
|
||||
.write_event(Event::PI(pi_content))
|
||||
|
@ -43,6 +43,12 @@ impl Command for Input {
|
||||
"number of characters to read; suppresses output",
|
||||
Some('n'),
|
||||
)
|
||||
.named(
|
||||
"default",
|
||||
SyntaxShape::String,
|
||||
"default value if no input is provided",
|
||||
Some('d'),
|
||||
)
|
||||
.switch("suppress-output", "don't print keystroke values", Some('s'))
|
||||
.category(Category::Platform)
|
||||
}
|
||||
@ -72,8 +78,12 @@ impl Command for Input {
|
||||
});
|
||||
}
|
||||
|
||||
let default_val: Option<String> = call.get_flag(engine_state, stack, "default")?;
|
||||
if let Some(prompt) = &prompt {
|
||||
print!("{prompt}");
|
||||
match &default_val {
|
||||
None => print!("{prompt}"),
|
||||
Some(val) => print!("{prompt} (default: {val})"),
|
||||
}
|
||||
let _ = std::io::stdout().flush();
|
||||
}
|
||||
|
||||
@ -149,7 +159,10 @@ impl Command for Input {
|
||||
if !suppress_output {
|
||||
std::io::stdout().write_all(b"\n")?;
|
||||
}
|
||||
Ok(Value::string(buf, call.head).into_pipeline_data())
|
||||
match default_val {
|
||||
Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()),
|
||||
_ => Ok(Value::string(buf, call.head).into_pipeline_data()),
|
||||
}
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
@ -164,6 +177,11 @@ impl Command for Input {
|
||||
example: "let user_input = (input --numchar 2)",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
description: "Get input from the user with default value, and assign to a variable",
|
||||
example: "let user_input = (input --default 10)",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -77,20 +77,6 @@ impl Command for SubCommand {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
if call.has_flag_const(working_set, "not")? {
|
||||
nu_protocol::report_shell_error(
|
||||
working_set.permanent(),
|
||||
&ShellError::GenericError {
|
||||
error: "Deprecated option".into(),
|
||||
msg: "`str contains --not {string}` is deprecated and will be removed in 0.95."
|
||||
.into(),
|
||||
span: Some(call.head),
|
||||
help: Some("Please use the `not` operator instead.".into()),
|
||||
inner: vec![],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let cell_paths: Vec<CellPath> = call.rest_const(working_set, 1)?;
|
||||
let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths);
|
||||
let args = Arguments {
|
||||
|
@ -5,6 +5,8 @@ use nu_protocol::{did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDe
|
||||
use nu_system::ForegroundChild;
|
||||
use nu_utils::IgnoreCaseExt;
|
||||
use pathdiff::diff_paths;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::{OsStr, OsString},
|
||||
@ -91,6 +93,22 @@ impl Command for External {
|
||||
false
|
||||
};
|
||||
|
||||
// let's make sure it's a .ps1 script, but only on Windows
|
||||
let potential_powershell_script = if cfg!(windows) {
|
||||
if let Some(executable) = which(&expanded_name, "", cwd.as_ref()) {
|
||||
let ext = executable
|
||||
.extension()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_uppercase();
|
||||
ext == "PS1"
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// Find the absolute path to the executable. On Windows, set the
|
||||
// executable to "cmd.exe" if it's a CMD internal command. If the
|
||||
// command is not found, display a helpful error message.
|
||||
@ -98,11 +116,16 @@ impl Command for External {
|
||||
&& (is_cmd_internal_command(&name_str) || potential_nuscript_in_windows)
|
||||
{
|
||||
PathBuf::from("cmd.exe")
|
||||
} else if cfg!(windows) && potential_powershell_script {
|
||||
// If we're on Windows and we're trying to run a PowerShell script, we'll use
|
||||
// `powershell.exe` to run it. We shouldn't have to check for powershell.exe because
|
||||
// it's automatically installed on all modern windows systems.
|
||||
PathBuf::from("powershell.exe")
|
||||
} else {
|
||||
// Determine the PATH to be used and then use `which` to find it - though this has no
|
||||
// effect if it's an absolute path already
|
||||
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||
let Some(executable) = which(expanded_name, &paths, cwd.as_ref()) else {
|
||||
let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) else {
|
||||
return Err(command_not_found(&name_str, call.head, engine_state, stack));
|
||||
};
|
||||
executable
|
||||
@ -123,15 +146,29 @@ impl Command for External {
|
||||
let args = eval_arguments_from_call(engine_state, stack, call)?;
|
||||
#[cfg(windows)]
|
||||
if is_cmd_internal_command(&name_str) || potential_nuscript_in_windows {
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
// The /D flag disables execution of AutoRun commands from registry.
|
||||
// The /C flag followed by a command name instructs CMD to execute
|
||||
// that command and quit.
|
||||
command.args(["/D", "/C", &name_str]);
|
||||
command.args(["/D", "/C", &expanded_name.to_string_lossy()]);
|
||||
for arg in &args {
|
||||
command.raw_arg(escape_cmd_argument(arg)?);
|
||||
}
|
||||
} else if potential_powershell_script {
|
||||
use nu_path::canonicalize_with;
|
||||
|
||||
// canonicalize the path to the script so that tests pass
|
||||
let canon_path = if let Ok(cwd) = engine_state.cwd_as_string(None) {
|
||||
canonicalize_with(&expanded_name, cwd)?
|
||||
} else {
|
||||
// If we can't get the current working directory, just provide the expanded name
|
||||
expanded_name
|
||||
};
|
||||
// The -Command flag followed by a script name instructs PowerShell to
|
||||
// execute that script and quit.
|
||||
command.args(["-Command", &canon_path.to_string_lossy()]);
|
||||
for arg in &args {
|
||||
command.raw_arg(arg.item.clone());
|
||||
}
|
||||
} else {
|
||||
command.args(args.into_iter().map(|s| s.item));
|
||||
}
|
||||
|
@ -44,8 +44,29 @@ fn net(span: Span) -> Value {
|
||||
let networks = Networks::new_with_refreshed_list()
|
||||
.iter()
|
||||
.map(|(iface, data)| {
|
||||
let ip_addresses = data
|
||||
.ip_networks()
|
||||
.iter()
|
||||
.map(|ip| {
|
||||
let protocol = match ip.addr {
|
||||
std::net::IpAddr::V4(_) => "ipv4",
|
||||
std::net::IpAddr::V6(_) => "ipv6",
|
||||
};
|
||||
Value::record(
|
||||
record! {
|
||||
"address" => Value::string(ip.addr.to_string(), span),
|
||||
"protocol" => Value::string(protocol, span),
|
||||
"loop" => Value::bool(ip.addr.is_loopback(), span),
|
||||
"multicast" => Value::bool(ip.addr.is_multicast(), span),
|
||||
},
|
||||
span,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let record = record! {
|
||||
"name" => Value::string(trim_cstyle_null(iface), span),
|
||||
"mac" => Value::string(data.mac_address().to_string(), span),
|
||||
"ip" => Value::list(ip_addresses, span),
|
||||
"sent" => Value::filesize(data.total_transmitted() as i64, span),
|
||||
"recv" => Value::filesize(data.total_received() as i64, span),
|
||||
};
|
||||
|
@ -1088,7 +1088,7 @@ fn create_empty_placeholder(
|
||||
let data = vec![vec![cell]];
|
||||
let mut table = NuTable::from(data);
|
||||
table.set_data_style(TextStyle::default().dimmed());
|
||||
let out = TableOutput::new(table, false, false, false);
|
||||
let out = TableOutput::new(table, false, false, 1);
|
||||
|
||||
let style_computer = &StyleComputer::from_config(engine_state, stack);
|
||||
let config = create_nu_table_config(&config, style_computer, &out, false, TableMode::default());
|
||||
|
@ -355,9 +355,9 @@ fn external_command_receives_raw_binary_data() {
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn can_run_batch_files() {
|
||||
fn can_run_cmd_files() {
|
||||
use nu_test_support::fs::Stub::FileWithContent;
|
||||
Playground::setup("run a Windows batch file", |dirs, sandbox| {
|
||||
Playground::setup("run a Windows cmd file", |dirs, sandbox| {
|
||||
sandbox.with_files(&[FileWithContent(
|
||||
"foo.cmd",
|
||||
r#"
|
||||
@ -371,12 +371,30 @@ fn can_run_batch_files() {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn can_run_batch_files() {
|
||||
use nu_test_support::fs::Stub::FileWithContent;
|
||||
Playground::setup("run a Windows batch file", |dirs, sandbox| {
|
||||
sandbox.with_files(&[FileWithContent(
|
||||
"foo.bat",
|
||||
r#"
|
||||
@echo off
|
||||
echo Hello World
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let actual = nu!(cwd: dirs.test(), pipeline("foo.bat"));
|
||||
assert!(actual.out.contains("Hello World"));
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn can_run_batch_files_without_cmd_extension() {
|
||||
use nu_test_support::fs::Stub::FileWithContent;
|
||||
Playground::setup(
|
||||
"run a Windows batch file without specifying the extension",
|
||||
"run a Windows cmd file without specifying the extension",
|
||||
|dirs, sandbox| {
|
||||
sandbox.with_files(&[FileWithContent(
|
||||
"foo.cmd",
|
||||
@ -440,3 +458,20 @@ fn redirect_combine() {
|
||||
assert_eq!(actual.out, "FooBar");
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn can_run_ps1_files() {
|
||||
use nu_test_support::fs::Stub::FileWithContent;
|
||||
Playground::setup("run_a_windows_ps_file", |dirs, sandbox| {
|
||||
sandbox.with_files(&[FileWithContent(
|
||||
"foo.ps1",
|
||||
r#"
|
||||
Write-Host Hello World
|
||||
"#,
|
||||
)]);
|
||||
|
||||
let actual = nu!(cwd: dirs.test(), pipeline("foo.ps1"));
|
||||
assert!(actual.out.contains("Hello World"));
|
||||
});
|
||||
}
|
||||
|
@ -2941,3 +2941,123 @@ fn table_footer_inheritance() {
|
||||
assert_eq!(actual.out.match_indices("x2").count(), 1);
|
||||
assert_eq!(actual.out.match_indices("x3").count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_footer_inheritance_kv_rows() {
|
||||
let actual = nu!(
|
||||
concat!(
|
||||
"$env.config.table.footer_inheritance = true;",
|
||||
"$env.config.footer_mode = 7;",
|
||||
"[[a b]; ['kv' {0: 0, 1: 1, 2: 2, 3: 3, 4: 4} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
actual.out,
|
||||
"╭───┬──────┬───────────╮\
|
||||
│ # │ a │ b │\
|
||||
├───┼──────┼───────────┤\
|
||||
│ 0 │ kv │ ╭───┬───╮ │\
|
||||
│ │ │ │ 0 │ 0 │ │\
|
||||
│ │ │ │ 1 │ 1 │ │\
|
||||
│ │ │ │ 2 │ 2 │ │\
|
||||
│ │ │ │ 3 │ 3 │ │\
|
||||
│ │ │ │ 4 │ 4 │ │\
|
||||
│ │ │ ╰───┴───╯ │\
|
||||
│ 1 │ data │ 0 │\
|
||||
│ 2 │ data │ 0 │\
|
||||
╰───┴──────┴───────────╯"
|
||||
);
|
||||
|
||||
let actual = nu!(
|
||||
concat!(
|
||||
"$env.config.table.footer_inheritance = true;",
|
||||
"$env.config.footer_mode = 7;",
|
||||
"[[a b]; ['kv' {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
actual.out,
|
||||
"╭───┬──────┬───────────╮\
|
||||
│ # │ a │ b │\
|
||||
├───┼──────┼───────────┤\
|
||||
│ 0 │ kv │ ╭───┬───╮ │\
|
||||
│ │ │ │ 0 │ 0 │ │\
|
||||
│ │ │ │ 1 │ 1 │ │\
|
||||
│ │ │ │ 2 │ 2 │ │\
|
||||
│ │ │ │ 3 │ 3 │ │\
|
||||
│ │ │ │ 4 │ 4 │ │\
|
||||
│ │ │ │ 5 │ 5 │ │\
|
||||
│ │ │ ╰───┴───╯ │\
|
||||
│ 1 │ data │ 0 │\
|
||||
│ 2 │ data │ 0 │\
|
||||
├───┼──────┼───────────┤\
|
||||
│ # │ a │ b │\
|
||||
╰───┴──────┴───────────╯"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_footer_inheritance_list_rows() {
|
||||
let actual = nu!(
|
||||
concat!(
|
||||
"$env.config.table.footer_inheritance = true;",
|
||||
"$env.config.footer_mode = 7;",
|
||||
"[[a b]; ['kv' {0: [[field]; [0] [1] [2] [3] [4]]} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
actual.out,
|
||||
"╭───┬──────┬───────────────────────╮\
|
||||
│ # │ a │ b │\
|
||||
├───┼──────┼───────────────────────┤\
|
||||
│ 0 │ kv │ ╭───┬───────────────╮ │\
|
||||
│ │ │ │ │ ╭───┬───────╮ │ │\
|
||||
│ │ │ │ 0 │ │ # │ field │ │ │\
|
||||
│ │ │ │ │ ├───┼───────┤ │ │\
|
||||
│ │ │ │ │ │ 0 │ 0 │ │ │\
|
||||
│ │ │ │ │ │ 1 │ 1 │ │ │\
|
||||
│ │ │ │ │ │ 2 │ 2 │ │ │\
|
||||
│ │ │ │ │ │ 3 │ 3 │ │ │\
|
||||
│ │ │ │ │ │ 4 │ 4 │ │ │\
|
||||
│ │ │ │ │ ╰───┴───────╯ │ │\
|
||||
│ │ │ ╰───┴───────────────╯ │\
|
||||
│ 1 │ data │ 0 │\
|
||||
│ 2 │ data │ 0 │\
|
||||
╰───┴──────┴───────────────────────╯"
|
||||
);
|
||||
|
||||
let actual = nu!(
|
||||
concat!(
|
||||
"$env.config.table.footer_inheritance = true;",
|
||||
"$env.config.footer_mode = 7;",
|
||||
"[[a b]; ['kv' {0: [[field]; [0] [1] [2] [3] [4] [5]]} ], ['data' 0], ['data' 0] ] | table --expand --width=80",
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
actual.out,
|
||||
"╭───┬──────┬───────────────────────╮\
|
||||
│ # │ a │ b │\
|
||||
├───┼──────┼───────────────────────┤\
|
||||
│ 0 │ kv │ ╭───┬───────────────╮ │\
|
||||
│ │ │ │ │ ╭───┬───────╮ │ │\
|
||||
│ │ │ │ 0 │ │ # │ field │ │ │\
|
||||
│ │ │ │ │ ├───┼───────┤ │ │\
|
||||
│ │ │ │ │ │ 0 │ 0 │ │ │\
|
||||
│ │ │ │ │ │ 1 │ 1 │ │ │\
|
||||
│ │ │ │ │ │ 2 │ 2 │ │ │\
|
||||
│ │ │ │ │ │ 3 │ 3 │ │ │\
|
||||
│ │ │ │ │ │ 4 │ 4 │ │ │\
|
||||
│ │ │ │ │ │ 5 │ 5 │ │ │\
|
||||
│ │ │ │ │ ╰───┴───────╯ │ │\
|
||||
│ │ │ ╰───┴───────────────╯ │\
|
||||
│ 1 │ data │ 0 │\
|
||||
│ 2 │ data │ 0 │\
|
||||
├───┼──────┼───────────────────────┤\
|
||||
│ # │ a │ b │\
|
||||
╰───┴──────┴───────────────────────╯"
|
||||
);
|
||||
}
|
||||
|
@ -469,7 +469,7 @@ fn from_csv_test_flexible_extra_vals() {
|
||||
echo "a,b\n1,2,3" | from csv --flexible | first | values | to nuon
|
||||
"#
|
||||
));
|
||||
assert_eq!(actual.out, "[1, 2]");
|
||||
assert_eq!(actual.out, "[1, 2, 3]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -479,5 +479,5 @@ fn from_csv_test_flexible_missing_vals() {
|
||||
echo "a,b\n1" | from csv --flexible | first | values | to nuon
|
||||
"#
|
||||
));
|
||||
assert_eq!(actual.out, "[1, null]");
|
||||
assert_eq!(actual.out, "[1]");
|
||||
}
|
||||
|
@ -29,3 +29,20 @@ fn from_ods_file_to_table_select_sheet() {
|
||||
|
||||
assert_eq!(actual.out, "SalesOrders");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ods_file_to_table_select_sheet_with_annotations() {
|
||||
let actual = nu!(
|
||||
cwd: "tests/fixtures/formats", pipeline(
|
||||
r#"
|
||||
open sample_data_with_annotation.ods --raw
|
||||
| from ods --sheets ["SalesOrders"]
|
||||
| get SalesOrders
|
||||
| get column4
|
||||
| get 0
|
||||
"#
|
||||
));
|
||||
|
||||
// The Units column in the sheet SalesOrders has an annotation and should be ignored.
|
||||
assert_eq!(actual.out, "Units");
|
||||
}
|
||||
|
Reference in New Issue
Block a user