Merge branch 'main' into make-assignment-consistent
This commit is contained in:
commit
5f87fc378e
45
Cargo.lock
generated
45
Cargo.lock
generated
|
@ -1219,24 +1219,24 @@ dependencies = [
|
|||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-next"
|
||||
version = "2.0.0"
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dirs-sys-next",
|
||||
"dirs-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-sys-next"
|
||||
version = "0.1.2"
|
||||
name = "dirs-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
||||
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"winapi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2873,7 +2873,7 @@ dependencies = [
|
|||
"assert_cmd",
|
||||
"crossterm",
|
||||
"ctrlc",
|
||||
"dirs-next",
|
||||
"dirs",
|
||||
"log",
|
||||
"miette",
|
||||
"mimalloc",
|
||||
|
@ -3047,7 +3047,7 @@ dependencies = [
|
|||
"deunicode",
|
||||
"dialoguer",
|
||||
"digest",
|
||||
"dirs-next",
|
||||
"dirs",
|
||||
"dtparse",
|
||||
"encoding_rs",
|
||||
"fancy-regex",
|
||||
|
@ -3245,7 +3245,7 @@ dependencies = [
|
|||
name = "nu-path"
|
||||
version = "0.95.1"
|
||||
dependencies = [
|
||||
"dirs-next",
|
||||
"dirs",
|
||||
"omnipath",
|
||||
"pwd",
|
||||
]
|
||||
|
@ -3344,6 +3344,8 @@ dependencies = [
|
|||
"chrono",
|
||||
"chrono-humanize",
|
||||
"convert_case",
|
||||
"dirs",
|
||||
"dirs-sys",
|
||||
"fancy-regex",
|
||||
"indexmap",
|
||||
"log",
|
||||
|
@ -3367,6 +3369,7 @@ dependencies = [
|
|||
"tempfile",
|
||||
"thiserror",
|
||||
"typetag",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3773,9 +3776,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
|||
|
||||
[[package]]
|
||||
name = "open"
|
||||
version = "5.2.0"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d2c909a3fce3bd80efef4cd1c6c056bd9376a8fe06fcfdbebaf32cb485a7e37"
|
||||
checksum = "61a877bf6abd716642a53ef1b89fb498923a4afca5c754f9050b4d081c05c4b3"
|
||||
dependencies = [
|
||||
"is-wsl",
|
||||
"libc",
|
||||
|
@ -3836,6 +3839,12 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "option-ext"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-multimap"
|
||||
version = "0.7.3"
|
||||
|
@ -5210,9 +5219,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rust-embed"
|
||||
version = "8.4.0"
|
||||
version = "8.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a"
|
||||
checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0"
|
||||
dependencies = [
|
||||
"rust-embed-impl",
|
||||
"rust-embed-utils",
|
||||
|
@ -6632,9 +6641,9 @@ checksum = "425a23c7b7145bc7620c9c445817c37b1f78b6790aee9f208133f3c028975b60"
|
|||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.9.1"
|
||||
version = "1.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439"
|
||||
checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
|
|
14
Cargo.toml
14
Cargo.toml
|
@ -83,7 +83,8 @@ ctrlc = "3.4"
|
|||
deunicode = "1.6.0"
|
||||
dialoguer = { default-features = false, version = "0.11" }
|
||||
digest = { default-features = false, version = "0.10" }
|
||||
dirs-next = "2.0"
|
||||
dirs = "5.0"
|
||||
dirs-sys = "0.4"
|
||||
dtparse = "2.0"
|
||||
encoding_rs = "0.8"
|
||||
fancy-regex = "0.13"
|
||||
|
@ -120,7 +121,7 @@ num-format = "0.4"
|
|||
num-traits = "0.2"
|
||||
omnipath = "0.1"
|
||||
once_cell = "1.18"
|
||||
open = "5.2"
|
||||
open = "5.3"
|
||||
os_pipe = { version = "1.2", features = ["io_safety"] }
|
||||
pathdiff = "0.2"
|
||||
percent-encoding = "2"
|
||||
|
@ -145,7 +146,7 @@ ropey = "1.6.1"
|
|||
roxmltree = "0.19"
|
||||
rstest = { version = "0.18", default-features = false }
|
||||
rusqlite = "0.31"
|
||||
rust-embed = "8.4.0"
|
||||
rust-embed = "8.5.0"
|
||||
same-file = "1.0"
|
||||
serde = { version = "1.0", default-features = false }
|
||||
serde_json = "1.0"
|
||||
|
@ -173,11 +174,12 @@ uu_mv = "0.0.27"
|
|||
uu_whoami = "0.0.27"
|
||||
uu_uname = "0.0.27"
|
||||
uucore = "0.0.27"
|
||||
uuid = "1.9.1"
|
||||
uuid = "1.10.0"
|
||||
v_htmlescape = "0.15.0"
|
||||
wax = "0.6"
|
||||
which = "6.0.0"
|
||||
windows = "0.54"
|
||||
windows-sys = "0.48"
|
||||
winreg = "0.52"
|
||||
|
||||
[dependencies]
|
||||
|
@ -201,7 +203,7 @@ reedline = { workspace = true, features = ["bashisms", "sqlite"] }
|
|||
|
||||
crossterm = { workspace = true }
|
||||
ctrlc = { workspace = true }
|
||||
dirs-next = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
log = { workspace = true }
|
||||
miette = { workspace = true, features = ["fancy-no-backtrace", "fancy"] }
|
||||
mimalloc = { version = "0.1.42", default-features = false, optional = true }
|
||||
|
@ -229,7 +231,7 @@ nu-test-support = { path = "./crates/nu-test-support", version = "0.95.1" }
|
|||
nu-plugin-protocol = { path = "./crates/nu-plugin-protocol", version = "0.95.1" }
|
||||
nu-plugin-core = { path = "./crates/nu-plugin-core", version = "0.95.1" }
|
||||
assert_cmd = "2.0"
|
||||
dirs-next = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
tango-bench = "0.5"
|
||||
pretty_assertions = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
|
|
|
@ -429,6 +429,14 @@ fn find_matching_block_end_in_expr(
|
|||
)
|
||||
}),
|
||||
|
||||
Expr::Collect(_, expr) => find_matching_block_end_in_expr(
|
||||
line,
|
||||
working_set,
|
||||
expr,
|
||||
global_span_offset,
|
||||
global_cursor_offset,
|
||||
),
|
||||
|
||||
Expr::Block(block_id)
|
||||
| Expr::Closure(block_id)
|
||||
| Expr::RowCondition(block_id)
|
||||
|
|
|
@ -833,7 +833,7 @@ fn variables_completions() {
|
|||
"plugin-path".into(),
|
||||
"startup-time".into(),
|
||||
"temp-path".into(),
|
||||
"vendor-autoload-dir".into(),
|
||||
"vendor-autoload-dirs".into(),
|
||||
];
|
||||
|
||||
// Match results
|
||||
|
|
|
@ -21,7 +21,9 @@ impl Command for Break {
|
|||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check:
|
||||
https://www.nushell.sh/book/thinking_in_nu.html"#
|
||||
https://www.nushell.sh/book/thinking_in_nu.html
|
||||
|
||||
break can only be used in while, loop, and for loops. It can not be used with each or other filter commands"#
|
||||
}
|
||||
|
||||
fn command_type(&self) -> CommandType {
|
||||
|
|
|
@ -21,7 +21,9 @@ impl Command for Continue {
|
|||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"This command is a parser keyword. For details, check:
|
||||
https://www.nushell.sh/book/thinking_in_nu.html"#
|
||||
https://www.nushell.sh/book/thinking_in_nu.html
|
||||
|
||||
continue can only be used in while, loop, and for loops. It can not be used with each or other filter commands"#
|
||||
}
|
||||
|
||||
fn command_type(&self) -> CommandType {
|
||||
|
|
|
@ -4,7 +4,7 @@ use nu_protocol::{
|
|||
ast::Block,
|
||||
debugger::WithoutDebug,
|
||||
engine::{StateDelta, StateWorkingSet},
|
||||
Range,
|
||||
report_error_new, Range,
|
||||
};
|
||||
use std::{
|
||||
sync::Arc,
|
||||
|
@ -124,7 +124,10 @@ pub fn eval_block(
|
|||
|
||||
nu_engine::eval_block::<WithoutDebug>(engine_state, &mut stack, &block, input)
|
||||
.and_then(|data| data.into_value(Span::test_data()))
|
||||
.unwrap_or_else(|err| panic!("test eval error in `{}`: {:?}", "TODO", err))
|
||||
.unwrap_or_else(|err| {
|
||||
report_error_new(engine_state, &err);
|
||||
panic!("test eval error in `{}`: {:?}", "TODO", err)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn check_example_evaluates_to_expected_output(
|
||||
|
|
|
@ -140,10 +140,10 @@ trash-support = ["trash"]
|
|||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.95.1" }
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.95.1" }
|
||||
|
||||
dirs-next = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
mockito = { workspace = true, default-features = false }
|
||||
quickcheck = { workspace = true }
|
||||
quickcheck_macros = { workspace = true }
|
||||
rstest = { workspace = true, default-features = false }
|
||||
pretty_assertions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
|
|
@ -42,32 +42,32 @@ impl Command for MetadataSet {
|
|||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
mut input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let ds_fp: Option<String> = call.get_flag(engine_state, stack, "datasource-filepath")?;
|
||||
let ds_ls = call.has_flag(engine_state, stack, "datasource-ls")?;
|
||||
let content_type: Option<String> = call.get_flag(engine_state, stack, "content-type")?;
|
||||
let signals = engine_state.signals().clone();
|
||||
let metadata = input
|
||||
.metadata()
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.with_content_type(content_type);
|
||||
|
||||
let mut metadata = match &mut input {
|
||||
PipelineData::Value(_, metadata)
|
||||
| PipelineData::ListStream(_, metadata)
|
||||
| PipelineData::ByteStream(_, metadata) => metadata.take().unwrap_or_default(),
|
||||
PipelineData::Empty => return Err(ShellError::PipelineEmpty { dst_span: head }),
|
||||
};
|
||||
|
||||
if let Some(content_type) = content_type {
|
||||
metadata.content_type = Some(content_type);
|
||||
}
|
||||
|
||||
match (ds_fp, ds_ls) {
|
||||
(Some(path), false) => Ok(input.into_pipeline_data_with_metadata(
|
||||
head,
|
||||
signals,
|
||||
metadata.with_data_source(DataSource::FilePath(path.into())),
|
||||
)),
|
||||
(None, true) => Ok(input.into_pipeline_data_with_metadata(
|
||||
head,
|
||||
signals,
|
||||
metadata.with_data_source(DataSource::Ls),
|
||||
)),
|
||||
_ => Ok(input.into_pipeline_data_with_metadata(head, signals, metadata)),
|
||||
(Some(path), false) => metadata.data_source = DataSource::FilePath(path.into()),
|
||||
(None, true) => metadata.data_source = DataSource::Ls,
|
||||
(Some(_), true) => (), // TODO: error here
|
||||
(None, false) => (),
|
||||
}
|
||||
|
||||
Ok(input.set_metadata(Some(metadata)))
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
|
@ -85,7 +85,9 @@ impl Command for MetadataSet {
|
|||
Example {
|
||||
description: "Set the metadata of a file path",
|
||||
example: "'crates' | metadata set --content-type text/plain | metadata",
|
||||
result: Some(Value::record(record!("content_type" => Value::string("text/plain", Span::test_data())), Span::test_data())),
|
||||
result: Some(Value::test_record(record! {
|
||||
"content_type" => Value::test_string("text/plain"),
|
||||
})),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState {
|
|||
All,
|
||||
Any,
|
||||
Append,
|
||||
Chunks,
|
||||
Columns,
|
||||
Compact,
|
||||
Default,
|
||||
|
|
|
@ -31,7 +31,7 @@ mod test_examples {
|
|||
check_example_evaluates_to_expected_output,
|
||||
check_example_input_and_output_types_match_command_signature,
|
||||
};
|
||||
use nu_cmd_lang::{Break, Echo, If, Let, Mut};
|
||||
use nu_cmd_lang::{Break, Describe, Echo, If, Let, Mut};
|
||||
use nu_protocol::{
|
||||
engine::{Command, EngineState, StateWorkingSet},
|
||||
Type,
|
||||
|
@ -81,6 +81,7 @@ mod test_examples {
|
|||
working_set.add_decl(Box::new(Break));
|
||||
working_set.add_decl(Box::new(Date));
|
||||
working_set.add_decl(Box::new(Default));
|
||||
working_set.add_decl(Box::new(Describe));
|
||||
working_set.add_decl(Box::new(Each));
|
||||
working_set.add_decl(Box::new(Echo));
|
||||
working_set.add_decl(Box::new(Enumerate));
|
||||
|
|
|
@ -61,6 +61,7 @@ impl Command for Watch {
|
|||
"Watch all directories under `<path>` recursively. Will be ignored if `<path>` is a file (default: true)",
|
||||
Some('r'),
|
||||
)
|
||||
.switch("quiet", "Hide the initial status message (default: false)", Some('q'))
|
||||
.switch("verbose", "Operate in verbose mode (default: false)", Some('v'))
|
||||
.category(Category::FileSystem)
|
||||
}
|
||||
|
@ -94,6 +95,8 @@ impl Command for Watch {
|
|||
|
||||
let verbose = call.has_flag(engine_state, stack, "verbose")?;
|
||||
|
||||
let quiet = call.has_flag(engine_state, stack, "quiet")?;
|
||||
|
||||
let debounce_duration_flag: Option<Spanned<i64>> =
|
||||
call.get_flag(engine_state, stack, "debounce-ms")?;
|
||||
let debounce_duration = match debounce_duration_flag {
|
||||
|
@ -161,7 +164,9 @@ impl Command for Watch {
|
|||
// need to cache to make sure that rename event works.
|
||||
debouncer.cache().add_root(&path, recursive_mode);
|
||||
|
||||
eprintln!("Now watching files at {path:?}. Press ctrl+c to abort.");
|
||||
if !quiet {
|
||||
eprintln!("Now watching files at {path:?}. Press ctrl+c to abort.");
|
||||
}
|
||||
|
||||
let mut closure = ClosureEval::new(engine_state, stack, closure);
|
||||
|
||||
|
|
161
crates/nu-command/src/filters/chunks.rs
Normal file
161
crates/nu-command/src/filters/chunks.rs
Normal file
|
@ -0,0 +1,161 @@
|
|||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::ListStream;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Chunks;
|
||||
|
||||
impl Command for Chunks {
|
||||
fn name(&self) -> &str {
|
||||
"chunks"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("chunks")
|
||||
.input_output_types(vec![
|
||||
(Type::table(), Type::list(Type::table())),
|
||||
(Type::list(Type::Any), Type::list(Type::list(Type::Any))),
|
||||
])
|
||||
.required("chunk_size", SyntaxShape::Int, "The size of each chunk.")
|
||||
.category(Category::Filters)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Divide a list or table into chunks of `chunk_size`."
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
"This command will error if `chunk_size` is negative or zero."
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["batch", "group"]
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
example: "[1 2 3 4] | chunks 2",
|
||||
description: "Chunk a list into pairs",
|
||||
result: Some(Value::test_list(vec![
|
||||
Value::test_list(vec![Value::test_int(1), Value::test_int(2)]),
|
||||
Value::test_list(vec![Value::test_int(3), Value::test_int(4)]),
|
||||
])),
|
||||
},
|
||||
Example {
|
||||
example: "[[foo bar]; [0 1] [2 3] [4 5] [6 7] [8 9]] | chunks 3",
|
||||
description: "Chunk the rows of a table into triplets",
|
||||
result: Some(Value::test_list(vec![
|
||||
Value::test_list(vec![
|
||||
Value::test_record(record! {
|
||||
"foo" => Value::test_int(0),
|
||||
"bar" => Value::test_int(1),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"foo" => Value::test_int(2),
|
||||
"bar" => Value::test_int(3),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"foo" => Value::test_int(4),
|
||||
"bar" => Value::test_int(5),
|
||||
}),
|
||||
]),
|
||||
Value::test_list(vec![
|
||||
Value::test_record(record! {
|
||||
"foo" => Value::test_int(6),
|
||||
"bar" => Value::test_int(7),
|
||||
}),
|
||||
Value::test_record(record! {
|
||||
"foo" => Value::test_int(8),
|
||||
"bar" => Value::test_int(9),
|
||||
}),
|
||||
]),
|
||||
])),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let chunk_size: Value = call.req(engine_state, stack, 0)?;
|
||||
|
||||
let size =
|
||||
usize::try_from(chunk_size.as_int()?).map_err(|_| ShellError::NeedsPositiveValue {
|
||||
span: chunk_size.span(),
|
||||
})?;
|
||||
|
||||
let size = NonZeroUsize::try_from(size).map_err(|_| ShellError::IncorrectValue {
|
||||
msg: "`chunk_size` cannot be zero".into(),
|
||||
val_span: chunk_size.span(),
|
||||
call_span: head,
|
||||
})?;
|
||||
|
||||
chunks(engine_state, input, size, head)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chunks(
|
||||
engine_state: &EngineState,
|
||||
input: PipelineData,
|
||||
chunk_size: NonZeroUsize,
|
||||
span: Span,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
match input {
|
||||
PipelineData::Value(Value::List { vals, .. }, metadata) => {
|
||||
let chunks = ChunksIter::new(vals, chunk_size, span);
|
||||
let stream = ListStream::new(chunks, span, engine_state.signals().clone());
|
||||
Ok(PipelineData::ListStream(stream, metadata))
|
||||
}
|
||||
PipelineData::ListStream(stream, metadata) => {
|
||||
let stream = stream.modify(|iter| ChunksIter::new(iter, chunk_size, span));
|
||||
Ok(PipelineData::ListStream(stream, metadata))
|
||||
}
|
||||
input => Err(input.unsupported_input_error("list", span)),
|
||||
}
|
||||
}
|
||||
|
||||
struct ChunksIter<I: Iterator<Item = Value>> {
|
||||
iter: I,
|
||||
size: usize,
|
||||
span: Span,
|
||||
}
|
||||
|
||||
impl<I: Iterator<Item = Value>> ChunksIter<I> {
|
||||
fn new(iter: impl IntoIterator<IntoIter = I>, size: NonZeroUsize, span: Span) -> Self {
|
||||
Self {
|
||||
iter: iter.into_iter(),
|
||||
size: size.into(),
|
||||
span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Iterator<Item = Value>> Iterator for ChunksIter<I> {
|
||||
type Item = Value;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let first = self.iter.next()?;
|
||||
let mut chunk = Vec::with_capacity(self.size); // delay allocation to optimize for empty iter
|
||||
chunk.push(first);
|
||||
chunk.extend((&mut self.iter).take(self.size - 1));
|
||||
Some(Value::list(chunk, self.span))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_examples() {
|
||||
use crate::test_examples;
|
||||
|
||||
test_examples(Chunks {})
|
||||
}
|
||||
}
|
|
@ -51,11 +51,11 @@ impl Command for Default {
|
|||
description:
|
||||
"Get the env value of `MY_ENV` with a default value 'abc' if not present",
|
||||
example: "$env | get --ignore-errors MY_ENV | default 'abc'",
|
||||
result: None, // Some(Value::test_string("abc")),
|
||||
result: Some(Value::test_string("abc")),
|
||||
},
|
||||
Example {
|
||||
description: "Replace the `null` value in a list",
|
||||
example: "[1, 2, null, 4] | default 3",
|
||||
example: "[1, 2, null, 4] | each { default 3 }",
|
||||
result: Some(Value::list(
|
||||
vec![
|
||||
Value::test_int(1),
|
||||
|
@ -113,15 +113,7 @@ fn default(
|
|||
} else if input.is_nothing() {
|
||||
Ok(value.into_pipeline_data())
|
||||
} else {
|
||||
input
|
||||
.map(
|
||||
move |item| match item {
|
||||
Value::Nothing { .. } => value.clone(),
|
||||
x => x,
|
||||
},
|
||||
engine_state.signals(),
|
||||
)
|
||||
.map(|x| x.set_metadata(metadata))
|
||||
Ok(input)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -132,8 +132,6 @@ with 'transpose' first."#
|
|||
Ok(data) => Some(data.into_value(head).unwrap_or_else(|err| {
|
||||
Value::error(chain_error_with_input(err, is_error, span), span)
|
||||
})),
|
||||
Err(ShellError::Continue { span }) => Some(Value::nothing(span)),
|
||||
Err(ShellError::Break { .. }) => None,
|
||||
Err(error) => {
|
||||
let error = chain_error_with_input(error, is_error, span);
|
||||
Some(Value::error(error, span))
|
||||
|
@ -149,10 +147,6 @@ with 'transpose' first."#
|
|||
.map_while(move |value| {
|
||||
let value = match value {
|
||||
Ok(value) => value,
|
||||
Err(ShellError::Continue { span }) => {
|
||||
return Some(Value::nothing(span))
|
||||
}
|
||||
Err(ShellError::Break { .. }) => return None,
|
||||
Err(err) => return Some(Value::error(err, head)),
|
||||
};
|
||||
|
||||
|
@ -163,8 +157,6 @@ with 'transpose' first."#
|
|||
.and_then(|data| data.into_value(head))
|
||||
{
|
||||
Ok(value) => Some(value),
|
||||
Err(ShellError::Continue { span }) => Some(Value::nothing(span)),
|
||||
Err(ShellError::Break { .. }) => None,
|
||||
Err(error) => {
|
||||
let error = chain_error_with_input(error, is_error, span);
|
||||
Some(Value::error(error, span))
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::ValueIterator;
|
||||
use nu_protocol::{report_warning_new, ParseWarning, ValueIterator};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Group;
|
||||
|
@ -54,6 +54,17 @@ impl Command for Group {
|
|||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
|
||||
report_warning_new(
|
||||
engine_state,
|
||||
&ParseWarning::DeprecatedWarning {
|
||||
old_command: "group".into(),
|
||||
new_suggestion: "the new `chunks` command".into(),
|
||||
span: head,
|
||||
url: "`help chunks`".into(),
|
||||
},
|
||||
);
|
||||
|
||||
let group_size: Spanned<usize> = call.req(engine_state, stack, 0)?;
|
||||
let metadata = input.metadata();
|
||||
|
||||
|
|
|
@ -60,7 +60,6 @@ impl Command for Items {
|
|||
|
||||
match result {
|
||||
Ok(value) => Some(value),
|
||||
Err(ShellError::Break { .. }) => None,
|
||||
Err(err) => {
|
||||
let err = chain_error_with_input(err, false, span);
|
||||
Some(Value::error(err, head))
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
mod all;
|
||||
mod any;
|
||||
mod append;
|
||||
mod chunks;
|
||||
mod columns;
|
||||
mod compact;
|
||||
mod default;
|
||||
|
@ -58,6 +59,7 @@ mod zip;
|
|||
pub use all::All;
|
||||
pub use any::Any;
|
||||
pub use append::Append;
|
||||
pub use chunks::Chunks;
|
||||
pub use columns::Columns;
|
||||
pub use compact::Compact;
|
||||
pub use default::Default;
|
||||
|
|
|
@ -36,7 +36,7 @@ impl Command for Reduce {
|
|||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Aggregate a list to a single value using an accumulator closure."
|
||||
"Aggregate a list (starting from the left) to a single value using an accumulator closure."
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
|
@ -50,6 +50,11 @@ impl Command for Reduce {
|
|||
description: "Sum values of a list (same as 'math sum')",
|
||||
result: Some(Value::test_int(10)),
|
||||
},
|
||||
Example {
|
||||
example: "[ 1 2 3 4 ] | reduce {|it, acc| $acc - $it }",
|
||||
description: r#"`reduce` accumulates value from left to right, equivalent to (((1 - 2) - 3) - 4)."#,
|
||||
result: Some(Value::test_int(-8)),
|
||||
},
|
||||
Example {
|
||||
example:
|
||||
"[ 8 7 6 ] | enumerate | reduce --fold 0 {|it, acc| $acc + $it.item + $it.index }",
|
||||
|
@ -61,6 +66,11 @@ impl Command for Reduce {
|
|||
description: "Sum values with a starting value (fold)",
|
||||
result: Some(Value::test_int(20)),
|
||||
},
|
||||
Example {
|
||||
example: r#"[[foo baz] [baz quux]] | reduce --fold "foobar" {|it, acc| $acc | str replace $it.0 $it.1}"#,
|
||||
description: "Iteratively perform string replace (from left to right): 'foobar' -> 'bazbar' -> 'quuxbar'",
|
||||
result: Some(Value::test_string("quuxbar")),
|
||||
},
|
||||
Example {
|
||||
example: r#"[ i o t ] | reduce --fold "Arthur, King of the Britons" {|it, acc| $acc | str replace --all $it "X" }"#,
|
||||
description: "Replace selected characters in a string with 'X'",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use nu_engine::command_prelude::*;
|
||||
use nu_protocol::ValueIterator;
|
||||
use nu_protocol::ListStream;
|
||||
use std::num::NonZeroUsize;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Window;
|
||||
|
@ -12,8 +13,8 @@ impl Command for Window {
|
|||
fn signature(&self) -> Signature {
|
||||
Signature::build("window")
|
||||
.input_output_types(vec![(
|
||||
Type::List(Box::new(Type::Any)),
|
||||
Type::List(Box::new(Type::List(Box::new(Type::Any)))),
|
||||
Type::list(Type::Any),
|
||||
Type::list(Type::list(Type::Any)),
|
||||
)])
|
||||
.required("window_size", SyntaxShape::Int, "The size of each window.")
|
||||
.named(
|
||||
|
@ -34,72 +35,41 @@ impl Command for Window {
|
|||
"Creates a sliding window of `window_size` that slide by n rows/elements across input."
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
"This command will error if `window_size` or `stride` are negative or zero."
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
let stream_test_1 = vec![
|
||||
Value::list(
|
||||
vec![Value::test_int(1), Value::test_int(2)],
|
||||
Span::test_data(),
|
||||
),
|
||||
Value::list(
|
||||
vec![Value::test_int(2), Value::test_int(3)],
|
||||
Span::test_data(),
|
||||
),
|
||||
Value::list(
|
||||
vec![Value::test_int(3), Value::test_int(4)],
|
||||
Span::test_data(),
|
||||
),
|
||||
];
|
||||
|
||||
let stream_test_2 = vec![
|
||||
Value::list(
|
||||
vec![Value::test_int(1), Value::test_int(2)],
|
||||
Span::test_data(),
|
||||
),
|
||||
Value::list(
|
||||
vec![Value::test_int(4), Value::test_int(5)],
|
||||
Span::test_data(),
|
||||
),
|
||||
Value::list(
|
||||
vec![Value::test_int(7), Value::test_int(8)],
|
||||
Span::test_data(),
|
||||
),
|
||||
];
|
||||
|
||||
let stream_test_3 = vec![
|
||||
Value::list(
|
||||
vec![Value::test_int(1), Value::test_int(2), Value::test_int(3)],
|
||||
Span::test_data(),
|
||||
),
|
||||
Value::list(
|
||||
vec![Value::test_int(4), Value::test_int(5)],
|
||||
Span::test_data(),
|
||||
),
|
||||
];
|
||||
|
||||
vec![
|
||||
Example {
|
||||
example: "[1 2 3 4] | window 2",
|
||||
description: "A sliding window of two elements",
|
||||
result: Some(Value::list(
|
||||
stream_test_1,
|
||||
Span::test_data(),
|
||||
)),
|
||||
result: Some(Value::test_list(vec![
|
||||
Value::test_list(vec![Value::test_int(1), Value::test_int(2)]),
|
||||
Value::test_list(vec![Value::test_int(2), Value::test_int(3)]),
|
||||
Value::test_list(vec![Value::test_int(3), Value::test_int(4)]),
|
||||
])),
|
||||
},
|
||||
Example {
|
||||
example: "[1, 2, 3, 4, 5, 6, 7, 8] | window 2 --stride 3",
|
||||
description: "A sliding window of two elements, with a stride of 3",
|
||||
result: Some(Value::list(
|
||||
stream_test_2,
|
||||
Span::test_data(),
|
||||
)),
|
||||
result: Some(Value::test_list(vec![
|
||||
Value::test_list(vec![Value::test_int(1), Value::test_int(2)]),
|
||||
Value::test_list(vec![Value::test_int(4), Value::test_int(5)]),
|
||||
Value::test_list(vec![Value::test_int(7), Value::test_int(8)]),
|
||||
])),
|
||||
},
|
||||
Example {
|
||||
example: "[1, 2, 3, 4, 5] | window 3 --stride 3 --remainder",
|
||||
description: "A sliding window of equal stride that includes remainder. Equivalent to chunking",
|
||||
result: Some(Value::list(
|
||||
stream_test_3,
|
||||
Span::test_data(),
|
||||
)),
|
||||
result: Some(Value::test_list(vec![
|
||||
Value::test_list(vec![
|
||||
Value::test_int(1),
|
||||
Value::test_int(2),
|
||||
Value::test_int(3),
|
||||
]),
|
||||
Value::test_list(vec![Value::test_int(4), Value::test_int(5)]),
|
||||
])),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -112,116 +82,169 @@ impl Command for Window {
|
|||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let group_size: Spanned<usize> = call.req(engine_state, stack, 0)?;
|
||||
let metadata = input.metadata();
|
||||
let stride: Option<usize> = call.get_flag(engine_state, stack, "stride")?;
|
||||
let window_size: Value = call.req(engine_state, stack, 0)?;
|
||||
let stride: Option<Value> = call.get_flag(engine_state, stack, "stride")?;
|
||||
let remainder = call.has_flag(engine_state, stack, "remainder")?;
|
||||
|
||||
let stride = stride.unwrap_or(1);
|
||||
let size =
|
||||
usize::try_from(window_size.as_int()?).map_err(|_| ShellError::NeedsPositiveValue {
|
||||
span: window_size.span(),
|
||||
})?;
|
||||
|
||||
//FIXME: add in support for external redirection when engine-q supports it generally
|
||||
let size = NonZeroUsize::try_from(size).map_err(|_| ShellError::IncorrectValue {
|
||||
msg: "`window_size` cannot be zero".into(),
|
||||
val_span: window_size.span(),
|
||||
call_span: head,
|
||||
})?;
|
||||
|
||||
let each_group_iterator = EachWindowIterator {
|
||||
group_size: group_size.item,
|
||||
input: Box::new(input.into_iter()),
|
||||
span: head,
|
||||
previous: None,
|
||||
stride,
|
||||
remainder,
|
||||
let stride = if let Some(stride_val) = stride {
|
||||
let stride = usize::try_from(stride_val.as_int()?).map_err(|_| {
|
||||
ShellError::NeedsPositiveValue {
|
||||
span: stride_val.span(),
|
||||
}
|
||||
})?;
|
||||
|
||||
NonZeroUsize::try_from(stride).map_err(|_| ShellError::IncorrectValue {
|
||||
msg: "`stride` cannot be zero".into(),
|
||||
val_span: stride_val.span(),
|
||||
call_span: head,
|
||||
})?
|
||||
} else {
|
||||
NonZeroUsize::MIN
|
||||
};
|
||||
|
||||
Ok(each_group_iterator.into_pipeline_data_with_metadata(
|
||||
head,
|
||||
engine_state.signals().clone(),
|
||||
metadata,
|
||||
))
|
||||
if remainder && size == stride {
|
||||
super::chunks::chunks(engine_state, input, size, head)
|
||||
} else if stride >= size {
|
||||
match input {
|
||||
PipelineData::Value(Value::List { vals, .. }, metadata) => {
|
||||
let chunks = WindowGapIter::new(vals, size, stride, remainder, head);
|
||||
let stream = ListStream::new(chunks, head, engine_state.signals().clone());
|
||||
Ok(PipelineData::ListStream(stream, metadata))
|
||||
}
|
||||
PipelineData::ListStream(stream, metadata) => {
|
||||
let stream = stream
|
||||
.modify(|iter| WindowGapIter::new(iter, size, stride, remainder, head));
|
||||
Ok(PipelineData::ListStream(stream, metadata))
|
||||
}
|
||||
input => Err(input.unsupported_input_error("list", head)),
|
||||
}
|
||||
} else {
|
||||
match input {
|
||||
PipelineData::Value(Value::List { vals, .. }, metadata) => {
|
||||
let chunks = WindowOverlapIter::new(vals, size, stride, remainder, head);
|
||||
let stream = ListStream::new(chunks, head, engine_state.signals().clone());
|
||||
Ok(PipelineData::ListStream(stream, metadata))
|
||||
}
|
||||
PipelineData::ListStream(stream, metadata) => {
|
||||
let stream = stream
|
||||
.modify(|iter| WindowOverlapIter::new(iter, size, stride, remainder, head));
|
||||
Ok(PipelineData::ListStream(stream, metadata))
|
||||
}
|
||||
input => Err(input.unsupported_input_error("list", head)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EachWindowIterator {
|
||||
group_size: usize,
|
||||
input: ValueIterator,
|
||||
span: Span,
|
||||
previous: Option<Vec<Value>>,
|
||||
struct WindowOverlapIter<I: Iterator<Item = Value>> {
|
||||
iter: I,
|
||||
window: Vec<Value>,
|
||||
stride: usize,
|
||||
remainder: bool,
|
||||
span: Span,
|
||||
}
|
||||
|
||||
impl Iterator for EachWindowIterator {
|
||||
impl<I: Iterator<Item = Value>> WindowOverlapIter<I> {
|
||||
fn new(
|
||||
iter: impl IntoIterator<IntoIter = I>,
|
||||
size: NonZeroUsize,
|
||||
stride: NonZeroUsize,
|
||||
remainder: bool,
|
||||
span: Span,
|
||||
) -> Self {
|
||||
Self {
|
||||
iter: iter.into_iter(),
|
||||
window: Vec::with_capacity(size.into()),
|
||||
stride: stride.into(),
|
||||
remainder,
|
||||
span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Iterator<Item = Value>> Iterator for WindowOverlapIter<I> {
|
||||
type Item = Value;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut group = self.previous.take().unwrap_or_else(|| {
|
||||
let mut vec = Vec::new();
|
||||
|
||||
// We default to a Vec of capacity size + stride as striding pushes n extra elements to the end
|
||||
vec.try_reserve(self.group_size + self.stride).ok();
|
||||
|
||||
vec
|
||||
});
|
||||
let mut current_count = 0;
|
||||
|
||||
if group.is_empty() {
|
||||
loop {
|
||||
let item = self.input.next();
|
||||
|
||||
match item {
|
||||
Some(v) => {
|
||||
group.push(v);
|
||||
|
||||
current_count += 1;
|
||||
if current_count >= self.group_size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if self.remainder {
|
||||
break;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let len = if self.window.is_empty() {
|
||||
self.window.capacity()
|
||||
} else {
|
||||
// our historic buffer is already full, so stride instead
|
||||
self.stride
|
||||
};
|
||||
|
||||
loop {
|
||||
let item = self.input.next();
|
||||
self.window.extend((&mut self.iter).take(len));
|
||||
|
||||
match item {
|
||||
Some(v) => {
|
||||
group.push(v);
|
||||
|
||||
current_count += 1;
|
||||
if current_count >= self.stride {
|
||||
break;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if self.remainder {
|
||||
break;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We now have elements + stride in our group, and need to
|
||||
// drop the skipped elements. Drain to preserve allocation and capacity
|
||||
// Dropping this iterator consumes it.
|
||||
group.drain(..self.stride.min(group.len()));
|
||||
if self.window.len() == self.window.capacity()
|
||||
|| (self.remainder && !self.window.is_empty())
|
||||
{
|
||||
let mut next = Vec::with_capacity(self.window.len());
|
||||
next.extend(self.window.iter().skip(self.stride).cloned());
|
||||
let window = std::mem::replace(&mut self.window, next);
|
||||
Some(Value::list(window, self.span))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if group.is_empty() {
|
||||
return None;
|
||||
struct WindowGapIter<I: Iterator<Item = Value>> {
|
||||
iter: I,
|
||||
size: usize,
|
||||
skip: usize,
|
||||
first: bool,
|
||||
remainder: bool,
|
||||
span: Span,
|
||||
}
|
||||
|
||||
impl<I: Iterator<Item = Value>> WindowGapIter<I> {
|
||||
fn new(
|
||||
iter: impl IntoIterator<IntoIter = I>,
|
||||
size: NonZeroUsize,
|
||||
stride: NonZeroUsize,
|
||||
remainder: bool,
|
||||
span: Span,
|
||||
) -> Self {
|
||||
let size = size.into();
|
||||
Self {
|
||||
iter: iter.into_iter(),
|
||||
size,
|
||||
skip: stride.get() - size,
|
||||
first: true,
|
||||
remainder,
|
||||
span,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let return_group = group.clone();
|
||||
self.previous = Some(group);
|
||||
impl<I: Iterator<Item = Value>> Iterator for WindowGapIter<I> {
|
||||
type Item = Value;
|
||||
|
||||
Some(Value::list(return_group, self.span))
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let mut window = Vec::with_capacity(self.size);
|
||||
window.extend(
|
||||
(&mut self.iter)
|
||||
.skip(if self.first { 0 } else { self.skip })
|
||||
.take(self.size),
|
||||
);
|
||||
|
||||
self.first = false;
|
||||
|
||||
if window.len() == self.size || (self.remainder && !window.is_empty()) {
|
||||
Some(Value::list(window, self.span))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,12 +12,12 @@ impl Command for Generate {
|
|||
fn signature(&self) -> Signature {
|
||||
Signature::build("generate")
|
||||
.input_output_types(vec![(Type::Nothing, Type::List(Box::new(Type::Any)))])
|
||||
.required("initial", SyntaxShape::Any, "Initial value.")
|
||||
.required(
|
||||
"closure",
|
||||
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
|
||||
"Generator function.",
|
||||
)
|
||||
.optional("initial", SyntaxShape::Any, "Initial value.")
|
||||
.allow_variants_without_examples(true)
|
||||
.category(Category::Generators)
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ used as the next argument to the closure, otherwise generation stops.
|
|||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
example: "generate 0 {|i| if $i <= 10 { {out: $i, next: ($i + 2)} }}",
|
||||
example: "generate {|i| if $i <= 10 { {out: $i, next: ($i + 2)} }} 0",
|
||||
description: "Generate a sequence of numbers",
|
||||
result: Some(Value::list(
|
||||
vec![
|
||||
|
@ -57,10 +57,17 @@ used as the next argument to the closure, otherwise generation stops.
|
|||
},
|
||||
Example {
|
||||
example:
|
||||
"generate [0, 1] {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} }",
|
||||
"generate {|fib| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} } [0, 1]",
|
||||
description: "Generate a continuous stream of Fibonacci numbers",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
example:
|
||||
"generate {|fib=[0, 1]| {out: $fib.0, next: [$fib.1, ($fib.0 + $fib.1)]} }",
|
||||
description:
|
||||
"Generate a continuous stream of Fibonacci numbers, using default parameters",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -72,15 +79,15 @@ used as the next argument to the closure, otherwise generation stops.
|
|||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
let initial: Value = call.req(engine_state, stack, 0)?;
|
||||
let closure: Closure = call.req(engine_state, stack, 1)?;
|
||||
|
||||
let closure: Closure = call.req(engine_state, stack, 0)?;
|
||||
let initial: Option<Value> = call.opt(engine_state, stack, 1)?;
|
||||
let block = engine_state.get_block(closure.block_id);
|
||||
let mut closure = ClosureEval::new(engine_state, stack, closure);
|
||||
|
||||
// A type of Option<S> is used to represent state. Invocation
|
||||
// will stop on None. Using Option<S> allows functions to output
|
||||
// one final value before stopping.
|
||||
let mut state = Some(initial);
|
||||
let mut state = Some(get_initial_state(initial, &block.signature, call.head)?);
|
||||
let iter = std::iter::from_fn(move || {
|
||||
let arg = state.take()?;
|
||||
|
||||
|
@ -170,6 +177,38 @@ used as the next argument to the closure, otherwise generation stops.
|
|||
}
|
||||
}
|
||||
|
||||
fn get_initial_state(
|
||||
initial: Option<Value>,
|
||||
signature: &Signature,
|
||||
span: Span,
|
||||
) -> Result<Value, ShellError> {
|
||||
match initial {
|
||||
Some(v) => Ok(v),
|
||||
None => {
|
||||
// the initial state should be referred from signature
|
||||
if !signature.optional_positional.is_empty()
|
||||
&& signature.optional_positional[0].default_value.is_some()
|
||||
{
|
||||
Ok(signature.optional_positional[0]
|
||||
.default_value
|
||||
.clone()
|
||||
.expect("Already checked default value"))
|
||||
} else {
|
||||
Err(ShellError::GenericError {
|
||||
error: "The initial value is missing".to_string(),
|
||||
msg: "Missing initial value".to_string(),
|
||||
span: Some(span),
|
||||
help: Some(
|
||||
"Provide an <initial> value as an argument to generate, or assign a default value to the closure parameter"
|
||||
.to_string(),
|
||||
),
|
||||
inner: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
|
|
@ -3,6 +3,7 @@ use nu_engine::command_prelude::*;
|
|||
|
||||
use nu_protocol::Signals;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::collections::HashSet;
|
||||
|
||||
// Character used to separate directories in a Path Environment variable on windows is ";"
|
||||
#[cfg(target_family = "windows")]
|
||||
|
@ -149,6 +150,24 @@ static CHAR_MAP: Lazy<IndexMap<&'static str, String>> = Lazy::new(|| {
|
|||
}
|
||||
});
|
||||
|
||||
static NO_OUTPUT_CHARS: Lazy<HashSet<&'static str>> = Lazy::new(|| {
|
||||
[
|
||||
// If the character is in the this set, we don't output it to prevent
|
||||
// the broken of `char --list` command table format and alignment.
|
||||
"newline",
|
||||
"enter",
|
||||
"nl",
|
||||
"line_feed",
|
||||
"lf",
|
||||
"cr",
|
||||
"crlf",
|
||||
"bel",
|
||||
"backspace",
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
});
|
||||
|
||||
impl Command for Char {
|
||||
fn name(&self) -> &str {
|
||||
"char"
|
||||
|
@ -297,6 +316,11 @@ fn generate_character_list(signals: Signals, call_span: Span) -> PipelineData {
|
|||
CHAR_MAP
|
||||
.iter()
|
||||
.map(move |(name, s)| {
|
||||
let character = if NO_OUTPUT_CHARS.contains(name) {
|
||||
Value::string("", call_span)
|
||||
} else {
|
||||
Value::string(s, call_span)
|
||||
};
|
||||
let unicode = Value::string(
|
||||
s.chars()
|
||||
.map(|c| format!("{:x}", c as u32))
|
||||
|
@ -306,7 +330,7 @@ fn generate_character_list(signals: Signals, call_span: Span) -> PipelineData {
|
|||
);
|
||||
let record = record! {
|
||||
"name" => Value::string(*name, call_span),
|
||||
"character" => Value::string(s, call_span),
|
||||
"character" => character,
|
||||
"unicode" => unicode,
|
||||
};
|
||||
|
||||
|
|
|
@ -606,7 +606,7 @@ mod test {
|
|||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = expand_glob("~/foo.txt", cwd, Span::unknown(), &Signals::empty()).unwrap();
|
||||
let home = dirs_next::home_dir().expect("failed to get home dir");
|
||||
let home = dirs::home_dir().expect("failed to get home dir");
|
||||
let expected: Vec<OsString> = vec![home.join("foo.txt").into()];
|
||||
assert_eq!(actual, expected);
|
||||
})
|
||||
|
|
|
@ -15,12 +15,3 @@ fn break_while_loop() {
|
|||
|
||||
assert_eq!(actual.out, "hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn break_each() {
|
||||
let actual = nu!("
|
||||
[1, 2, 3, 4, 5] | each {|x| if $x > 3 { break }; $x} | math sum
|
||||
");
|
||||
|
||||
assert_eq!(actual.out, "6");
|
||||
}
|
||||
|
|
|
@ -151,7 +151,7 @@ fn filesystem_change_to_home_directory() {
|
|||
"
|
||||
);
|
||||
|
||||
assert_eq!(Some(PathBuf::from(actual.out)), dirs_next::home_dir());
|
||||
assert_eq!(Some(PathBuf::from(actual.out)), dirs::home_dir());
|
||||
})
|
||||
}
|
||||
|
||||
|
|
43
crates/nu-command/tests/commands/chunks.rs
Normal file
43
crates/nu-command/tests/commands/chunks.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use nu_test_support::nu;
|
||||
|
||||
#[test]
|
||||
fn chunk_size_negative() {
|
||||
let actual = nu!("[0 1 2] | chunks -1");
|
||||
assert!(actual.err.contains("positive"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_size_zero() {
|
||||
let actual = nu!("[0 1 2] | chunks 0");
|
||||
assert!(actual.err.contains("zero"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_size_not_int() {
|
||||
let actual = nu!("[0 1 2] | chunks (if true { 1sec })");
|
||||
assert!(actual.err.contains("can't convert"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let actual = nu!("[] | chunks 2 | is-empty");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_stream() {
|
||||
let actual = nu!("([0 1 2] | every 1 | chunks 2) == ([0 1 2] | chunks 2)");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_stream() {
|
||||
let actual = nu!("([[foo bar]; [0 1] [2 3] [4 5]] | every 1 | chunks 2) == ([[foo bar]; [0 1] [2 3] [4 5]] | chunks 2)");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_empty_chunks() {
|
||||
let actual = nu!("([0 1 2 3 4 5] | chunks 3 | length) == 2");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
|
@ -32,3 +32,15 @@ fn default_after_empty_filter() {
|
|||
|
||||
assert_eq!(actual.out, "d");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keeps_nulls_in_lists() {
|
||||
let actual = nu!(r#"[null, 2, 3] | default [] | to json -r"#);
|
||||
assert_eq!(actual.out, "[null,2,3]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn replaces_null() {
|
||||
let actual = nu!(r#"null | default 1"#);
|
||||
assert_eq!(actual.out, "1");
|
||||
}
|
||||
|
|
|
@ -58,22 +58,6 @@ fn each_while_uses_enumerate_index() {
|
|||
assert_eq!(actual.out, "[0, 1, 2, 3]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_element_continue_command() {
|
||||
let actual =
|
||||
nu!("[1,2,3,4,6,7] | each { |x| if ($x mod 2 == 0) {continue} else { $x }} | to nuon");
|
||||
|
||||
assert_eq!(actual.out, "[1, 3, 7]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_element_break_command() {
|
||||
let actual =
|
||||
nu!("[1,2,5,4,6,7] | each { |x| if ($x mod 3 == 0) {break} else { $x }} | to nuon");
|
||||
|
||||
assert_eq!(actual.out, "[1, 2, 5, 4]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_in_nested_each_show() {
|
||||
let actual = nu!("[[1,2]] | each {|x| $x | each {|y| error make {msg: \"oh noes\"} } }");
|
||||
|
|
|
@ -3,7 +3,7 @@ use nu_test_support::{nu, pipeline};
|
|||
#[test]
|
||||
fn generate_no_next_break() {
|
||||
let actual = nu!(
|
||||
"generate 1 {|x| if $x == 3 { {out: $x}} else { {out: $x, next: ($x + 1)} }} | to nuon"
|
||||
"generate {|x| if $x == 3 { {out: $x}} else { {out: $x, next: ($x + 1)} }} 1 | to nuon"
|
||||
);
|
||||
|
||||
assert_eq!(actual.out, "[1, 2, 3]");
|
||||
|
@ -11,7 +11,7 @@ fn generate_no_next_break() {
|
|||
|
||||
#[test]
|
||||
fn generate_null_break() {
|
||||
let actual = nu!("generate 1 {|x| if $x <= 3 { {out: $x, next: ($x + 1)} }} | to nuon");
|
||||
let actual = nu!("generate {|x| if $x <= 3 { {out: $x, next: ($x + 1)} }} 1 | to nuon");
|
||||
|
||||
assert_eq!(actual.out, "[1, 2, 3]");
|
||||
}
|
||||
|
@ -20,13 +20,13 @@ fn generate_null_break() {
|
|||
fn generate_allows_empty_output() {
|
||||
let actual = nu!(pipeline(
|
||||
r#"
|
||||
generate 0 {|x|
|
||||
generate {|x|
|
||||
if $x == 1 {
|
||||
{next: ($x + 1)}
|
||||
} else if $x < 3 {
|
||||
{out: $x, next: ($x + 1)}
|
||||
}
|
||||
} | to nuon
|
||||
} 0 | to nuon
|
||||
"#
|
||||
));
|
||||
|
||||
|
@ -37,11 +37,11 @@ fn generate_allows_empty_output() {
|
|||
fn generate_allows_no_output() {
|
||||
let actual = nu!(pipeline(
|
||||
r#"
|
||||
generate 0 {|x|
|
||||
generate {|x|
|
||||
if $x < 3 {
|
||||
{next: ($x + 1)}
|
||||
}
|
||||
} | to nuon
|
||||
} 0 | to nuon
|
||||
"#
|
||||
));
|
||||
|
||||
|
@ -52,7 +52,7 @@ fn generate_allows_no_output() {
|
|||
fn generate_allows_null_state() {
|
||||
let actual = nu!(pipeline(
|
||||
r#"
|
||||
generate 0 {|x|
|
||||
generate {|x|
|
||||
if $x == null {
|
||||
{out: "done"}
|
||||
} else if $x < 1 {
|
||||
|
@ -60,7 +60,7 @@ fn generate_allows_null_state() {
|
|||
} else {
|
||||
{out: "stopping", next: null}
|
||||
}
|
||||
} | to nuon
|
||||
} 0 | to nuon
|
||||
"#
|
||||
));
|
||||
|
||||
|
@ -71,7 +71,42 @@ fn generate_allows_null_state() {
|
|||
fn generate_allows_null_output() {
|
||||
let actual = nu!(pipeline(
|
||||
r#"
|
||||
generate 0 {|x|
|
||||
generate {|x|
|
||||
if $x == 3 {
|
||||
{out: "done"}
|
||||
} else {
|
||||
{out: null, next: ($x + 1)}
|
||||
}
|
||||
} 0 | to nuon
|
||||
"#
|
||||
));
|
||||
|
||||
assert_eq!(actual.out, "[null, null, null, done]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_disallows_extra_keys() {
|
||||
let actual = nu!("generate {|x| {foo: bar, out: $x}} 0 ");
|
||||
assert!(actual.err.contains("Invalid block return"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_disallows_list() {
|
||||
let actual = nu!("generate {|x| [$x, ($x + 1)]} 0 ");
|
||||
assert!(actual.err.contains("Invalid block return"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_disallows_primitive() {
|
||||
let actual = nu!("generate {|x| 1} 0");
|
||||
assert!(actual.err.contains("Invalid block return"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_allow_default_parameter() {
|
||||
let actual = nu!(pipeline(
|
||||
r#"
|
||||
generate {|x = 0|
|
||||
if $x == 3 {
|
||||
{out: "done"}
|
||||
} else {
|
||||
|
@ -82,22 +117,34 @@ fn generate_allows_null_output() {
|
|||
));
|
||||
|
||||
assert_eq!(actual.out, "[null, null, null, done]");
|
||||
|
||||
// if initial is given, use initial value
|
||||
let actual = nu!(pipeline(
|
||||
r#"
|
||||
generate {|x = 0|
|
||||
if $x == 3 {
|
||||
{out: "done"}
|
||||
} else {
|
||||
{out: null, next: ($x + 1)}
|
||||
}
|
||||
} 1 | to nuon
|
||||
"#
|
||||
));
|
||||
assert_eq!(actual.out, "[null, null, done]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_disallows_extra_keys() {
|
||||
let actual = nu!("generate 0 {|x| {foo: bar, out: $x}}");
|
||||
assert!(actual.err.contains("Invalid block return"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_disallows_list() {
|
||||
let actual = nu!("generate 0 {|x| [$x, ($x + 1)]}");
|
||||
assert!(actual.err.contains("Invalid block return"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_disallows_primitive() {
|
||||
let actual = nu!("generate 0 {|x| 1}");
|
||||
assert!(actual.err.contains("Invalid block return"));
|
||||
fn generate_raise_error_on_no_default_parameter_closure_and_init_val() {
|
||||
let actual = nu!(pipeline(
|
||||
r#"
|
||||
generate {|x|
|
||||
if $x == 3 {
|
||||
{out: "done"}
|
||||
} else {
|
||||
{out: null, next: ($x + 1)}
|
||||
}
|
||||
} | to nuon
|
||||
"#
|
||||
));
|
||||
assert!(actual.err.contains("The initial value is missing"));
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ mod break_;
|
|||
mod bytes;
|
||||
mod cal;
|
||||
mod cd;
|
||||
mod chunks;
|
||||
mod compact;
|
||||
mod complete;
|
||||
mod config_env_default;
|
||||
|
@ -114,6 +115,7 @@ mod try_;
|
|||
mod ucp;
|
||||
#[cfg(unix)]
|
||||
mod ulimit;
|
||||
mod window;
|
||||
|
||||
mod debug;
|
||||
mod umkdir;
|
||||
|
|
|
@ -439,3 +439,37 @@ fn no_duplicate_redirection() {
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[rstest::rstest]
|
||||
#[case("let", "out>")]
|
||||
#[case("let", "err>")]
|
||||
#[case("let", "out+err>")]
|
||||
#[case("mut", "out>")]
|
||||
#[case("mut", "err>")]
|
||||
#[case("mut", "out+err>")]
|
||||
fn file_redirection_in_let_and_mut(#[case] keyword: &str, #[case] redir: &str) {
|
||||
Playground::setup("file redirection in let and mut", |dirs, _| {
|
||||
let actual = nu!(
|
||||
cwd: dirs.test(),
|
||||
format!("$env.BAZ = 'foo'; {keyword} v = nu --testbin echo_env_mixed out-err BAZ BAZ {redir} result.txt")
|
||||
);
|
||||
assert!(actual.status.success());
|
||||
assert!(file_contents(dirs.test().join("result.txt")).contains("foo"));
|
||||
})
|
||||
}
|
||||
|
||||
#[rstest::rstest]
|
||||
#[case("let", "err>|", "foo3")]
|
||||
#[case("let", "out+err>|", "7")]
|
||||
#[case("mut", "err>|", "foo3")]
|
||||
#[case("mut", "out+err>|", "7")]
|
||||
fn pipe_redirection_in_let_and_mut(
|
||||
#[case] keyword: &str,
|
||||
#[case] redir: &str,
|
||||
#[case] output: &str,
|
||||
) {
|
||||
let actual = nu!(
|
||||
format!("$env.BAZ = 'foo'; {keyword} v = nu --testbin echo_env_mixed out-err BAZ BAZ {redir} str length; $v")
|
||||
);
|
||||
assert_eq!(actual.out, output);
|
||||
}
|
||||
|
|
|
@ -309,7 +309,7 @@ fn external_arg_expand_tilde() {
|
|||
"#
|
||||
));
|
||||
|
||||
let home = dirs_next::home_dir().expect("failed to find home dir");
|
||||
let home = dirs::home_dir().expect("failed to find home dir");
|
||||
|
||||
assert_eq!(
|
||||
actual.out,
|
||||
|
|
|
@ -2901,3 +2901,9 @@ fn table_general_header_on_separator_trim_algorithm() {
|
|||
let actual = nu!("$env.config.table.header_on_separator = true; [[a b]; ['11111111111111111111111111111111111111' 2] ] | table --width=20 --theme basic");
|
||||
assert_eq!(actual.out, "+-#-+----a-----+-b-+| 0 | 11111111 | 2 || | 11111111 | || | 11111111 | || | 11111111 | || | 111111 | |+---+----------+---+");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_general_header_on_separator_issue1() {
|
||||
let actual = nu!("$env.config.table.header_on_separator = true; [['Llll oo Bbbbbbbb' 'Bbbbbbbb Aaaa' Nnnnnn Ggggg 'Xxxxx Llllllll #' Bbb 'Pppp Ccccc' 'Rrrrrrrr Dddd' Rrrrrr 'Rrrrrr Ccccc II' 'Rrrrrr Ccccc Ppppppp II' 'Pppppp Dddddddd Tttt' 'Pppppp Dddddddd Dddd' 'Rrrrrrrrr Trrrrrr' 'Pppppp Ppppp Dddd' 'Ppppp Dddd' Hhhh]; [RRRRRRR FFFFFFFF UUUU VV 202407160001 BBB 1 '7/16/2024' '' AAA-1111 AAA-1111-11 '7 YEARS' 2555 'RRRRRRRR DDDD' '7/16/2031' '7/16/2031' NN]] | table --width=87 --theme basic");
|
||||
assert_eq!(actual.out, "+-#-+-Llll oo Bbbbbbbb-+-Bbbbbbbb Aaaa-+-Nnnnnn-+-Ggggg-+-Xxxxx Llllllll #-+-...-+| 0 | RRRRRRR | FFFFFFFF | UUUU | VV | 202407160001 | ... |+---+------------------+---------------+--------+-------+------------------+-----+");
|
||||
}
|
||||
|
|
103
crates/nu-command/tests/commands/window.rs
Normal file
103
crates/nu-command/tests/commands/window.rs
Normal file
|
@ -0,0 +1,103 @@
|
|||
use nu_test_support::nu;
|
||||
|
||||
#[test]
|
||||
fn window_size_negative() {
|
||||
let actual = nu!("[0 1 2] | window -1");
|
||||
assert!(actual.err.contains("positive"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_size_zero() {
|
||||
let actual = nu!("[0 1 2] | window 0");
|
||||
assert!(actual.err.contains("zero"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_size_not_int() {
|
||||
let actual = nu!("[0 1 2] | window (if true { 1sec })");
|
||||
assert!(actual.err.contains("can't convert"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stride_negative() {
|
||||
let actual = nu!("[0 1 2] | window 1 -s -1");
|
||||
assert!(actual.err.contains("positive"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stride_zero() {
|
||||
let actual = nu!("[0 1 2] | window 1 -s 0");
|
||||
assert!(actual.err.contains("zero"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stride_not_int() {
|
||||
let actual = nu!("[0 1 2] | window 1 -s (if true { 1sec })");
|
||||
assert!(actual.err.contains("can't convert"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let actual = nu!("[] | window 2 | is-empty");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_stream() {
|
||||
let actual = nu!("([0 1 2] | every 1 | window 2) == ([0 1 2] | window 2)");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_stream() {
|
||||
let actual = nu!("([[foo bar]; [0 1] [2 3] [4 5]] | every 1 | window 2) == ([[foo bar]; [0 1] [2 3] [4 5]] | window 2)");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_empty_chunks() {
|
||||
let actual = nu!("([0 1 2 3 4 5] | window 3 -s 3 -r | length) == 2");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_as_chunks() {
|
||||
let actual = nu!("([0 1 2 3 4] | window 2 -s 2 -r) == ([0 1 2 3 4 ] | chunks 2)");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stride_equal_to_window_size() {
|
||||
let actual = nu!("([0 1 2 3] | window 2 -s 2 | flatten) == [0 1 2 3]");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stride_greater_than_window_size() {
|
||||
let actual = nu!("([0 1 2 3 4] | window 2 -s 3 | flatten) == [0 1 3 4]");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stride_less_than_window_size() {
|
||||
let actual = nu!("([0 1 2 3 4 5] | window 3 -s 2 | length) == 2");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stride_equal_to_window_size_remainder() {
|
||||
let actual = nu!("([0 1 2 3 4] | window 2 -s 2 -r | flatten) == [0 1 2 3 4]");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stride_greater_than_window_size_remainder() {
|
||||
let actual = nu!("([0 1 2 3 4] | window 2 -s 3 -r | flatten) == [0 1 3 4]");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stride_less_than_window_size_remainder() {
|
||||
let actual = nu!("([0 1 2 3 4 5] | window 3 -s 2 -r | length) == 3");
|
||||
assert_eq!(actual.out, "true");
|
||||
}
|
|
@ -204,6 +204,7 @@ impl BlockBuilder {
|
|||
Instruction::Drain { src } => allocate(&[*src], &[]),
|
||||
Instruction::LoadVariable { dst, var_id: _ } => allocate(&[], &[*dst]),
|
||||
Instruction::StoreVariable { var_id: _, src } => allocate(&[*src], &[]),
|
||||
Instruction::DropVariable { var_id: _ } => Ok(()),
|
||||
Instruction::LoadEnv { dst, key: _ } => allocate(&[], &[*dst]),
|
||||
Instruction::LoadEnvOpt { dst, key: _ } => allocate(&[], &[*dst]),
|
||||
Instruction::StoreEnv { key: _, src } => allocate(&[*src], &[]),
|
||||
|
|
|
@ -171,6 +171,27 @@ pub(crate) fn compile_expression(
|
|||
Err(CompileError::UnsupportedOperatorExpression { span: op.span })
|
||||
}
|
||||
}
|
||||
Expr::Collect(var_id, expr) => {
|
||||
let store_reg = if let Some(in_reg) = in_reg {
|
||||
// Collect, clone, store
|
||||
builder.push(Instruction::Collect { src_dst: in_reg }.into_spanned(expr.span))?;
|
||||
builder.clone_reg(in_reg, expr.span)?
|
||||
} else {
|
||||
// Just store nothing in the variable
|
||||
builder.literal(Literal::Nothing.into_spanned(Span::unknown()))?
|
||||
};
|
||||
builder.push(
|
||||
Instruction::StoreVariable {
|
||||
var_id: *var_id,
|
||||
src: store_reg,
|
||||
}
|
||||
.into_spanned(expr.span),
|
||||
)?;
|
||||
compile_expression(working_set, builder, expr, redirect_modes, in_reg, out_reg)?;
|
||||
// Clean it up afterward
|
||||
builder.push(Instruction::DropVariable { var_id: *var_id }.into_spanned(expr.span))?;
|
||||
Ok(())
|
||||
}
|
||||
Expr::Subexpression(block_id) => {
|
||||
let block = working_set.get_block(*block_id);
|
||||
compile_block(working_set, builder, block, redirect_modes, in_reg, out_reg)
|
||||
|
|
|
@ -10,8 +10,8 @@ use nu_protocol::{
|
|||
debugger::DebugContext,
|
||||
engine::{Closure, EngineState, Redirection, Stack, StateWorkingSet},
|
||||
eval_base::Eval,
|
||||
ByteStreamSource, Config, FromValue, IntoPipelineData, OutDest, PipelineData, ShellError, Span,
|
||||
Spanned, Type, Value, VarId, ENV_VARIABLE_ID,
|
||||
ByteStreamSource, Config, DataSource, FromValue, IntoPipelineData, OutDest, PipelineData,
|
||||
PipelineMetadata, ShellError, Span, Spanned, Type, Value, VarId, ENV_VARIABLE_ID,
|
||||
};
|
||||
use nu_utils::IgnoreCaseExt;
|
||||
use std::{fs::OpenOptions, path::PathBuf, sync::Arc};
|
||||
|
@ -259,6 +259,10 @@ pub fn eval_expression_with_input<D: DebugContext>(
|
|||
input = eval_external(engine_state, stack, head, args, input)?;
|
||||
}
|
||||
|
||||
Expr::Collect(var_id, expr) => {
|
||||
input = eval_collect::<D>(engine_state, stack, *var_id, expr, input)?;
|
||||
}
|
||||
|
||||
Expr::Subexpression(block_id) => {
|
||||
let block = engine_state.get_block(*block_id);
|
||||
// FIXME: protect this collect with ctrl-c
|
||||
|
@ -605,6 +609,44 @@ pub fn eval_block<D: DebugContext>(
|
|||
Ok(input)
|
||||
}
|
||||
|
||||
pub fn eval_collect<D: DebugContext>(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
var_id: VarId,
|
||||
expr: &Expression,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
// Evaluate the expression with the variable set to the collected input
|
||||
let span = input.span().unwrap_or(Span::unknown());
|
||||
|
||||
let metadata = match input.metadata() {
|
||||
// Remove the `FilePath` metadata, because after `collect` it's no longer necessary to
|
||||
// check where some input came from.
|
||||
Some(PipelineMetadata {
|
||||
data_source: DataSource::FilePath(_),
|
||||
content_type: None,
|
||||
}) => None,
|
||||
other => other,
|
||||
};
|
||||
|
||||
let input = input.into_value(span)?;
|
||||
|
||||
stack.add_var(var_id, input.clone());
|
||||
|
||||
let result = eval_expression_with_input::<D>(
|
||||
engine_state,
|
||||
stack,
|
||||
expr,
|
||||
// We still have to pass it as input
|
||||
input.into_pipeline_data_with_metadata(metadata),
|
||||
)
|
||||
.map(|(result, _failed)| result);
|
||||
|
||||
stack.remove_var(var_id);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn eval_subexpression<D: DebugContext>(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
|
@ -729,6 +771,18 @@ impl Eval for EvalRuntime {
|
|||
eval_external(engine_state, stack, head, args, PipelineData::empty())?.into_value(span)
|
||||
}
|
||||
|
||||
fn eval_collect<D: DebugContext>(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
var_id: VarId,
|
||||
expr: &Expression,
|
||||
) -> Result<Value, ShellError> {
|
||||
// It's a little bizarre, but the expression can still have some kind of result even with
|
||||
// nothing input
|
||||
eval_collect::<D>(engine_state, stack, var_id, expr, PipelineData::empty())?
|
||||
.into_value(expr.span)
|
||||
}
|
||||
|
||||
fn eval_subexpression<D: DebugContext>(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
|
|
|
@ -6,9 +6,9 @@ use nu_protocol::{
|
|||
debugger::DebugContext,
|
||||
engine::{Argument, Closure, EngineState, ErrorHandler, Matcher, Redirection, Stack},
|
||||
ir::{Call, DataSlice, Instruction, IrAstRef, IrBlock, Literal, RedirectMode},
|
||||
record, ByteStreamSource, DeclId, ErrSpan, Flag, IntoPipelineData, IntoSpanned, ListStream,
|
||||
OutDest, PipelineData, PositionalArg, Range, Record, RegId, ShellError, Signals, Signature,
|
||||
Span, Spanned, Type, Value, VarId, ENV_VARIABLE_ID,
|
||||
record, ByteStreamSource, DataSource, DeclId, ErrSpan, Flag, IntoPipelineData, IntoSpanned,
|
||||
ListStream, OutDest, PipelineData, PipelineMetadata, PositionalArg, Range, Record, RegId,
|
||||
ShellError, Signals, Signature, Span, Spanned, Type, Value, VarId, ENV_VARIABLE_ID,
|
||||
};
|
||||
use nu_utils::IgnoreCaseExt;
|
||||
|
||||
|
@ -345,6 +345,10 @@ fn eval_instruction<D: DebugContext>(
|
|||
ctx.stack.add_var(*var_id, value);
|
||||
Ok(Continue)
|
||||
}
|
||||
Instruction::DropVariable { var_id } => {
|
||||
ctx.stack.remove_var(*var_id);
|
||||
Ok(Continue)
|
||||
}
|
||||
Instruction::LoadEnv { dst, key } => {
|
||||
let key = ctx.get_str(*key, *span)?;
|
||||
if let Some(value) = get_env_var_case_insensitive(ctx, key) {
|
||||
|
@ -1341,9 +1345,19 @@ fn get_env_var_name_case_insensitive<'a>(ctx: &mut EvalContext<'_>, key: &'a str
|
|||
}
|
||||
|
||||
/// Helper to collect values into [`PipelineData`], preserving original span and metadata
|
||||
///
|
||||
/// The metadata is removed if it is the file data source, as that's just meant to mark streams.
|
||||
fn collect(data: PipelineData, fallback_span: Span) -> Result<PipelineData, ShellError> {
|
||||
let span = data.span().unwrap_or(fallback_span);
|
||||
let metadata = data.metadata();
|
||||
let metadata = match data.metadata() {
|
||||
// Remove the `FilePath` metadata, because after `collect` it's no longer necessary to
|
||||
// check where some input came from.
|
||||
Some(PipelineMetadata {
|
||||
data_source: DataSource::FilePath(_),
|
||||
content_type: None,
|
||||
}) => None,
|
||||
other => other,
|
||||
};
|
||||
let value = data.into_value(span)?;
|
||||
Ok(PipelineData::Value(value, metadata))
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ use nu_protocol::{
|
|||
RecordItem,
|
||||
},
|
||||
engine::StateWorkingSet,
|
||||
DeclId, Span, VarId,
|
||||
DeclId, Span, SyntaxShape, VarId,
|
||||
};
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
|
@ -166,6 +166,22 @@ fn flatten_pipeline_element_into(
|
|||
}
|
||||
}
|
||||
|
||||
fn flatten_positional_arg_into(
|
||||
working_set: &StateWorkingSet,
|
||||
positional: &Expression,
|
||||
shape: &SyntaxShape,
|
||||
output: &mut Vec<(Span, FlatShape)>,
|
||||
) {
|
||||
if matches!(shape, SyntaxShape::ExternalArgument)
|
||||
&& matches!(positional.expr, Expr::String(..) | Expr::GlobPattern(..))
|
||||
{
|
||||
// Make known external arguments look more like external arguments
|
||||
output.push((positional.span, FlatShape::ExternalArg));
|
||||
} else {
|
||||
flatten_expression_into(working_set, positional, output)
|
||||
}
|
||||
}
|
||||
|
||||
fn flatten_expression_into(
|
||||
working_set: &StateWorkingSet,
|
||||
expr: &Expression,
|
||||
|
@ -189,6 +205,9 @@ fn flatten_expression_into(
|
|||
));
|
||||
flatten_expression_into(working_set, not, output);
|
||||
}
|
||||
Expr::Collect(_, expr) => {
|
||||
flatten_expression_into(working_set, expr, output);
|
||||
}
|
||||
Expr::Closure(block_id) => {
|
||||
let outer_span = expr.span;
|
||||
|
||||
|
@ -246,16 +265,40 @@ fn flatten_expression_into(
|
|||
}
|
||||
}
|
||||
Expr::Call(call) => {
|
||||
let decl = working_set.get_decl(call.decl_id);
|
||||
|
||||
if call.head.end != 0 {
|
||||
// Make sure we don't push synthetic calls
|
||||
output.push((call.head, FlatShape::InternalCall(call.decl_id)));
|
||||
}
|
||||
|
||||
// Follow positional arguments from the signature.
|
||||
let signature = decl.signature();
|
||||
let mut positional_args = signature
|
||||
.required_positional
|
||||
.iter()
|
||||
.chain(&signature.optional_positional);
|
||||
|
||||
let arg_start = output.len();
|
||||
for arg in &call.arguments {
|
||||
match arg {
|
||||
Argument::Positional(positional) | Argument::Unknown(positional) => {
|
||||
flatten_expression_into(working_set, positional, output)
|
||||
Argument::Positional(positional) => {
|
||||
let positional_arg = positional_args.next();
|
||||
let shape = positional_arg
|
||||
.or(signature.rest_positional.as_ref())
|
||||
.map(|arg| &arg.shape)
|
||||
.unwrap_or(&SyntaxShape::Any);
|
||||
|
||||
flatten_positional_arg_into(working_set, positional, shape, output)
|
||||
}
|
||||
Argument::Unknown(positional) => {
|
||||
let shape = signature
|
||||
.rest_positional
|
||||
.as_ref()
|
||||
.map(|arg| &arg.shape)
|
||||
.unwrap_or(&SyntaxShape::Any);
|
||||
|
||||
flatten_positional_arg_into(working_set, positional, shape, output)
|
||||
}
|
||||
Argument::Named(named) => {
|
||||
if named.0.span.end != 0 {
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
//! can be parsed.
|
||||
|
||||
use crate::{Token, TokenContents};
|
||||
use itertools::{Either, Itertools};
|
||||
use nu_protocol::{ast::RedirectionSource, ParseError, Span};
|
||||
use std::mem;
|
||||
|
||||
|
@ -24,6 +25,15 @@ impl LiteRedirectionTarget {
|
|||
| LiteRedirectionTarget::Pipe { connector } => *connector,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spans(&self) -> impl Iterator<Item = Span> {
|
||||
match *self {
|
||||
LiteRedirectionTarget::File {
|
||||
connector, file, ..
|
||||
} => Either::Left([connector, file].into_iter()),
|
||||
LiteRedirectionTarget::Pipe { connector } => Either::Right(std::iter::once(connector)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -38,6 +48,17 @@ pub enum LiteRedirection {
|
|||
},
|
||||
}
|
||||
|
||||
impl LiteRedirection {
|
||||
pub fn spans(&self) -> impl Iterator<Item = Span> {
|
||||
match self {
|
||||
LiteRedirection::Single { target, .. } => Either::Left(target.spans()),
|
||||
LiteRedirection::Separate { out, err } => {
|
||||
Either::Right(out.spans().chain(err.spans()).sorted())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct LiteCommand {
|
||||
pub pipe: Option<Span>,
|
||||
|
@ -113,6 +134,14 @@ impl LiteCommand {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parts_including_redirection(&self) -> impl Iterator<Item = Span> + '_ {
|
||||
self.parts.iter().copied().chain(
|
||||
self.redirection
|
||||
.iter()
|
||||
.flat_map(|redirection| redirection.spans()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
|
|
|
@ -783,6 +783,16 @@ pub fn parse_extern(
|
|||
working_set.get_block_mut(block_id).signature = signature;
|
||||
}
|
||||
} else {
|
||||
if signature.rest_positional.is_none() {
|
||||
// Make sure that a known external takes rest args with ExternalArgument
|
||||
// shape
|
||||
*signature = signature.rest(
|
||||
"args",
|
||||
SyntaxShape::ExternalArgument,
|
||||
"all other arguments to the command",
|
||||
);
|
||||
}
|
||||
|
||||
let decl = KnownExternal {
|
||||
name: external_name,
|
||||
usage,
|
||||
|
|
|
@ -221,6 +221,22 @@ pub(crate) fn check_call(
|
|||
}
|
||||
}
|
||||
|
||||
/// Parses an unknown argument for the given signature. This handles the parsing as appropriate to
|
||||
/// the rest type of the command.
|
||||
fn parse_unknown_arg(
|
||||
working_set: &mut StateWorkingSet,
|
||||
span: Span,
|
||||
signature: &Signature,
|
||||
) -> Expression {
|
||||
let shape = signature
|
||||
.rest_positional
|
||||
.as_ref()
|
||||
.map(|arg| arg.shape.clone())
|
||||
.unwrap_or(SyntaxShape::Any);
|
||||
|
||||
parse_value(working_set, span, &shape)
|
||||
}
|
||||
|
||||
/// Parses a string in the arg or head position of an external call.
|
||||
///
|
||||
/// If the string begins with `r#`, it is parsed as a raw string. If it doesn't contain any quotes
|
||||
|
@ -427,11 +443,7 @@ fn parse_external_string(working_set: &mut StateWorkingSet, span: Span) -> Expre
|
|||
fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> ExternalArgument {
|
||||
let contents = working_set.get_span_contents(span);
|
||||
|
||||
if contents.starts_with(b"$") || contents.starts_with(b"(") {
|
||||
ExternalArgument::Regular(parse_dollar_expr(working_set, span))
|
||||
} else if contents.starts_with(b"[") {
|
||||
ExternalArgument::Regular(parse_list_expression(working_set, span, &SyntaxShape::Any))
|
||||
} else if contents.len() > 3
|
||||
if contents.len() > 3
|
||||
&& contents.starts_with(b"...")
|
||||
&& (contents[3] == b'$' || contents[3] == b'[' || contents[3] == b'(')
|
||||
{
|
||||
|
@ -441,7 +453,19 @@ fn parse_external_arg(working_set: &mut StateWorkingSet, span: Span) -> External
|
|||
&SyntaxShape::List(Box::new(SyntaxShape::Any)),
|
||||
))
|
||||
} else {
|
||||
ExternalArgument::Regular(parse_external_string(working_set, span))
|
||||
ExternalArgument::Regular(parse_regular_external_arg(working_set, span))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_regular_external_arg(working_set: &mut StateWorkingSet, span: Span) -> Expression {
|
||||
let contents = working_set.get_span_contents(span);
|
||||
|
||||
if contents.starts_with(b"$") || contents.starts_with(b"(") {
|
||||
parse_dollar_expr(working_set, span)
|
||||
} else if contents.starts_with(b"[") {
|
||||
parse_list_expression(working_set, span, &SyntaxShape::Any)
|
||||
} else {
|
||||
parse_external_string(working_set, span)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -998,7 +1022,7 @@ pub fn parse_internal_call(
|
|||
&& signature.allows_unknown_args
|
||||
{
|
||||
working_set.parse_errors.truncate(starting_error_count);
|
||||
let arg = parse_value(working_set, arg_span, &SyntaxShape::Any);
|
||||
let arg = parse_unknown_arg(working_set, arg_span, &signature);
|
||||
|
||||
call.add_unknown(arg);
|
||||
} else {
|
||||
|
@ -1040,7 +1064,7 @@ pub fn parse_internal_call(
|
|||
&& signature.allows_unknown_args
|
||||
{
|
||||
working_set.parse_errors.truncate(starting_error_count);
|
||||
let arg = parse_value(working_set, arg_span, &SyntaxShape::Any);
|
||||
let arg = parse_unknown_arg(working_set, arg_span, &signature);
|
||||
|
||||
call.add_unknown(arg);
|
||||
} else {
|
||||
|
@ -1135,6 +1159,10 @@ pub fn parse_internal_call(
|
|||
call.add_positional(Expression::garbage(working_set, arg_span));
|
||||
} else {
|
||||
let rest_shape = match &signature.rest_positional {
|
||||
Some(arg) if matches!(arg.shape, SyntaxShape::ExternalArgument) => {
|
||||
// External args aren't parsed inside lists in spread position.
|
||||
SyntaxShape::Any
|
||||
}
|
||||
Some(arg) => arg.shape.clone(),
|
||||
None => SyntaxShape::Any,
|
||||
};
|
||||
|
@ -1196,7 +1224,7 @@ pub fn parse_internal_call(
|
|||
call.add_positional(arg);
|
||||
positional_idx += 1;
|
||||
} else if signature.allows_unknown_args {
|
||||
let arg = parse_value(working_set, arg_span, &SyntaxShape::Any);
|
||||
let arg = parse_unknown_arg(working_set, arg_span, &signature);
|
||||
|
||||
call.add_unknown(arg);
|
||||
} else {
|
||||
|
@ -4671,7 +4699,8 @@ pub fn parse_value(
|
|||
| SyntaxShape::Signature
|
||||
| SyntaxShape::Filepath
|
||||
| SyntaxShape::String
|
||||
| SyntaxShape::GlobPattern => {}
|
||||
| SyntaxShape::GlobPattern
|
||||
| SyntaxShape::ExternalArgument => {}
|
||||
_ => {
|
||||
working_set.error(ParseError::Expected("non-[] value", span));
|
||||
return Expression::garbage(working_set, span);
|
||||
|
@ -4748,6 +4777,8 @@ pub fn parse_value(
|
|||
Expression::garbage(working_set, span)
|
||||
}
|
||||
|
||||
SyntaxShape::ExternalArgument => parse_regular_external_arg(working_set, span),
|
||||
|
||||
SyntaxShape::Any => {
|
||||
if bytes.starts_with(b"[") {
|
||||
//parse_value(working_set, span, &SyntaxShape::Table)
|
||||
|
@ -5398,6 +5429,7 @@ pub fn parse_expression(working_set: &mut StateWorkingSet, spans: &[Span]) -> Ex
|
|||
let mut block = Block::default();
|
||||
let ty = output.ty.clone();
|
||||
block.pipelines = vec![Pipeline::from_vec(vec![output])];
|
||||
block.span = Some(Span::concat(spans));
|
||||
|
||||
compile_block(working_set, &mut block);
|
||||
|
||||
|
@ -5492,9 +5524,19 @@ pub fn parse_builtin_commands(
|
|||
match name {
|
||||
b"def" => parse_def(working_set, lite_command, None).0,
|
||||
b"extern" => parse_extern(working_set, lite_command, None),
|
||||
b"let" => parse_let(working_set, &lite_command.parts),
|
||||
b"let" => parse_let(
|
||||
working_set,
|
||||
&lite_command
|
||||
.parts_including_redirection()
|
||||
.collect::<Vec<Span>>(),
|
||||
),
|
||||
b"const" => parse_const(working_set, &lite_command.parts),
|
||||
b"mut" => parse_mut(working_set, &lite_command.parts),
|
||||
b"mut" => parse_mut(
|
||||
working_set,
|
||||
&lite_command
|
||||
.parts_including_redirection()
|
||||
.collect::<Vec<Span>>(),
|
||||
),
|
||||
b"for" => {
|
||||
let expr = parse_for(working_set, lite_command);
|
||||
Pipeline::from_vec(vec![expr])
|
||||
|
@ -5746,169 +5788,73 @@ pub(crate) fn redirecting_builtin_error(
|
|||
}
|
||||
}
|
||||
|
||||
pub fn parse_pipeline(
|
||||
working_set: &mut StateWorkingSet,
|
||||
pipeline: &LitePipeline,
|
||||
is_subexpression: bool,
|
||||
pipeline_index: usize,
|
||||
) -> Pipeline {
|
||||
pub fn parse_pipeline(working_set: &mut StateWorkingSet, pipeline: &LitePipeline) -> Pipeline {
|
||||
let first_command = pipeline.commands.first();
|
||||
let first_command_name = first_command
|
||||
.and_then(|command| command.parts.first())
|
||||
.map(|span| working_set.get_span_contents(*span));
|
||||
|
||||
if pipeline.commands.len() > 1 {
|
||||
// Special case: allow `let` and `mut` to consume the whole pipeline, eg) `let abc = "foo" | str length`
|
||||
if let Some(&first) = pipeline.commands[0].parts.first() {
|
||||
let first = working_set.get_span_contents(first);
|
||||
if first == b"let" || first == b"mut" {
|
||||
let name = if first == b"let" { "let" } else { "mut" };
|
||||
let mut new_command = LiteCommand {
|
||||
comments: vec![],
|
||||
parts: pipeline.commands[0].parts.clone(),
|
||||
pipe: None,
|
||||
redirection: None,
|
||||
};
|
||||
// Special case: allow "let" or "mut" to consume the whole pipeline, if this is a pipeline
|
||||
// with multiple commands
|
||||
if matches!(first_command_name, Some(b"let" | b"mut")) {
|
||||
// Merge the pipeline into one command
|
||||
let first_command = first_command.expect("must be Some");
|
||||
|
||||
if let Some(redirection) = pipeline.commands[0].redirection.as_ref() {
|
||||
working_set.error(redirecting_builtin_error(name, redirection));
|
||||
}
|
||||
let remainder_span = first_command
|
||||
.parts_including_redirection()
|
||||
.skip(3)
|
||||
.chain(
|
||||
pipeline.commands[1..]
|
||||
.iter()
|
||||
.flat_map(|command| command.parts_including_redirection()),
|
||||
)
|
||||
.reduce(Span::append);
|
||||
|
||||
for element in &pipeline.commands[1..] {
|
||||
if let Some(redirection) = pipeline.commands[0].redirection.as_ref() {
|
||||
working_set.error(redirecting_builtin_error(name, redirection));
|
||||
} else {
|
||||
new_command.parts.push(element.pipe.expect("pipe span"));
|
||||
new_command.comments.extend_from_slice(&element.comments);
|
||||
new_command.parts.extend_from_slice(&element.parts);
|
||||
}
|
||||
}
|
||||
let parts = first_command
|
||||
.parts
|
||||
.iter()
|
||||
.take(3) // the let/mut start itself
|
||||
.copied()
|
||||
.chain(remainder_span) // everything else
|
||||
.collect();
|
||||
|
||||
// if the 'let' is complete enough, use it, if not, fall through for now
|
||||
if new_command.parts.len() > 3 {
|
||||
let rhs_span = Span::concat(&new_command.parts[3..]);
|
||||
let comments = pipeline
|
||||
.commands
|
||||
.iter()
|
||||
.flat_map(|command| command.comments.iter())
|
||||
.copied()
|
||||
.collect();
|
||||
|
||||
new_command.parts.truncate(3);
|
||||
new_command.parts.push(rhs_span);
|
||||
|
||||
let mut pipeline = parse_builtin_commands(working_set, &new_command);
|
||||
|
||||
if pipeline_index == 0 {
|
||||
let let_decl_id = working_set.find_decl(b"let");
|
||||
let mut_decl_id = working_set.find_decl(b"mut");
|
||||
for element in pipeline.elements.iter_mut() {
|
||||
if let Expr::Call(call) = &element.expr.expr {
|
||||
if Some(call.decl_id) == let_decl_id
|
||||
|| Some(call.decl_id) == mut_decl_id
|
||||
{
|
||||
// Do an expansion
|
||||
if let Some(Expression {
|
||||
expr: Expr::Block(block_id),
|
||||
..
|
||||
}) = call.positional_iter().nth(1)
|
||||
{
|
||||
let block = working_set.get_block(*block_id);
|
||||
|
||||
if let Some(element) = block
|
||||
.pipelines
|
||||
.first()
|
||||
.and_then(|p| p.elements.first())
|
||||
.cloned()
|
||||
{
|
||||
if element.has_in_variable(working_set) {
|
||||
let element = wrap_element_with_collect(
|
||||
working_set,
|
||||
&element,
|
||||
);
|
||||
let block = working_set.get_block_mut(*block_id);
|
||||
block.pipelines[0].elements[0] = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
} else if element.has_in_variable(working_set) && !is_subexpression
|
||||
{
|
||||
*element = wrap_element_with_collect(working_set, element);
|
||||
}
|
||||
} else if element.has_in_variable(working_set) && !is_subexpression {
|
||||
*element = wrap_element_with_collect(working_set, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pipeline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut elements = pipeline
|
||||
.commands
|
||||
.iter()
|
||||
.map(|element| parse_pipeline_element(working_set, element))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if is_subexpression {
|
||||
for element in elements.iter_mut().skip(1) {
|
||||
if element.has_in_variable(working_set) {
|
||||
*element = wrap_element_with_collect(working_set, element);
|
||||
}
|
||||
}
|
||||
let new_command = LiteCommand {
|
||||
pipe: None,
|
||||
comments,
|
||||
parts,
|
||||
redirection: None,
|
||||
};
|
||||
parse_builtin_commands(working_set, &new_command)
|
||||
} else {
|
||||
for element in elements.iter_mut() {
|
||||
if element.has_in_variable(working_set) {
|
||||
*element = wrap_element_with_collect(working_set, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Pipeline { elements }
|
||||
} else {
|
||||
if let Some(&first) = pipeline.commands[0].parts.first() {
|
||||
let first = working_set.get_span_contents(first);
|
||||
if first == b"let" || first == b"mut" {
|
||||
if let Some(redirection) = pipeline.commands[0].redirection.as_ref() {
|
||||
let name = if first == b"let" { "let" } else { "mut" };
|
||||
working_set.error(redirecting_builtin_error(name, redirection));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut pipeline = parse_builtin_commands(working_set, &pipeline.commands[0]);
|
||||
|
||||
let let_decl_id = working_set.find_decl(b"let");
|
||||
let mut_decl_id = working_set.find_decl(b"mut");
|
||||
|
||||
if pipeline_index == 0 {
|
||||
for element in pipeline.elements.iter_mut() {
|
||||
if let Expr::Call(call) = &element.expr.expr {
|
||||
if Some(call.decl_id) == let_decl_id || Some(call.decl_id) == mut_decl_id {
|
||||
// Do an expansion
|
||||
if let Some(Expression {
|
||||
expr: Expr::Block(block_id),
|
||||
..
|
||||
}) = call.positional_iter().nth(1)
|
||||
{
|
||||
let block = working_set.get_block(*block_id);
|
||||
|
||||
if let Some(element) = block
|
||||
.pipelines
|
||||
.first()
|
||||
.and_then(|p| p.elements.first())
|
||||
.cloned()
|
||||
{
|
||||
if element.has_in_variable(working_set) {
|
||||
let element = wrap_element_with_collect(working_set, &element);
|
||||
let block = working_set.get_block_mut(*block_id);
|
||||
block.pipelines[0].elements[0] = element;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
} else if element.has_in_variable(working_set) && !is_subexpression {
|
||||
*element = wrap_element_with_collect(working_set, element);
|
||||
// Parse a normal multi command pipeline
|
||||
let elements: Vec<_> = pipeline
|
||||
.commands
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, element)| {
|
||||
let element = parse_pipeline_element(working_set, element);
|
||||
// Handle $in for pipeline elements beyond the first one
|
||||
if index > 0 && element.has_in_variable(working_set) {
|
||||
wrap_element_with_collect(working_set, element.clone())
|
||||
} else {
|
||||
element
|
||||
}
|
||||
} else if element.has_in_variable(working_set) && !is_subexpression {
|
||||
*element = wrap_element_with_collect(working_set, element);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
pipeline
|
||||
Pipeline { elements }
|
||||
}
|
||||
} else {
|
||||
// If there's only one command in the pipeline, this could be a builtin command
|
||||
parse_builtin_commands(working_set, &pipeline.commands[0])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5939,18 +5885,45 @@ pub fn parse_block(
|
|||
}
|
||||
|
||||
let mut block = Block::new_with_capacity(lite_block.block.len());
|
||||
block.span = Some(span);
|
||||
|
||||
for (idx, lite_pipeline) in lite_block.block.iter().enumerate() {
|
||||
let pipeline = parse_pipeline(working_set, lite_pipeline, is_subexpression, idx);
|
||||
for lite_pipeline in &lite_block.block {
|
||||
let pipeline = parse_pipeline(working_set, lite_pipeline);
|
||||
block.pipelines.push(pipeline);
|
||||
}
|
||||
|
||||
// If this is not a subexpression and there are any pipelines where the first element has $in,
|
||||
// we can wrap the whole block in collect so that they all reference the same $in
|
||||
if !is_subexpression
|
||||
&& block
|
||||
.pipelines
|
||||
.iter()
|
||||
.flat_map(|pipeline| pipeline.elements.first())
|
||||
.any(|element| element.has_in_variable(working_set))
|
||||
{
|
||||
// Move the block out to prepare it to become a subexpression
|
||||
let inner_block = std::mem::take(&mut block);
|
||||
block.span = inner_block.span;
|
||||
let ty = inner_block.output_type();
|
||||
let block_id = working_set.add_block(Arc::new(inner_block));
|
||||
|
||||
// Now wrap it in a Collect expression, and put it in the block as the only pipeline
|
||||
let subexpression = Expression::new(working_set, Expr::Subexpression(block_id), span, ty);
|
||||
let collect = wrap_expr_with_collect(working_set, subexpression);
|
||||
|
||||
block.pipelines.push(Pipeline {
|
||||
elements: vec![PipelineElement {
|
||||
pipe: None,
|
||||
expr: collect,
|
||||
redirection: None,
|
||||
}],
|
||||
});
|
||||
}
|
||||
|
||||
if scoped {
|
||||
working_set.exit_scope();
|
||||
}
|
||||
|
||||
block.span = Some(span);
|
||||
|
||||
let errors = type_check::check_block_input_output(working_set, &block);
|
||||
if !errors.is_empty() {
|
||||
working_set.parse_errors.extend_from_slice(&errors);
|
||||
|
@ -6319,6 +6292,10 @@ pub fn discover_captures_in_expr(
|
|||
discover_captures_in_expr(working_set, &match_.1, seen, seen_blocks, output)?;
|
||||
}
|
||||
}
|
||||
Expr::Collect(var_id, expr) => {
|
||||
seen.push(*var_id);
|
||||
discover_captures_in_expr(working_set, expr, seen, seen_blocks, output)?
|
||||
}
|
||||
Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => {
|
||||
let block = working_set.get_block(*block_id);
|
||||
|
||||
|
@ -6369,28 +6346,28 @@ pub fn discover_captures_in_expr(
|
|||
|
||||
fn wrap_redirection_with_collect(
|
||||
working_set: &mut StateWorkingSet,
|
||||
target: &RedirectionTarget,
|
||||
target: RedirectionTarget,
|
||||
) -> RedirectionTarget {
|
||||
match target {
|
||||
RedirectionTarget::File { expr, append, span } => RedirectionTarget::File {
|
||||
expr: wrap_expr_with_collect(working_set, expr),
|
||||
span: *span,
|
||||
append: *append,
|
||||
span,
|
||||
append,
|
||||
},
|
||||
RedirectionTarget::Pipe { span } => RedirectionTarget::Pipe { span: *span },
|
||||
RedirectionTarget::Pipe { span } => RedirectionTarget::Pipe { span },
|
||||
}
|
||||
}
|
||||
|
||||
fn wrap_element_with_collect(
|
||||
working_set: &mut StateWorkingSet,
|
||||
element: &PipelineElement,
|
||||
element: PipelineElement,
|
||||
) -> PipelineElement {
|
||||
PipelineElement {
|
||||
pipe: element.pipe,
|
||||
expr: wrap_expr_with_collect(working_set, &element.expr),
|
||||
redirection: element.redirection.as_ref().map(|r| match r {
|
||||
expr: wrap_expr_with_collect(working_set, element.expr),
|
||||
redirection: element.redirection.map(|r| match r {
|
||||
PipelineRedirection::Single { source, target } => PipelineRedirection::Single {
|
||||
source: *source,
|
||||
source,
|
||||
target: wrap_redirection_with_collect(working_set, target),
|
||||
},
|
||||
PipelineRedirection::Separate { out, err } => PipelineRedirection::Separate {
|
||||
|
@ -6401,65 +6378,24 @@ fn wrap_element_with_collect(
|
|||
}
|
||||
}
|
||||
|
||||
fn wrap_expr_with_collect(working_set: &mut StateWorkingSet, expr: &Expression) -> Expression {
|
||||
fn wrap_expr_with_collect(working_set: &mut StateWorkingSet, expr: Expression) -> Expression {
|
||||
let span = expr.span;
|
||||
|
||||
if let Some(decl_id) = working_set.find_decl(b"collect") {
|
||||
let mut output = vec![];
|
||||
// IN_VARIABLE_ID should get replaced with a unique variable, so that we don't have to
|
||||
// execute as a closure
|
||||
let var_id = working_set.add_variable(b"$in".into(), expr.span, Type::Any, false);
|
||||
let mut expr = expr.clone();
|
||||
expr.replace_in_variable(working_set, var_id);
|
||||
|
||||
let var_id = IN_VARIABLE_ID;
|
||||
let mut signature = Signature::new("");
|
||||
signature.required_positional.push(PositionalArg {
|
||||
var_id: Some(var_id),
|
||||
name: "$in".into(),
|
||||
desc: String::new(),
|
||||
shape: SyntaxShape::Any,
|
||||
default_value: None,
|
||||
});
|
||||
|
||||
let mut block = Block {
|
||||
pipelines: vec![Pipeline::from_vec(vec![expr.clone()])],
|
||||
signature: Box::new(signature),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
compile_block(working_set, &mut block);
|
||||
|
||||
let block_id = working_set.add_block(Arc::new(block));
|
||||
|
||||
output.push(Argument::Positional(Expression::new(
|
||||
working_set,
|
||||
Expr::Closure(block_id),
|
||||
span,
|
||||
Type::Any,
|
||||
)));
|
||||
|
||||
output.push(Argument::Named((
|
||||
Spanned {
|
||||
item: "keep-env".to_string(),
|
||||
span: Span::new(0, 0),
|
||||
},
|
||||
None,
|
||||
None,
|
||||
)));
|
||||
|
||||
// The containing, synthetic call to `collect`.
|
||||
// We don't want to have a real span as it will confuse flattening
|
||||
// The args are where we'll get the real info
|
||||
Expression::new(
|
||||
working_set,
|
||||
Expr::Call(Box::new(Call {
|
||||
head: Span::new(0, 0),
|
||||
arguments: output,
|
||||
decl_id,
|
||||
parser_info: HashMap::new(),
|
||||
})),
|
||||
span,
|
||||
Type::Any,
|
||||
)
|
||||
} else {
|
||||
Expression::garbage(working_set, span)
|
||||
}
|
||||
// Bind the custom `$in` variable for that particular expression
|
||||
let ty = expr.ty.clone();
|
||||
Expression::new(
|
||||
working_set,
|
||||
Expr::Collect(var_id, Box::new(expr)),
|
||||
span,
|
||||
// We can expect it to have the same result type
|
||||
ty,
|
||||
)
|
||||
}
|
||||
|
||||
// Parses a vector of u8 to create an AST Block. If a file name is given, then
|
||||
|
|
|
@ -41,6 +41,41 @@ impl Command for Let {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[derive(Clone)]
|
||||
pub struct Mut;
|
||||
|
||||
#[cfg(test)]
|
||||
impl Command for Mut {
|
||||
fn name(&self) -> &str {
|
||||
"mut"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Mock mut command."
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("mut")
|
||||
.required("var_name", SyntaxShape::VarWithOptType, "variable name")
|
||||
.required(
|
||||
"initial_value",
|
||||
SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)),
|
||||
"equals sign followed by value",
|
||||
)
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
_call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
fn test_int(
|
||||
test_tag: &str, // name of sub-test
|
||||
test: &[u8], // input expression
|
||||
|
@ -1147,12 +1182,28 @@ fn test_redirection_with_let(#[case] phase: &[u8]) {
|
|||
let engine_state = EngineState::new();
|
||||
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||
working_set.add_decl(Box::new(Let));
|
||||
let _block = parse(&mut working_set, None, phase, true);
|
||||
working_set.add_decl(Box::new(Mut));
|
||||
|
||||
let block = parse(&mut working_set, None, phase, true);
|
||||
assert!(
|
||||
working_set.parse_errors.is_empty(),
|
||||
"parse errors: {:?}",
|
||||
working_set.parse_errors
|
||||
);
|
||||
assert_eq!(1, block.pipelines[0].elements.len());
|
||||
|
||||
let element = &block.pipelines[0].elements[0];
|
||||
assert!(element.redirection.is_none()); // it should be in the let block, not here
|
||||
|
||||
if let Expr::Call(call) = &element.expr.expr {
|
||||
let arg = call.positional_nth(1).expect("no positional args");
|
||||
let block_id = arg.as_block().expect("arg 1 is not a block");
|
||||
let block = working_set.get_block(block_id);
|
||||
let inner_element = &block.pipelines[0].elements[0];
|
||||
assert!(inner_element.redirection.is_some());
|
||||
} else {
|
||||
panic!("expected Call: {:?}", block.pipelines[0].elements[0])
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
|
|
|
@ -12,10 +12,10 @@ exclude = ["/fuzz"]
|
|||
bench = false
|
||||
|
||||
[dependencies]
|
||||
dirs-next = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
omnipath = { workspace = true }
|
||||
|
||||
[target.'cfg(all(unix, not(target_os = "macos"), not(target_os = "android")))'.dependencies]
|
||||
pwd = { workspace = true }
|
||||
pwd = { workspace = true }
|
||||
|
|
|
@ -3,14 +3,14 @@ use omnipath::WinPathExt;
|
|||
use std::path::PathBuf;
|
||||
|
||||
pub fn home_dir() -> Option<PathBuf> {
|
||||
dirs_next::home_dir()
|
||||
dirs::home_dir()
|
||||
}
|
||||
|
||||
/// Return the data directory for the current platform or XDG_DATA_HOME if specified.
|
||||
pub fn data_dir() -> Option<PathBuf> {
|
||||
match std::env::var("XDG_DATA_HOME").map(PathBuf::from) {
|
||||
Ok(xdg_data) if xdg_data.is_absolute() => Some(canonicalize(&xdg_data).unwrap_or(xdg_data)),
|
||||
_ => get_canonicalized_path(dirs_next::data_dir()),
|
||||
_ => get_canonicalized_path(dirs::data_dir()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ pub fn cache_dir() -> Option<PathBuf> {
|
|||
Ok(xdg_cache) if xdg_cache.is_absolute() => {
|
||||
Some(canonicalize(&xdg_cache).unwrap_or(xdg_cache))
|
||||
}
|
||||
_ => get_canonicalized_path(dirs_next::cache_dir()),
|
||||
_ => get_canonicalized_path(dirs::cache_dir()),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,7 +30,7 @@ pub fn config_dir() -> Option<PathBuf> {
|
|||
Ok(xdg_config) if xdg_config.is_absolute() => {
|
||||
Some(canonicalize(&xdg_config).unwrap_or(xdg_config))
|
||||
}
|
||||
_ => get_canonicalized_path(dirs_next::config_dir()),
|
||||
_ => get_canonicalized_path(dirs::config_dir()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,7 @@ fn user_home_dir(username: &str) -> PathBuf {
|
|||
fn user_home_dir(username: &str) -> PathBuf {
|
||||
use std::path::Component;
|
||||
|
||||
match dirs_next::home_dir() {
|
||||
match dirs::home_dir() {
|
||||
None => {
|
||||
// Termux always has the same home directory
|
||||
#[cfg(target_os = "android")]
|
||||
|
@ -145,7 +145,7 @@ fn expand_tilde_with_another_user_home(path: &Path) -> PathBuf {
|
|||
/// Expand tilde ("~") into a home directory if it is the first path component
|
||||
pub fn expand_tilde(path: impl AsRef<Path>) -> PathBuf {
|
||||
// TODO: Extend this to work with "~user" style of home paths
|
||||
expand_tilde_with_home(path, dirs_next::home_dir())
|
||||
expand_tilde_with_home(path, dirs::home_dir())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use crate::util::MutableCow;
|
||||
use nu_engine::{get_eval_block_with_early_return, get_full_help, ClosureEvalOnce};
|
||||
use nu_plugin_protocol::EvaluatedCall;
|
||||
use nu_protocol::{
|
||||
engine::{Call, Closure, EngineState, Redirection, Stack},
|
||||
Config, IntoSpanned, OutDest, PipelineData, PluginIdentity, ShellError, Signals, Span, Spanned,
|
||||
Value,
|
||||
ir, Config, DeclId, IntoSpanned, OutDest, PipelineData, PluginIdentity, ShellError, Signals,
|
||||
Span, Spanned, Value,
|
||||
};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
|
@ -44,6 +45,17 @@ pub trait PluginExecutionContext: Send + Sync {
|
|||
redirect_stdout: bool,
|
||||
redirect_stderr: bool,
|
||||
) -> Result<PipelineData, ShellError>;
|
||||
/// Find a declaration by name
|
||||
fn find_decl(&self, name: &str) -> Result<Option<DeclId>, ShellError>;
|
||||
/// Call a declaration with arguments and input
|
||||
fn call_decl(
|
||||
&mut self,
|
||||
decl_id: DeclId,
|
||||
call: EvaluatedCall,
|
||||
input: PipelineData,
|
||||
redirect_stdout: bool,
|
||||
redirect_stderr: bool,
|
||||
) -> Result<PipelineData, ShellError>;
|
||||
/// Create an owned version of the context with `'static` lifetime
|
||||
fn boxed(&self) -> Box<dyn PluginExecutionContext>;
|
||||
}
|
||||
|
@ -177,19 +189,10 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> {
|
|||
.captures_to_stack(closure.item.captures)
|
||||
.reset_pipes();
|
||||
|
||||
let stdout = if redirect_stdout {
|
||||
Some(Redirection::Pipe(OutDest::Capture))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let stderr = if redirect_stderr {
|
||||
Some(Redirection::Pipe(OutDest::Capture))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let stack = &mut stack.push_redirection(stdout, stderr);
|
||||
let stack = &mut stack.push_redirection(
|
||||
redirect_stdout.then_some(Redirection::Pipe(OutDest::Capture)),
|
||||
redirect_stderr.then_some(Redirection::Pipe(OutDest::Capture)),
|
||||
);
|
||||
|
||||
// Set up the positional arguments
|
||||
for (idx, value) in positional.into_iter().enumerate() {
|
||||
|
@ -211,6 +214,57 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> {
|
|||
eval_block_with_early_return(&self.engine_state, stack, block, input)
|
||||
}
|
||||
|
||||
fn find_decl(&self, name: &str) -> Result<Option<DeclId>, ShellError> {
|
||||
Ok(self.engine_state.find_decl(name.as_bytes(), &[]))
|
||||
}
|
||||
|
||||
fn call_decl(
|
||||
&mut self,
|
||||
decl_id: DeclId,
|
||||
call: EvaluatedCall,
|
||||
input: PipelineData,
|
||||
redirect_stdout: bool,
|
||||
redirect_stderr: bool,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
if decl_id >= self.engine_state.num_decls() {
|
||||
return Err(ShellError::GenericError {
|
||||
error: "Plugin misbehaving".into(),
|
||||
msg: format!("Tried to call unknown decl id: {}", decl_id),
|
||||
span: Some(call.head),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
});
|
||||
}
|
||||
|
||||
let decl = self.engine_state.get_decl(decl_id);
|
||||
|
||||
let stack = &mut self.stack.push_redirection(
|
||||
redirect_stdout.then_some(Redirection::Pipe(OutDest::Capture)),
|
||||
redirect_stderr.then_some(Redirection::Pipe(OutDest::Capture)),
|
||||
);
|
||||
|
||||
let mut call_builder = ir::Call::build(decl_id, call.head);
|
||||
|
||||
for positional in call.positional {
|
||||
call_builder.add_positional(stack, positional.span(), positional);
|
||||
}
|
||||
|
||||
for (name, value) in call.named {
|
||||
if let Some(value) = value {
|
||||
call_builder.add_named(stack, &name.item, "", name.span, value);
|
||||
} else {
|
||||
call_builder.add_flag(stack, &name.item, "", name.span);
|
||||
}
|
||||
}
|
||||
|
||||
decl.run(
|
||||
&self.engine_state,
|
||||
stack,
|
||||
&(&call_builder.finish()).into(),
|
||||
input,
|
||||
)
|
||||
}
|
||||
|
||||
fn boxed(&self) -> Box<dyn PluginExecutionContext + 'static> {
|
||||
Box::new(PluginExecutionCommandContext {
|
||||
identity: self.identity.clone(),
|
||||
|
@ -298,6 +352,25 @@ impl PluginExecutionContext for PluginExecutionBogusContext {
|
|||
})
|
||||
}
|
||||
|
||||
fn find_decl(&self, _name: &str) -> Result<Option<DeclId>, ShellError> {
|
||||
Err(ShellError::NushellFailed {
|
||||
msg: "find_decl not implemented on bogus".into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn call_decl(
|
||||
&mut self,
|
||||
_decl_id: DeclId,
|
||||
_call: EvaluatedCall,
|
||||
_input: PipelineData,
|
||||
_redirect_stdout: bool,
|
||||
_redirect_stderr: bool,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
Err(ShellError::NushellFailed {
|
||||
msg: "call_decl not implemented on bogus".into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn boxed(&self) -> Box<dyn PluginExecutionContext + 'static> {
|
||||
Box::new(PluginExecutionBogusContext)
|
||||
}
|
||||
|
|
|
@ -1316,6 +1316,22 @@ pub(crate) fn handle_engine_call(
|
|||
} => context
|
||||
.eval_closure(closure, positional, input, redirect_stdout, redirect_stderr)
|
||||
.map(EngineCallResponse::PipelineData),
|
||||
EngineCall::FindDecl(name) => context.find_decl(&name).map(|decl_id| {
|
||||
if let Some(decl_id) = decl_id {
|
||||
EngineCallResponse::Identifier(decl_id)
|
||||
} else {
|
||||
EngineCallResponse::empty()
|
||||
}
|
||||
}),
|
||||
EngineCall::CallDecl {
|
||||
decl_id,
|
||||
call,
|
||||
input,
|
||||
redirect_stdout,
|
||||
redirect_stderr,
|
||||
} => context
|
||||
.call_decl(decl_id, call, input, redirect_stdout, redirect_stderr)
|
||||
.map(EngineCallResponse::PipelineData),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,6 +27,82 @@ pub struct EvaluatedCall {
|
|||
}
|
||||
|
||||
impl EvaluatedCall {
|
||||
/// Create a new [`EvaluatedCall`] with the given head span.
|
||||
pub fn new(head: Span) -> EvaluatedCall {
|
||||
EvaluatedCall {
|
||||
head,
|
||||
positional: vec![],
|
||||
named: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a positional argument to an [`EvaluatedCall`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use nu_protocol::{Value, Span, IntoSpanned};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let head = Span::test_data();
|
||||
/// let mut call = EvaluatedCall::new(head);
|
||||
/// call.add_positional(Value::test_int(1337));
|
||||
/// ```
|
||||
pub fn add_positional(&mut self, value: Value) -> &mut Self {
|
||||
self.positional.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a named argument to an [`EvaluatedCall`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use nu_protocol::{Value, Span, IntoSpanned};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let head = Span::test_data();
|
||||
/// let mut call = EvaluatedCall::new(head);
|
||||
/// call.add_named("foo".into_spanned(head), Value::test_string("bar"));
|
||||
/// ```
|
||||
pub fn add_named(&mut self, name: Spanned<impl Into<String>>, value: Value) -> &mut Self {
|
||||
self.named.push((name.map(Into::into), Some(value)));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a flag argument to an [`EvaluatedCall`]. A flag argument is a named argument with no
|
||||
/// value.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use nu_protocol::{Value, Span, IntoSpanned};
|
||||
/// # use nu_plugin_protocol::EvaluatedCall;
|
||||
/// # let head = Span::test_data();
|
||||
/// let mut call = EvaluatedCall::new(head);
|
||||
/// call.add_flag("pretty".into_spanned(head));
|
||||
/// ```
|
||||
pub fn add_flag(&mut self, name: Spanned<impl Into<String>>) -> &mut Self {
|
||||
self.named.push((name.map(Into::into), None));
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder variant of [`.add_positional()`].
|
||||
pub fn with_positional(mut self, value: Value) -> Self {
|
||||
self.add_positional(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder variant of [`.add_named()`].
|
||||
pub fn with_named(mut self, name: Spanned<impl Into<String>>, value: Value) -> Self {
|
||||
self.add_named(name, value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Builder variant of [`.add_flag()`].
|
||||
pub fn with_flag(mut self, name: Spanned<impl Into<String>>) -> Self {
|
||||
self.add_flag(name);
|
||||
self
|
||||
}
|
||||
|
||||
/// Try to create an [`EvaluatedCall`] from a command `Call`.
|
||||
pub fn try_from_call(
|
||||
call: &Call,
|
||||
|
@ -192,6 +268,16 @@ impl EvaluatedCall {
|
|||
Ok(false)
|
||||
}
|
||||
|
||||
/// Returns the [`Span`] of the name of an optional named argument.
|
||||
///
|
||||
/// This can be used in errors for named arguments that don't take values.
|
||||
pub fn get_flag_span(&self, flag_name: &str) -> Option<Span> {
|
||||
self.named
|
||||
.iter()
|
||||
.find(|(name, _)| name.item == flag_name)
|
||||
.map(|(name, _)| name.span)
|
||||
}
|
||||
|
||||
/// Returns the [`Value`] of an optional named argument
|
||||
///
|
||||
/// # Examples
|
||||
|
|
|
@ -22,7 +22,7 @@ mod tests;
|
|||
pub mod test_util;
|
||||
|
||||
use nu_protocol::{
|
||||
ast::Operator, engine::Closure, ByteStreamType, Config, LabeledError, PipelineData,
|
||||
ast::Operator, engine::Closure, ByteStreamType, Config, DeclId, LabeledError, PipelineData,
|
||||
PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value,
|
||||
};
|
||||
use nu_utils::SharedCow;
|
||||
|
@ -494,6 +494,21 @@ pub enum EngineCall<D> {
|
|||
/// Whether to redirect stderr from external commands
|
||||
redirect_stderr: bool,
|
||||
},
|
||||
/// Find a declaration by name
|
||||
FindDecl(String),
|
||||
/// Call a declaration with args
|
||||
CallDecl {
|
||||
/// The id of the declaration to be called (can be found with `FindDecl`)
|
||||
decl_id: DeclId,
|
||||
/// Information about the call (head span, arguments, etc.)
|
||||
call: EvaluatedCall,
|
||||
/// Pipeline input to the call
|
||||
input: D,
|
||||
/// Whether to redirect stdout from external commands
|
||||
redirect_stdout: bool,
|
||||
/// Whether to redirect stderr from external commands
|
||||
redirect_stderr: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl<D> EngineCall<D> {
|
||||
|
@ -511,6 +526,8 @@ impl<D> EngineCall<D> {
|
|||
EngineCall::LeaveForeground => "LeaveForeground",
|
||||
EngineCall::GetSpanContents(_) => "GetSpanContents",
|
||||
EngineCall::EvalClosure { .. } => "EvalClosure",
|
||||
EngineCall::FindDecl(_) => "FindDecl",
|
||||
EngineCall::CallDecl { .. } => "CallDecl",
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -544,6 +561,20 @@ impl<D> EngineCall<D> {
|
|||
redirect_stdout,
|
||||
redirect_stderr,
|
||||
},
|
||||
EngineCall::FindDecl(name) => EngineCall::FindDecl(name),
|
||||
EngineCall::CallDecl {
|
||||
decl_id,
|
||||
call,
|
||||
input,
|
||||
redirect_stdout,
|
||||
redirect_stderr,
|
||||
} => EngineCall::CallDecl {
|
||||
decl_id,
|
||||
call,
|
||||
input: f(input)?,
|
||||
redirect_stdout,
|
||||
redirect_stderr,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -556,6 +587,7 @@ pub enum EngineCallResponse<D> {
|
|||
PipelineData(D),
|
||||
Config(SharedCow<Config>),
|
||||
ValueMap(HashMap<String, Value>),
|
||||
Identifier(usize),
|
||||
}
|
||||
|
||||
impl<D> EngineCallResponse<D> {
|
||||
|
@ -570,6 +602,7 @@ impl<D> EngineCallResponse<D> {
|
|||
EngineCallResponse::PipelineData(data) => EngineCallResponse::PipelineData(f(data)?),
|
||||
EngineCallResponse::Config(config) => EngineCallResponse::Config(config),
|
||||
EngineCallResponse::ValueMap(map) => EngineCallResponse::ValueMap(map),
|
||||
EngineCallResponse::Identifier(id) => EngineCallResponse::Identifier(id),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,12 @@ use nu_plugin_core::{
|
|||
StreamManagerHandle,
|
||||
};
|
||||
use nu_plugin_protocol::{
|
||||
CallInfo, CustomValueOp, EngineCall, EngineCallId, EngineCallResponse, Ordering, PluginCall,
|
||||
PluginCallId, PluginCallResponse, PluginCustomValue, PluginInput, PluginOption, PluginOutput,
|
||||
ProtocolInfo,
|
||||
CallInfo, CustomValueOp, EngineCall, EngineCallId, EngineCallResponse, EvaluatedCall, Ordering,
|
||||
PluginCall, PluginCallId, PluginCallResponse, PluginCustomValue, PluginInput, PluginOption,
|
||||
PluginOutput, ProtocolInfo,
|
||||
};
|
||||
use nu_protocol::{
|
||||
engine::Closure, Config, LabeledError, PipelineData, PluginMetadata, PluginSignature,
|
||||
engine::Closure, Config, DeclId, LabeledError, PipelineData, PluginMetadata, PluginSignature,
|
||||
ShellError, Signals, Span, Spanned, Value,
|
||||
};
|
||||
use nu_utils::SharedCow;
|
||||
|
@ -872,6 +872,71 @@ impl EngineInterface {
|
|||
}
|
||||
}
|
||||
|
||||
/// Ask the engine for the identifier for a declaration. If found, the result can then be passed
|
||||
/// to [`.call_decl()`] to call other internal commands.
|
||||
///
|
||||
/// See [`.call_decl()`] for an example.
|
||||
pub fn find_decl(&self, name: impl Into<String>) -> Result<Option<DeclId>, ShellError> {
|
||||
let call = EngineCall::FindDecl(name.into());
|
||||
|
||||
match self.engine_call(call)? {
|
||||
EngineCallResponse::Error(err) => Err(err),
|
||||
EngineCallResponse::Identifier(id) => Ok(Some(id)),
|
||||
EngineCallResponse::PipelineData(PipelineData::Empty) => Ok(None),
|
||||
_ => Err(ShellError::PluginFailedToDecode {
|
||||
msg: "Received unexpected response type for EngineCall::FindDecl".into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask the engine to call an internal command, using the declaration ID previously looked up
|
||||
/// with [`.find_decl()`].
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use nu_protocol::{Value, ShellError, PipelineData};
|
||||
/// # use nu_plugin::{EngineInterface, EvaluatedCall};
|
||||
/// # fn example(engine: &EngineInterface, call: &EvaluatedCall) -> Result<Value, ShellError> {
|
||||
/// if let Some(decl_id) = engine.find_decl("scope commands")? {
|
||||
/// let commands = engine.call_decl(
|
||||
/// decl_id,
|
||||
/// EvaluatedCall::new(call.head),
|
||||
/// PipelineData::Empty,
|
||||
/// true,
|
||||
/// false,
|
||||
/// )?;
|
||||
/// commands.into_value(call.head)
|
||||
/// } else {
|
||||
/// Ok(Value::list(vec![], call.head))
|
||||
/// }
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn call_decl(
|
||||
&self,
|
||||
decl_id: DeclId,
|
||||
call: EvaluatedCall,
|
||||
input: PipelineData,
|
||||
redirect_stdout: bool,
|
||||
redirect_stderr: bool,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let call = EngineCall::CallDecl {
|
||||
decl_id,
|
||||
call,
|
||||
input,
|
||||
redirect_stdout,
|
||||
redirect_stderr,
|
||||
};
|
||||
|
||||
match self.engine_call(call)? {
|
||||
EngineCallResponse::Error(err) => Err(err),
|
||||
EngineCallResponse::PipelineData(data) => Ok(data),
|
||||
_ => Err(ShellError::PluginFailedToDecode {
|
||||
msg: "Received unexpected response type for EngineCall::CallDecl".into(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tell the engine whether to disable garbage collection for this plugin.
|
||||
///
|
||||
/// The garbage collector is enabled by default, but plugins can turn it off (ideally
|
||||
|
|
|
@ -23,6 +23,7 @@ byte-unit = { version = "5.1", features = [ "serde" ] }
|
|||
chrono = { workspace = true, features = [ "serde", "std", "unstable-locales" ], default-features = false }
|
||||
chrono-humanize = { workspace = true }
|
||||
convert_case = { workspace = true }
|
||||
dirs = { workspace = true }
|
||||
fancy-regex = { workspace = true }
|
||||
indexmap = { workspace = true }
|
||||
lru = { workspace = true }
|
||||
|
@ -38,6 +39,10 @@ log = { workspace = true }
|
|||
[target.'cfg(unix)'.dependencies]
|
||||
nix = { workspace = true, default-features = false, features = ["signal"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
dirs-sys = { workspace = true }
|
||||
windows-sys = { workspace = true }
|
||||
|
||||
[features]
|
||||
plugin = [
|
||||
"brotli",
|
||||
|
|
|
@ -78,6 +78,19 @@ impl Block {
|
|||
Type::Nothing
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace any `$in` variables in the initial element of pipelines within the block
|
||||
pub fn replace_in_variable(
|
||||
&mut self,
|
||||
working_set: &mut StateWorkingSet<'_>,
|
||||
new_var_id: VarId,
|
||||
) {
|
||||
for pipeline in self.pipelines.iter_mut() {
|
||||
if let Some(element) = pipeline.elements.first_mut() {
|
||||
element.replace_in_variable(working_set, new_var_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Block
|
||||
|
|
|
@ -110,17 +110,11 @@ impl Call {
|
|||
/// If there are one or more arguments the span encompasses the start of the first argument to
|
||||
/// end of the last argument
|
||||
pub fn arguments_span(&self) -> Span {
|
||||
let past = self.head.past();
|
||||
|
||||
let start = self
|
||||
.arguments
|
||||
.first()
|
||||
.map(|a| a.span())
|
||||
.unwrap_or(past)
|
||||
.start;
|
||||
let end = self.arguments.last().map(|a| a.span()).unwrap_or(past).end;
|
||||
|
||||
Span::new(start, end)
|
||||
if self.arguments.is_empty() {
|
||||
self.head.past()
|
||||
} else {
|
||||
Span::merge_many(self.arguments.iter().map(|a| a.span()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn named_iter(
|
||||
|
@ -341,27 +335,7 @@ impl Call {
|
|||
}
|
||||
|
||||
pub fn span(&self) -> Span {
|
||||
let mut span = self.head;
|
||||
|
||||
for positional in self.positional_iter() {
|
||||
if positional.span.end > span.end {
|
||||
span.end = positional.span.end;
|
||||
}
|
||||
}
|
||||
|
||||
for (named, _, val) in self.named_iter() {
|
||||
if named.span.end > span.end {
|
||||
span.end = named.span.end;
|
||||
}
|
||||
|
||||
if let Some(val) = &val {
|
||||
if val.span.end > span.end {
|
||||
span.end = val.span.end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span
|
||||
self.head.merge(self.arguments_span())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ pub enum Expr {
|
|||
RowCondition(BlockId),
|
||||
UnaryNot(Box<Expression>),
|
||||
BinaryOp(Box<Expression>, Box<Expression>, Box<Expression>), //lhs, op, rhs
|
||||
Collect(VarId, Box<Expression>),
|
||||
Subexpression(BlockId),
|
||||
Block(BlockId),
|
||||
Closure(BlockId),
|
||||
|
@ -65,11 +66,13 @@ impl Expr {
|
|||
&self,
|
||||
working_set: &StateWorkingSet,
|
||||
) -> (Option<OutDest>, Option<OutDest>) {
|
||||
// Usages of `$in` will be wrapped by a `collect` call by the parser,
|
||||
// so we do not have to worry about that when considering
|
||||
// which of the expressions below may consume pipeline output.
|
||||
match self {
|
||||
Expr::Call(call) => working_set.get_decl(call.decl_id).pipe_redirection(),
|
||||
Expr::Collect(_, _) => {
|
||||
// A collect expression always has default redirection, it's just going to collect
|
||||
// stdout unless another type of redirection is specified
|
||||
(None, None)
|
||||
},
|
||||
Expr::Subexpression(block_id) | Expr::Block(block_id) => working_set
|
||||
.get_block(*block_id)
|
||||
.pipe_redirection(working_set),
|
||||
|
|
|
@ -6,6 +6,8 @@ use crate::{
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::ListItem;
|
||||
|
||||
/// Wrapper around [`Expr`]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Expression {
|
||||
|
@ -106,37 +108,14 @@ impl Expression {
|
|||
left.has_in_variable(working_set) || right.has_in_variable(working_set)
|
||||
}
|
||||
Expr::UnaryNot(expr) => expr.has_in_variable(working_set),
|
||||
Expr::Block(block_id) => {
|
||||
Expr::Block(block_id) | Expr::Closure(block_id) => {
|
||||
let block = working_set.get_block(*block_id);
|
||||
|
||||
if block.captures.contains(&IN_VARIABLE_ID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(pipeline) = block.pipelines.first() {
|
||||
match pipeline.elements.first() {
|
||||
Some(element) => element.has_in_variable(working_set),
|
||||
None => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
Expr::Closure(block_id) => {
|
||||
let block = working_set.get_block(*block_id);
|
||||
|
||||
if block.captures.contains(&IN_VARIABLE_ID) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(pipeline) = block.pipelines.first() {
|
||||
match pipeline.elements.first() {
|
||||
Some(element) => element.has_in_variable(working_set),
|
||||
None => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
block.captures.contains(&IN_VARIABLE_ID)
|
||||
|| block
|
||||
.pipelines
|
||||
.iter()
|
||||
.flat_map(|pipeline| pipeline.elements.first())
|
||||
.any(|element| element.has_in_variable(working_set))
|
||||
}
|
||||
Expr::Binary(_) => false,
|
||||
Expr::Bool(_) => false,
|
||||
|
@ -251,6 +230,9 @@ impl Expression {
|
|||
Expr::Signature(_) => false,
|
||||
Expr::String(_) => false,
|
||||
Expr::RawString(_) => false,
|
||||
// A `$in` variable found within a `Collect` is local, as it's already been wrapped
|
||||
// This is probably unlikely to happen anyway - the expressions are wrapped depth-first
|
||||
Expr::Collect(_, _) => false,
|
||||
Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => {
|
||||
let block = working_set.get_block(*block_id);
|
||||
|
||||
|
@ -414,6 +396,7 @@ impl Expression {
|
|||
i.replace_span(working_set, replaced, new_span)
|
||||
}
|
||||
}
|
||||
Expr::Collect(_, expr) => expr.replace_span(working_set, replaced, new_span),
|
||||
Expr::RowCondition(block_id) | Expr::Subexpression(block_id) => {
|
||||
let mut block = (**working_set.get_block(*block_id)).clone();
|
||||
|
||||
|
@ -443,6 +426,129 @@ impl Expression {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn replace_in_variable(&mut self, working_set: &mut StateWorkingSet, new_var_id: VarId) {
|
||||
match &mut self.expr {
|
||||
Expr::Bool(_) => {}
|
||||
Expr::Int(_) => {}
|
||||
Expr::Float(_) => {}
|
||||
Expr::Binary(_) => {}
|
||||
Expr::Range(_) => {}
|
||||
Expr::Var(var_id) | Expr::VarDecl(var_id) => {
|
||||
if *var_id == IN_VARIABLE_ID {
|
||||
*var_id = new_var_id;
|
||||
}
|
||||
}
|
||||
Expr::Call(call) => {
|
||||
for arg in call.arguments.iter_mut() {
|
||||
match arg {
|
||||
Argument::Positional(expr)
|
||||
| Argument::Unknown(expr)
|
||||
| Argument::Named((_, _, Some(expr)))
|
||||
| Argument::Spread(expr) => {
|
||||
expr.replace_in_variable(working_set, new_var_id)
|
||||
}
|
||||
Argument::Named((_, _, None)) => {}
|
||||
}
|
||||
}
|
||||
for expr in call.parser_info.values_mut() {
|
||||
expr.replace_in_variable(working_set, new_var_id)
|
||||
}
|
||||
}
|
||||
Expr::ExternalCall(head, args) => {
|
||||
head.replace_in_variable(working_set, new_var_id);
|
||||
for arg in args.iter_mut() {
|
||||
match arg {
|
||||
ExternalArgument::Regular(expr) | ExternalArgument::Spread(expr) => {
|
||||
expr.replace_in_variable(working_set, new_var_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Operator(_) => {}
|
||||
// `$in` in `Collect` has already been handled, so we don't need to check further
|
||||
Expr::Collect(_, _) => {}
|
||||
Expr::Block(block_id)
|
||||
| Expr::Closure(block_id)
|
||||
| Expr::RowCondition(block_id)
|
||||
| Expr::Subexpression(block_id) => {
|
||||
let mut block = Block::clone(working_set.get_block(*block_id));
|
||||
block.replace_in_variable(working_set, new_var_id);
|
||||
*working_set.get_block_mut(*block_id) = block;
|
||||
}
|
||||
Expr::UnaryNot(expr) => {
|
||||
expr.replace_in_variable(working_set, new_var_id);
|
||||
}
|
||||
Expr::BinaryOp(lhs, op, rhs) => {
|
||||
for expr in [lhs, op, rhs] {
|
||||
expr.replace_in_variable(working_set, new_var_id);
|
||||
}
|
||||
}
|
||||
Expr::MatchBlock(match_patterns) => {
|
||||
for (_, expr) in match_patterns.iter_mut() {
|
||||
expr.replace_in_variable(working_set, new_var_id);
|
||||
}
|
||||
}
|
||||
Expr::List(items) => {
|
||||
for item in items.iter_mut() {
|
||||
match item {
|
||||
ListItem::Item(expr) | ListItem::Spread(_, expr) => {
|
||||
expr.replace_in_variable(working_set, new_var_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Table(table) => {
|
||||
for col_expr in table.columns.iter_mut() {
|
||||
col_expr.replace_in_variable(working_set, new_var_id);
|
||||
}
|
||||
for row in table.rows.iter_mut() {
|
||||
for row_expr in row.iter_mut() {
|
||||
row_expr.replace_in_variable(working_set, new_var_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Record(items) => {
|
||||
for item in items.iter_mut() {
|
||||
match item {
|
||||
RecordItem::Pair(key, val) => {
|
||||
key.replace_in_variable(working_set, new_var_id);
|
||||
val.replace_in_variable(working_set, new_var_id);
|
||||
}
|
||||
RecordItem::Spread(_, expr) => {
|
||||
expr.replace_in_variable(working_set, new_var_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Expr::Keyword(kw) => kw.expr.replace_in_variable(working_set, new_var_id),
|
||||
Expr::ValueWithUnit(value_with_unit) => value_with_unit
|
||||
.expr
|
||||
.replace_in_variable(working_set, new_var_id),
|
||||
Expr::DateTime(_) => {}
|
||||
Expr::Filepath(_, _) => {}
|
||||
Expr::Directory(_, _) => {}
|
||||
Expr::GlobPattern(_, _) => {}
|
||||
Expr::String(_) => {}
|
||||
Expr::RawString(_) => {}
|
||||
Expr::CellPath(_) => {}
|
||||
Expr::FullCellPath(full_cell_path) => {
|
||||
full_cell_path
|
||||
.head
|
||||
.replace_in_variable(working_set, new_var_id);
|
||||
}
|
||||
Expr::ImportPattern(_) => {}
|
||||
Expr::Overlay(_) => {}
|
||||
Expr::Signature(_) => {}
|
||||
Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => {
|
||||
for expr in exprs.iter_mut() {
|
||||
expr.replace_in_variable(working_set, new_var_id);
|
||||
}
|
||||
}
|
||||
Expr::Nothing => {}
|
||||
Expr::Garbage => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(working_set: &mut StateWorkingSet, expr: Expr, span: Span, ty: Type) -> Expression {
|
||||
let span_id = working_set.add_span(span);
|
||||
Expression {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{ast::Expression, engine::StateWorkingSet, OutDest, Span};
|
||||
use crate::{ast::Expression, engine::StateWorkingSet, OutDest, Span, VarId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
|
||||
|
@ -62,6 +62,19 @@ impl RedirectionTarget {
|
|||
RedirectionTarget::Pipe { .. } => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replace_in_variable(
|
||||
&mut self,
|
||||
working_set: &mut StateWorkingSet<'_>,
|
||||
new_var_id: VarId,
|
||||
) {
|
||||
match self {
|
||||
RedirectionTarget::File { expr, .. } => {
|
||||
expr.replace_in_variable(working_set, new_var_id)
|
||||
}
|
||||
RedirectionTarget::Pipe { .. } => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
|
@ -75,6 +88,23 @@ pub enum PipelineRedirection {
|
|||
err: RedirectionTarget,
|
||||
},
|
||||
}
|
||||
impl PipelineRedirection {
|
||||
pub fn replace_in_variable(
|
||||
&mut self,
|
||||
working_set: &mut StateWorkingSet<'_>,
|
||||
new_var_id: VarId,
|
||||
) {
|
||||
match self {
|
||||
PipelineRedirection::Single { source: _, target } => {
|
||||
target.replace_in_variable(working_set, new_var_id)
|
||||
}
|
||||
PipelineRedirection::Separate { out, err } => {
|
||||
out.replace_in_variable(working_set, new_var_id);
|
||||
err.replace_in_variable(working_set, new_var_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PipelineElement {
|
||||
|
@ -120,6 +150,17 @@ impl PipelineElement {
|
|||
) -> (Option<OutDest>, Option<OutDest>) {
|
||||
self.expr.expr.pipe_redirection(working_set)
|
||||
}
|
||||
|
||||
pub fn replace_in_variable(
|
||||
&mut self,
|
||||
working_set: &mut StateWorkingSet<'_>,
|
||||
new_var_id: VarId,
|
||||
) {
|
||||
self.expr.replace_in_variable(working_set, new_var_id);
|
||||
if let Some(redirection) = &mut self.redirection {
|
||||
redirection.replace_in_variable(working_set, new_var_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
|
@ -343,6 +343,7 @@ fn expr_to_string(engine_state: &EngineState, expr: &Expr) -> String {
|
|||
Expr::String(_) | Expr::RawString(_) => "string".to_string(),
|
||||
Expr::StringInterpolation(_) => "string interpolation".to_string(),
|
||||
Expr::GlobInterpolation(_, _) => "glob interpolation".to_string(),
|
||||
Expr::Collect(_, _) => "collect".to_string(),
|
||||
Expr::Subexpression(_) => "subexpression".to_string(),
|
||||
Expr::Table(_) => "table".to_string(),
|
||||
Expr::UnaryNot(_) => "unary not".to_string(),
|
||||
|
|
|
@ -25,7 +25,6 @@ use crate::{PluginIdentity, PluginRegistryItem, RegisteredPlugin};
|
|||
pub struct StateWorkingSet<'a> {
|
||||
pub permanent_state: &'a EngineState,
|
||||
pub delta: StateDelta,
|
||||
pub external_commands: Vec<Vec<u8>>,
|
||||
pub files: FileStack,
|
||||
/// Whether or not predeclarations are searched when looking up a command (used with aliases)
|
||||
pub search_predecls: bool,
|
||||
|
@ -46,7 +45,6 @@ impl<'a> StateWorkingSet<'a> {
|
|||
Self {
|
||||
delta: StateDelta::new(permanent_state),
|
||||
permanent_state,
|
||||
external_commands: vec![],
|
||||
files,
|
||||
search_predecls: true,
|
||||
parse_errors: vec![],
|
||||
|
|
|
@ -47,6 +47,26 @@ pub fn report_error_new(
|
|||
report_error(&working_set, error);
|
||||
}
|
||||
|
||||
pub fn report_warning(
|
||||
working_set: &StateWorkingSet,
|
||||
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
|
||||
) {
|
||||
eprintln!("Warning: {:?}", CliError(error, working_set));
|
||||
// reset vt processing, aka ansi because illbehaved externals can break it
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let _ = nu_utils::enable_vt_processing();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_warning_new(
|
||||
engine_state: &EngineState,
|
||||
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
|
||||
) {
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
report_error(&working_set, error);
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for CliError<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let config = self.1.get_config();
|
||||
|
|
|
@ -5,7 +5,9 @@ mod parse_error;
|
|||
mod parse_warning;
|
||||
mod shell_error;
|
||||
|
||||
pub use cli_error::{format_error, report_error, report_error_new};
|
||||
pub use cli_error::{
|
||||
format_error, report_error, report_error_new, report_warning, report_warning_new,
|
||||
};
|
||||
pub use compile_error::CompileError;
|
||||
pub use labeled_error::{ErrorLabel, LabeledError};
|
||||
pub use parse_error::{DidYouMean, ParseError};
|
||||
|
|
|
@ -6,11 +6,11 @@ use thiserror::Error;
|
|||
#[derive(Clone, Debug, Error, Diagnostic, Serialize, Deserialize)]
|
||||
pub enum ParseWarning {
|
||||
#[error("Deprecated: {old_command}")]
|
||||
#[diagnostic(help("for more info: {url}"))]
|
||||
#[diagnostic(help("for more info see {url}"))]
|
||||
DeprecatedWarning {
|
||||
old_command: String,
|
||||
new_suggestion: String,
|
||||
#[label("`{old_command}` is deprecated and will be removed in a future release. Please {new_suggestion} instead")]
|
||||
#[label("`{old_command}` is deprecated and will be removed in a future release. Please {new_suggestion} instead.")]
|
||||
span: Span,
|
||||
url: String,
|
||||
},
|
||||
|
|
|
@ -159,6 +159,9 @@ pub trait Eval {
|
|||
Expr::ExternalCall(head, args) => {
|
||||
Self::eval_external_call(state, mut_state, head, args, expr_span)
|
||||
}
|
||||
Expr::Collect(var_id, expr) => {
|
||||
Self::eval_collect::<D>(state, mut_state, *var_id, expr)
|
||||
}
|
||||
Expr::Subexpression(block_id) => {
|
||||
Self::eval_subexpression::<D>(state, mut_state, *block_id, expr_span)
|
||||
}
|
||||
|
@ -356,6 +359,13 @@ pub trait Eval {
|
|||
span: Span,
|
||||
) -> Result<Value, ShellError>;
|
||||
|
||||
fn eval_collect<D: DebugContext>(
|
||||
state: Self::State<'_>,
|
||||
mut_state: &mut Self::MutState,
|
||||
var_id: VarId,
|
||||
expr: &Expression,
|
||||
) -> Result<Value, ShellError>;
|
||||
|
||||
fn eval_subexpression<D: DebugContext>(
|
||||
state: Self::State<'_>,
|
||||
mut_state: &mut Self::MutState,
|
||||
|
|
|
@ -185,24 +185,15 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu
|
|||
},
|
||||
);
|
||||
|
||||
// Create a system level directory for nushell scripts, modules, completions, etc
|
||||
// that can be changed by setting the NU_VENDOR_AUTOLOAD_DIR env var on any platform
|
||||
// before nushell is compiled OR if NU_VENDOR_AUTOLOAD_DIR is not set for non-windows
|
||||
// systems, the PREFIX env var can be set before compile and used as PREFIX/nushell/vendor/autoload
|
||||
record.push(
|
||||
"vendor-autoload-dir",
|
||||
// pseudo code
|
||||
// if env var NU_VENDOR_AUTOLOAD_DIR is set, in any platform, use it
|
||||
// if not, if windows, use ALLUSERPROFILE\nushell\vendor\autoload
|
||||
// if not, if non-windows, if env var PREFIX is set, use PREFIX/share/nushell/vendor/autoload
|
||||
// if not, use the default /usr/share/nushell/vendor/autoload
|
||||
|
||||
// check to see if NU_VENDOR_AUTOLOAD_DIR env var is set, if not, use the default
|
||||
if let Some(path) = get_vendor_autoload_dir(engine_state) {
|
||||
Value::string(path.to_string_lossy(), span)
|
||||
} else {
|
||||
Value::error(ShellError::ConfigDirNotFound { span: Some(span) }, span)
|
||||
},
|
||||
"vendor-autoload-dirs",
|
||||
Value::list(
|
||||
get_vendor_autoload_dirs(engine_state)
|
||||
.iter()
|
||||
.map(|path| Value::string(path.to_string_lossy(), span))
|
||||
.collect(),
|
||||
span,
|
||||
),
|
||||
);
|
||||
|
||||
record.push("temp-path", {
|
||||
|
@ -259,39 +250,95 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu
|
|||
Value::record(record, span)
|
||||
}
|
||||
|
||||
pub fn get_vendor_autoload_dir(engine_state: &EngineState) -> Option<PathBuf> {
|
||||
// pseudo code
|
||||
// if env var NU_VENDOR_AUTOLOAD_DIR is set, in any platform, use it
|
||||
// if not, if windows, use ALLUSERPROFILE\nushell\vendor\autoload
|
||||
// if not, if non-windows, if env var PREFIX is set, use PREFIX/share/nushell/vendor/autoload
|
||||
// if not, use the default /usr/share/nushell/vendor/autoload
|
||||
pub fn get_vendor_autoload_dirs(_engine_state: &EngineState) -> Vec<PathBuf> {
|
||||
// load order for autoload dirs
|
||||
// /Library/Application Support/nushell/vendor/autoload on macOS
|
||||
// <dir>/nushell/vendor/autoload for every dir in XDG_DATA_DIRS in reverse order on platforms other than windows. If XDG_DATA_DIRS is not set, it falls back to <PREFIX>/share if PREFIX ends in local, or <PREFIX>/local/share:<PREFIX>/share otherwise. If PREFIX is not set, fall back to /usr/local/share:/usr/share.
|
||||
// %ProgramData%\nushell\vendor\autoload on windows
|
||||
// NU_VENDOR_AUTOLOAD_DIR from compile time, if env var is set at compile time
|
||||
// if on macOS, additionally check XDG_DATA_HOME, which `dirs` is only doing on Linux
|
||||
// <data_dir>/nushell/vendor/autoload of the current user according to the `dirs` crate
|
||||
// NU_VENDOR_AUTOLOAD_DIR at runtime, if env var is set
|
||||
|
||||
// check to see if NU_VENDOR_AUTOLOAD_DIR env var is set, if not, use the default
|
||||
Some(
|
||||
option_env!("NU_VENDOR_AUTOLOAD_DIR")
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| {
|
||||
if cfg!(windows) {
|
||||
let all_user_profile = match engine_state.get_env_var("ALLUSERPROFILE") {
|
||||
Some(v) => format!(
|
||||
"{}\\nushell\\vendor\\autoload",
|
||||
v.coerce_string().unwrap_or("C:\\ProgramData".into())
|
||||
),
|
||||
None => "C:\\ProgramData\\nushell\\vendor\\autoload".into(),
|
||||
};
|
||||
all_user_profile
|
||||
} else {
|
||||
// In non-Windows environments, if NU_VENDOR_AUTOLOAD_DIR is not set
|
||||
// check to see if PREFIX env var is set, and use it as PREFIX/nushell/vendor/autoload
|
||||
// otherwise default to /usr/share/nushell/vendor/autoload
|
||||
option_env!("PREFIX").map(String::from).map_or_else(
|
||||
|| "/usr/local/share/nushell/vendor/autoload".into(),
|
||||
|prefix| format!("{}/share/nushell/vendor/autoload", prefix),
|
||||
)
|
||||
}
|
||||
let into_autoload_path_fn = |mut path: PathBuf| {
|
||||
path.push("nushell");
|
||||
path.push("vendor");
|
||||
path.push("autoload");
|
||||
path
|
||||
};
|
||||
|
||||
let mut dirs = Vec::new();
|
||||
|
||||
let mut append_fn = |path: PathBuf| {
|
||||
if !dirs.contains(&path) {
|
||||
dirs.push(path)
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
std::iter::once("/Library/Application Support")
|
||||
.map(PathBuf::from)
|
||||
.map(into_autoload_path_fn)
|
||||
.for_each(&mut append_fn);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
|
||||
std::env::var_os("XDG_DATA_DIRS")
|
||||
.or_else(|| {
|
||||
option_env!("PREFIX").map(|prefix| {
|
||||
if prefix.ends_with("local") {
|
||||
std::ffi::OsString::from(format!("{prefix}/share"))
|
||||
} else {
|
||||
std::ffi::OsString::from(format!("{prefix}/local/share:{prefix}/share"))
|
||||
}
|
||||
})
|
||||
})
|
||||
.into(),
|
||||
)
|
||||
.unwrap_or_else(|| std::ffi::OsString::from("/usr/local/share/:/usr/share/"))
|
||||
.as_encoded_bytes()
|
||||
.split(|b| *b == b':')
|
||||
.map(|split| into_autoload_path_fn(PathBuf::from(std::ffi::OsStr::from_bytes(split))))
|
||||
.rev()
|
||||
.for_each(&mut append_fn);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
dirs_sys::known_folder(windows_sys::Win32::UI::Shell::FOLDERID_ProgramData)
|
||||
.into_iter()
|
||||
.map(into_autoload_path_fn)
|
||||
.for_each(&mut append_fn);
|
||||
|
||||
option_env!("NU_VENDOR_AUTOLOAD_DIR")
|
||||
.into_iter()
|
||||
.map(PathBuf::from)
|
||||
.for_each(&mut append_fn);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
std::env::var("XDG_DATA_HOME")
|
||||
.ok()
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| {
|
||||
dirs::home_dir().map(|mut home| {
|
||||
home.push(".local");
|
||||
home.push("share");
|
||||
home
|
||||
})
|
||||
})
|
||||
.map(into_autoload_path_fn)
|
||||
.into_iter()
|
||||
.for_each(&mut append_fn);
|
||||
|
||||
dirs::data_dir()
|
||||
.into_iter()
|
||||
.map(into_autoload_path_fn)
|
||||
.for_each(&mut append_fn);
|
||||
|
||||
std::env::var_os("NU_VENDOR_AUTOLOAD_DIR")
|
||||
.into_iter()
|
||||
.map(PathBuf::from)
|
||||
.for_each(&mut append_fn);
|
||||
|
||||
dirs
|
||||
}
|
||||
|
||||
fn eval_const_call(
|
||||
|
@ -422,6 +469,15 @@ impl Eval for EvalConst {
|
|||
Err(ShellError::NotAConstant { span })
|
||||
}
|
||||
|
||||
fn eval_collect<D: DebugContext>(
|
||||
_: &StateWorkingSet,
|
||||
_: &mut (),
|
||||
_var_id: VarId,
|
||||
expr: &Expression,
|
||||
) -> Result<Value, ShellError> {
|
||||
Err(ShellError::NotAConstant { span: expr.span })
|
||||
}
|
||||
|
||||
fn eval_subexpression<D: DebugContext>(
|
||||
working_set: &StateWorkingSet,
|
||||
_: &mut (),
|
||||
|
|
|
@ -239,7 +239,7 @@ impl CallBuilder {
|
|||
}
|
||||
self.inner.args_len += 1;
|
||||
if let Some(span) = argument.span() {
|
||||
self.inner.span = self.inner.span.append(span);
|
||||
self.inner.span = self.inner.span.merge(span);
|
||||
}
|
||||
stack.arguments.push(argument);
|
||||
self
|
||||
|
|
|
@ -102,6 +102,10 @@ impl<'a> fmt::Display for FmtInstruction<'a> {
|
|||
let var = FmtVar::new(self.engine_state, *var_id);
|
||||
write!(f, "{:WIDTH$} {var}, {src}", "store-variable")
|
||||
}
|
||||
Instruction::DropVariable { var_id } => {
|
||||
let var = FmtVar::new(self.engine_state, *var_id);
|
||||
write!(f, "{:WIDTH$} {var}", "drop-variable")
|
||||
}
|
||||
Instruction::LoadEnv { dst, key } => {
|
||||
let key = FmtData(self.data, *key);
|
||||
write!(f, "{:WIDTH$} {dst}, {key}", "load-env")
|
||||
|
|
|
@ -127,6 +127,8 @@ pub enum Instruction {
|
|||
LoadVariable { dst: RegId, var_id: VarId },
|
||||
/// Store the value of a variable from the `src` register
|
||||
StoreVariable { var_id: VarId, src: RegId },
|
||||
/// Remove a variable from the stack, freeing up whatever resources were associated with it
|
||||
DropVariable { var_id: VarId },
|
||||
/// Load the value of an environment variable into the `dst` register
|
||||
LoadEnv { dst: RegId, key: DataSlice },
|
||||
/// Load the value of an environment variable into the `dst` register, or `Nothing` if it
|
||||
|
@ -290,6 +292,7 @@ impl Instruction {
|
|||
Instruction::Drain { .. } => None,
|
||||
Instruction::LoadVariable { dst, .. } => Some(dst),
|
||||
Instruction::StoreVariable { .. } => None,
|
||||
Instruction::DropVariable { .. } => None,
|
||||
Instruction::LoadEnv { dst, .. } => Some(dst),
|
||||
Instruction::LoadEnvOpt { dst, .. } => Some(dst),
|
||||
Instruction::StoreEnv { .. } => None,
|
||||
|
|
|
@ -635,6 +635,34 @@ impl PipelineData {
|
|||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unsupported_input_error(
|
||||
self,
|
||||
expected_type: impl Into<String>,
|
||||
span: Span,
|
||||
) -> ShellError {
|
||||
match self {
|
||||
PipelineData::Empty => ShellError::PipelineEmpty { dst_span: span },
|
||||
PipelineData::Value(value, ..) => ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: expected_type.into(),
|
||||
wrong_type: value.get_type().get_non_specified_string(),
|
||||
dst_span: span,
|
||||
src_span: value.span(),
|
||||
},
|
||||
PipelineData::ListStream(stream, ..) => ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: expected_type.into(),
|
||||
wrong_type: "list (stream)".into(),
|
||||
dst_span: span,
|
||||
src_span: stream.span(),
|
||||
},
|
||||
PipelineData::ByteStream(stream, ..) => ShellError::OnlySupportsThisInputType {
|
||||
exp_input_type: expected_type.into(),
|
||||
wrong_type: stream.type_().describe().into(),
|
||||
dst_span: span,
|
||||
src_span: stream.span(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PipelineIteratorInner {
|
||||
|
|
|
@ -47,6 +47,12 @@ pub enum SyntaxShape {
|
|||
/// A general expression, eg `1 + 2` or `foo --bar`
|
||||
Expression,
|
||||
|
||||
/// A (typically) string argument that follows external command argument parsing rules.
|
||||
///
|
||||
/// Filepaths are expanded if unquoted, globs are allowed, and quotes embedded within unknown
|
||||
/// args are unquoted.
|
||||
ExternalArgument,
|
||||
|
||||
/// A filepath is allowed
|
||||
Filepath,
|
||||
|
||||
|
@ -145,6 +151,7 @@ impl SyntaxShape {
|
|||
SyntaxShape::DateTime => Type::Date,
|
||||
SyntaxShape::Duration => Type::Duration,
|
||||
SyntaxShape::Expression => Type::Any,
|
||||
SyntaxShape::ExternalArgument => Type::Any,
|
||||
SyntaxShape::Filepath => Type::String,
|
||||
SyntaxShape::Directory => Type::String,
|
||||
SyntaxShape::Float => Type::Float,
|
||||
|
@ -238,6 +245,7 @@ impl Display for SyntaxShape {
|
|||
SyntaxShape::Signature => write!(f, "signature"),
|
||||
SyntaxShape::MatchBlock => write!(f, "match-block"),
|
||||
SyntaxShape::Expression => write!(f, "expression"),
|
||||
SyntaxShape::ExternalArgument => write!(f, "external-argument"),
|
||||
SyntaxShape::Boolean => write!(f, "bool"),
|
||||
SyntaxShape::Error => write!(f, "error"),
|
||||
SyntaxShape::CompleterWrapper(x, _) => write!(f, "completable<{x}>"),
|
||||
|
|
|
@ -36,6 +36,10 @@ pub enum Type {
|
|||
}
|
||||
|
||||
impl Type {
|
||||
pub fn list(inner: Type) -> Self {
|
||||
Self::List(Box::new(inner))
|
||||
}
|
||||
|
||||
pub fn record() -> Self {
|
||||
Self::Record([].into())
|
||||
}
|
||||
|
|
|
@ -418,62 +418,7 @@ impl TableOption<NuRecords, CompleteDimensionVecRecords<'_>, ColoredConfig> for
|
|||
// we already must have been estimated that it's safe to do.
|
||||
// and all dims will be suffitient
|
||||
if self.trim_as_head {
|
||||
if recs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// even though it's safe to trim columns by header there might be left unused space
|
||||
// so we do use it if possible prioritizing left columns
|
||||
|
||||
let headers = recs[0].to_owned();
|
||||
let headers_widths = headers
|
||||
.iter()
|
||||
.map(CellInfo::width)
|
||||
.map(|v| v + self.pad)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let min_width_use = get_total_width2(&headers_widths, cfg);
|
||||
|
||||
let mut free_width = self.width_max.saturating_sub(min_width_use);
|
||||
|
||||
for (i, head_width) in headers_widths.into_iter().enumerate() {
|
||||
let head_width = head_width - self.pad;
|
||||
let column_width = self.width[i] - self.pad; // safe to assume width is bigger then paddding
|
||||
|
||||
let mut use_width = head_width;
|
||||
if free_width > 0 {
|
||||
// it's safe to assume that column_width is always bigger or equal to head_width
|
||||
debug_assert!(column_width >= head_width);
|
||||
|
||||
let additional_width = min(free_width, column_width - head_width);
|
||||
free_width -= additional_width;
|
||||
use_width += additional_width;
|
||||
}
|
||||
|
||||
match &self.strategy {
|
||||
TrimStrategy::Wrap { try_to_keep_words } => {
|
||||
let mut wrap = Width::wrap(use_width);
|
||||
if *try_to_keep_words {
|
||||
wrap = wrap.keep_words();
|
||||
}
|
||||
|
||||
Modify::new(Columns::single(i))
|
||||
.with(wrap)
|
||||
.change(recs, cfg, dims);
|
||||
}
|
||||
TrimStrategy::Truncate { suffix } => {
|
||||
let mut truncate = Width::truncate(use_width);
|
||||
if let Some(suffix) = suffix {
|
||||
truncate = truncate.suffix(suffix).suffix_try_color(true);
|
||||
}
|
||||
|
||||
Modify::new(Columns::single(i))
|
||||
.with(truncate)
|
||||
.change(recs, cfg, dims);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trim_as_header(recs, cfg, dims, self);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -498,6 +443,67 @@ impl TableOption<NuRecords, CompleteDimensionVecRecords<'_>, ColoredConfig> for
|
|||
}
|
||||
}
|
||||
|
||||
fn trim_as_header(
|
||||
recs: &mut VecRecords<CellInfo<String>>,
|
||||
cfg: &mut ColoredConfig,
|
||||
dims: &mut CompleteDimensionVecRecords,
|
||||
trim: TableTrim,
|
||||
) {
|
||||
if recs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let headers = recs[0].to_owned();
|
||||
let headers_widths = headers
|
||||
.iter()
|
||||
.map(CellInfo::width)
|
||||
.map(|v| v + trim.pad)
|
||||
.collect::<Vec<_>>();
|
||||
let min_width_use = get_total_width2(&headers_widths, cfg);
|
||||
let mut free_width = trim.width_max.saturating_sub(min_width_use);
|
||||
|
||||
// even though it's safe to trim columns by header there might be left unused space
|
||||
// so we do use it if possible prioritizing left columns
|
||||
|
||||
for (i, head_width) in headers_widths.into_iter().enumerate() {
|
||||
let head_width = head_width - trim.pad;
|
||||
let column_width = trim.width[i] - trim.pad; // safe to assume width is bigger then paddding
|
||||
|
||||
let mut use_width = head_width;
|
||||
if free_width > 0 {
|
||||
// it's safe to assume that column_width is always bigger or equal to head_width
|
||||
debug_assert!(column_width >= head_width);
|
||||
|
||||
let additional_width = min(free_width, column_width - head_width);
|
||||
free_width -= additional_width;
|
||||
use_width += additional_width;
|
||||
}
|
||||
|
||||
match &trim.strategy {
|
||||
TrimStrategy::Wrap { try_to_keep_words } => {
|
||||
let mut wrap = Width::wrap(use_width);
|
||||
if *try_to_keep_words {
|
||||
wrap = wrap.keep_words();
|
||||
}
|
||||
|
||||
Modify::new(Columns::single(i))
|
||||
.with(wrap)
|
||||
.change(recs, cfg, dims);
|
||||
}
|
||||
TrimStrategy::Truncate { suffix } => {
|
||||
let mut truncate = Width::truncate(use_width);
|
||||
if let Some(suffix) = suffix {
|
||||
truncate = truncate.suffix(suffix).suffix_try_color(true);
|
||||
}
|
||||
|
||||
Modify::new(Columns::single(i))
|
||||
.with(truncate)
|
||||
.change(recs, cfg, dims);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn align_table(
|
||||
table: &mut Table,
|
||||
alignments: Alignments,
|
||||
|
@ -793,14 +799,14 @@ fn truncate_columns_by_head(
|
|||
let mut truncate_pos = 0;
|
||||
for (i, column_header) in head.iter().enumerate() {
|
||||
let column_header_width = Cell::width(column_header);
|
||||
width += column_header_width;
|
||||
width += column_header_width + pad;
|
||||
|
||||
if i > 0 {
|
||||
width += has_vertical as usize;
|
||||
}
|
||||
|
||||
if width >= termwidth {
|
||||
width -= column_header_width + (i > 0 && has_vertical) as usize;
|
||||
width -= column_header_width + (i > 0 && has_vertical) as usize + pad;
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@ let dark_theme = {
|
|||
shape_flag: blue_bold
|
||||
shape_float: purple_bold
|
||||
# shapes are used to change the cli syntax highlighting
|
||||
shape_garbage: { fg: white bg: red attr: b}
|
||||
shape_garbage: { fg: white bg: red attr: b }
|
||||
shape_glob_interpolation: cyan_bold
|
||||
shape_globpattern: cyan_bold
|
||||
shape_int: purple_bold
|
||||
|
@ -114,7 +114,8 @@ let light_theme = {
|
|||
shape_flag: blue_bold
|
||||
shape_float: purple_bold
|
||||
# shapes are used to change the cli syntax highlighting
|
||||
shape_garbage: { fg: white bg: red attr: b}
|
||||
shape_garbage: { fg: white bg: red attr: b }
|
||||
shape_glob_interpolation: cyan_bold
|
||||
shape_globpattern: cyan_bold
|
||||
shape_int: purple_bold
|
||||
shape_internalcall: cyan_bold
|
||||
|
@ -226,9 +227,9 @@ $env.config = {
|
|||
|
||||
color_config: $dark_theme # if you want a more interesting theme, you can replace the empty record with `$dark_theme`, `$light_theme` or another custom record
|
||||
use_grid_icons: true
|
||||
footer_mode: "25" # always, never, number_of_rows, auto
|
||||
footer_mode: 25 # always, never, number_of_rows, auto
|
||||
float_precision: 2 # the precision for displaying floats in tables
|
||||
buffer_editor: "" # command that will be used to edit the current line buffer with ctrl+o, if unset fallback to $env.EDITOR and $env.VISUAL
|
||||
buffer_editor: null # command that will be used to edit the current line buffer with ctrl+o, if unset fallback to $env.EDITOR and $env.VISUAL
|
||||
use_ansi_coloring: true
|
||||
bracketed_paste: true # enable bracketed paste, currently useless on windows
|
||||
edit_mode: emacs # emacs, vi
|
||||
|
@ -888,4 +889,4 @@ $env.config = {
|
|||
event: { edit: selectall }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
78
crates/nu_plugin_example/src/commands/call_decl.rs
Normal file
78
crates/nu_plugin_example/src/commands/call_decl.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand};
|
||||
use nu_protocol::{
|
||||
IntoSpanned, LabeledError, PipelineData, Record, Signature, Spanned, SyntaxShape, Value,
|
||||
};
|
||||
|
||||
use crate::ExamplePlugin;
|
||||
|
||||
pub struct CallDecl;
|
||||
|
||||
impl PluginCommand for CallDecl {
|
||||
type Plugin = ExamplePlugin;
|
||||
|
||||
fn name(&self) -> &str {
|
||||
"example call-decl"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.required(
|
||||
"name",
|
||||
SyntaxShape::String,
|
||||
"the name of the command to call",
|
||||
)
|
||||
.optional(
|
||||
"named_args",
|
||||
SyntaxShape::Record(vec![]),
|
||||
"named arguments to pass to the command",
|
||||
)
|
||||
.rest(
|
||||
"positional_args",
|
||||
SyntaxShape::Any,
|
||||
"positional arguments to pass to the command",
|
||||
)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Demonstrates calling other commands from plugins using `call_decl()`."
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
"
|
||||
The arguments will not be typechecked at parse time. This command is for
|
||||
demonstration only, and should not be used for anything real.
|
||||
"
|
||||
.trim()
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_plugin: &ExamplePlugin,
|
||||
engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, LabeledError> {
|
||||
let name: Spanned<String> = call.req(0)?;
|
||||
let named_args: Option<Record> = call.opt(1)?;
|
||||
let positional_args: Vec<Value> = call.rest(2)?;
|
||||
|
||||
let decl_id = engine.find_decl(&name.item)?.ok_or_else(|| {
|
||||
LabeledError::new(format!("Can't find `{}`", name.item))
|
||||
.with_label("not in scope", name.span)
|
||||
})?;
|
||||
|
||||
let mut new_call = EvaluatedCall::new(call.head);
|
||||
|
||||
for (key, val) in named_args.into_iter().flatten() {
|
||||
new_call.add_named(key.into_spanned(val.span()), val);
|
||||
}
|
||||
|
||||
for val in positional_args {
|
||||
new_call.add_positional(val);
|
||||
}
|
||||
|
||||
let result = engine.call_decl(decl_id, new_call, input, true, false)?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
|
@ -13,11 +13,13 @@ pub use three::Three;
|
|||
pub use two::Two;
|
||||
|
||||
// Engine interface demos
|
||||
mod call_decl;
|
||||
mod config;
|
||||
mod disable_gc;
|
||||
mod env;
|
||||
mod view_span;
|
||||
|
||||
pub use call_decl::CallDecl;
|
||||
pub use config::Config;
|
||||
pub use disable_gc::DisableGc;
|
||||
pub use env::Env;
|
||||
|
|
|
@ -27,6 +27,7 @@ impl Plugin for ExamplePlugin {
|
|||
Box::new(Env),
|
||||
Box::new(ViewSpan),
|
||||
Box::new(DisableGc),
|
||||
Box::new(CallDecl),
|
||||
// Stream demos
|
||||
Box::new(CollectBytes),
|
||||
Box::new(Echo),
|
||||
|
|
|
@ -39,7 +39,7 @@ polars-utils = { version = "0.41"}
|
|||
typetag = "0.2"
|
||||
env_logger = "0.11.3"
|
||||
log.workspace = true
|
||||
uuid = { version = "1.9", features = ["v4", "serde"] }
|
||||
uuid = { version = "1.10", features = ["v4", "serde"] }
|
||||
|
||||
[dependencies.polars]
|
||||
features = [
|
||||
|
|
|
@ -76,7 +76,7 @@ pub fn web_examples() -> Vec<Example<'static>> {
|
|||
},
|
||||
Example {
|
||||
example: "http get https://en.wikipedia.org/wiki/List_of_cities_in_India_by_population |
|
||||
query web --as-table [City 'Population(2011)[3]' 'Population(2001)[3][a]' 'State or unionterritory' 'Ref']",
|
||||
query web --as-table [City 'Population(2011)[3]' 'Population(2001)[3][a]' 'State or unionterritory' 'Reference']",
|
||||
description: "Retrieve a html table from Wikipedia and parse it into a nushell table using table headers as guides",
|
||||
result: None
|
||||
},
|
||||
|
|
|
@ -329,6 +329,12 @@ fn convert_to_value(
|
|||
msg: "glob interpolation not supported in nuon".into(),
|
||||
span: expr.span,
|
||||
}),
|
||||
Expr::Collect(..) => Err(ShellError::OutsideSpannedLabeledError {
|
||||
src: original_text.to_string(),
|
||||
error: "Error when loading".into(),
|
||||
msg: "`$in` not supported in nuon".into(),
|
||||
span: expr.span,
|
||||
}),
|
||||
Expr::Subexpression(..) => Err(ShellError::OutsideSpannedLabeledError {
|
||||
src: original_text.to_string(),
|
||||
error: "Error when loading".into(),
|
||||
|
|
|
@ -200,8 +200,7 @@ pub(crate) fn read_vendor_autoload_files(engine_state: &mut EngineState, stack:
|
|||
column!()
|
||||
);
|
||||
|
||||
// read and source vendor_autoload_files file if exists
|
||||
if let Some(autoload_dir) = nu_protocol::eval_const::get_vendor_autoload_dir(engine_state) {
|
||||
for autoload_dir in nu_protocol::eval_const::get_vendor_autoload_dirs(engine_state) {
|
||||
warn!("read_vendor_autoload_files: {}", autoload_dir.display());
|
||||
|
||||
if autoload_dir.exists() {
|
||||
|
|
|
@ -100,7 +100,7 @@ fn main() -> Result<()> {
|
|||
},
|
||||
);
|
||||
} else if let Some(old_config) =
|
||||
nu_path::get_canonicalized_path(dirs_next::config_dir()).map(|p| p.join("nushell"))
|
||||
nu_path::get_canonicalized_path(dirs::config_dir()).map(|p| p.join("nushell"))
|
||||
{
|
||||
let xdg_config_empty = nushell_config_path
|
||||
.read_dir()
|
||||
|
|
42
tests/plugins/call_decl.rs
Normal file
42
tests/plugins/call_decl.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
use nu_test_support::nu_with_plugins;
|
||||
|
||||
#[test]
|
||||
fn call_to_json() {
|
||||
let result = nu_with_plugins!(
|
||||
cwd: ".",
|
||||
plugin: ("nu_plugin_example"),
|
||||
r#"
|
||||
[42] | example call-decl 'to json' {indent: 4}
|
||||
"#
|
||||
);
|
||||
assert!(result.status.success());
|
||||
// newlines are removed from test output
|
||||
assert_eq!("[ 42]", result.out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_reduce() {
|
||||
let result = nu_with_plugins!(
|
||||
cwd: ".",
|
||||
plugin: ("nu_plugin_example"),
|
||||
r#"
|
||||
[1 2 3] | example call-decl 'reduce' {fold: 10} { |it, acc| $it + $acc }
|
||||
"#
|
||||
);
|
||||
assert!(result.status.success());
|
||||
assert_eq!("16", result.out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_scope_variables() {
|
||||
let result = nu_with_plugins!(
|
||||
cwd: ".",
|
||||
plugin: ("nu_plugin_example"),
|
||||
r#"
|
||||
let test_var = 10
|
||||
example call-decl 'scope variables' | where name == '$test_var' | length
|
||||
"#
|
||||
);
|
||||
assert!(result.status.success());
|
||||
assert_eq!("1", result.out);
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
mod call_decl;
|
||||
mod config;
|
||||
mod core_inc;
|
||||
mod custom_values;
|
||||
|
|
|
@ -235,7 +235,7 @@ fn test_xdg_config_empty() {
|
|||
playground.with_env("XDG_CONFIG_HOME", "");
|
||||
|
||||
let actual = run(playground, "$nu.default-config-dir");
|
||||
let expected = dirs_next::config_dir().unwrap().join("nushell");
|
||||
let expected = dirs::config_dir().unwrap().join("nushell");
|
||||
assert_eq!(
|
||||
actual,
|
||||
adjust_canonicalization(expected.canonicalize().unwrap_or(expected))
|
||||
|
@ -250,7 +250,7 @@ fn test_xdg_config_bad() {
|
|||
playground.with_env("XDG_CONFIG_HOME", xdg_config_home);
|
||||
|
||||
let actual = run(playground, "$nu.default-config-dir");
|
||||
let expected = dirs_next::config_dir().unwrap().join("nushell");
|
||||
let expected = dirs::config_dir().unwrap().join("nushell");
|
||||
assert_eq!(
|
||||
actual,
|
||||
adjust_canonicalization(expected.canonicalize().unwrap_or(expected))
|
||||
|
|
|
@ -186,8 +186,12 @@ fn help_present_in_def() -> TestResult {
|
|||
#[test]
|
||||
fn help_not_present_in_extern() -> TestResult {
|
||||
run_test(
|
||||
"module test {export extern \"git fetch\" []}; use test `git fetch`; help git fetch | ansi strip",
|
||||
"Usage:\n > git fetch",
|
||||
r#"
|
||||
module test {export extern "git fetch" []};
|
||||
use test `git fetch`;
|
||||
help git fetch | find help | to text | ansi strip
|
||||
"#,
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,29 @@ fn in_and_if_else() -> TestResult {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_with_closure() -> TestResult {
|
||||
// Can use $in twice
|
||||
run_test(r#"3 | do { let x = $in; let y = $in; $x + $y }"#, "6")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_with_custom_command() -> TestResult {
|
||||
// Can use $in twice
|
||||
run_test(
|
||||
r#"def foo [] { let x = $in; let y = $in; $x + $y }; 3 | foo"#,
|
||||
"6",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_used_twice_and_also_in_pipeline() -> TestResult {
|
||||
run_test(
|
||||
r#"3 | do { let x = $in; let y = $in; $x + $y | $in * 4 }"#,
|
||||
"24",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_works_with_missing_requirements() -> TestResult {
|
||||
run_test(r#"each --help | lines | length"#, "72")
|
||||
|
|
|
@ -137,3 +137,39 @@ fn known_external_aliased_subcommand_from_module() -> TestResult {
|
|||
String::from_utf8(output.stdout)?.trim(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_external_arg_expansion() -> TestResult {
|
||||
run_test(
|
||||
r#"
|
||||
extern echo [];
|
||||
echo ~/foo
|
||||
"#,
|
||||
&dirs::home_dir()
|
||||
.expect("can't find home dir")
|
||||
.join("foo")
|
||||
.to_string_lossy(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_external_arg_quoted_no_expand() -> TestResult {
|
||||
run_test(
|
||||
r#"
|
||||
extern echo [];
|
||||
echo "~/foo"
|
||||
"#,
|
||||
"~/foo",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_external_arg_internally_quoted_options() -> TestResult {
|
||||
run_test(
|
||||
r#"
|
||||
extern echo [];
|
||||
echo --option="test"
|
||||
"#,
|
||||
"--option=test",
|
||||
)
|
||||
}
|
||||
|
|
|
@ -129,3 +129,16 @@ fn transpose_into_load_env() -> TestResult {
|
|||
"10",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_variable_expression_correct_output_type() -> TestResult {
|
||||
run_test(r#"def foo []: nothing -> string { 'foo' | $"($in)" }"#, "")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_variable_expression_wrong_output_type() -> TestResult {
|
||||
fail_test(
|
||||
r#"def foo []: nothing -> int { 'foo' | $"($in)" }"#,
|
||||
"expected int",
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user