diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2193598686..07e0b2b891 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -26,6 +26,13 @@ updates: patterns: - "polars" - "polars-*" + # uutils/coreutils also versions all their workspace crates the same at the moment + # Most of them have bleeding edge version requirements (some not) + # see: https://github.com/uutils/coreutils/blob/main/Cargo.toml + uutils: + patterns: + - "uucore" + - "uu_*" - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/nightly-build.yml b/.github/workflows/nightly-build.yml index 8097b02fe6..9b7f44feba 100644 --- a/.github/workflows/nightly-build.yml +++ b/.github/workflows/nightly-build.yml @@ -99,13 +99,13 @@ jobs: extra: msi os: windows-latest - target: x86_64-unknown-linux-gnu - os: ubuntu-20.04 + os: ubuntu-22.04 - target: x86_64-unknown-linux-musl - os: ubuntu-20.04 + os: ubuntu-22.04 - target: aarch64-unknown-linux-gnu - os: ubuntu-20.04 + os: ubuntu-22.04 - target: armv7-unknown-linux-gnueabihf - os: ubuntu-20.04 + os: ubuntu-22.04 - target: riscv64gc-unknown-linux-gnu os: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dd3371e0d3..34cad24fbf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,13 +49,13 @@ jobs: extra: msi os: windows-latest - target: x86_64-unknown-linux-gnu - os: ubuntu-20.04 + os: ubuntu-22.04 - target: x86_64-unknown-linux-musl - os: ubuntu-20.04 + os: ubuntu-22.04 - target: aarch64-unknown-linux-gnu - os: ubuntu-20.04 + os: ubuntu-22.04 - target: armv7-unknown-linux-gnueabihf - os: ubuntu-20.04 + os: ubuntu-22.04 - target: riscv64gc-unknown-linux-gnu os: ubuntu-latest diff --git a/.github/workflows/typos.yml b/.github/workflows/typos.yml index 95fc51b970..c7c153983d 100644 --- a/.github/workflows/typos.yml +++ b/.github/workflows/typos.yml @@ -10,4 +10,4 @@ jobs: uses: actions/checkout@v4.1.7 - name: Check spelling - uses: crate-ci/typos@v1.22.9 + uses: crate-ci/typos@v1.23.2 diff --git a/Cargo.lock b/Cargo.lock index 72b7f72b3b..49770a9a0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] @@ -2342,9 +2342,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libflate" @@ -2382,9 +2382,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", "windows-targets 0.52.5", @@ -2873,7 +2873,7 @@ dependencies = [ "assert_cmd", "crossterm", "ctrlc", - "dirs-next", + "dirs", "log", "miette", "mimalloc", @@ -2900,6 +2900,7 @@ dependencies = [ "openssl", "pretty_assertions", "reedline", + "regex", "rstest", "serde_json", "serial_test", @@ -3046,7 +3047,7 @@ dependencies = [ "deunicode", "dialoguer", "digest", - "dirs-next", + "dirs", "dtparse", "encoding_rs", "fancy-regex", @@ -3151,6 +3152,7 @@ dependencies = [ name = "nu-engine" version = "0.95.1" dependencies = [ + "log", "nu-glob", "nu-path", "nu-protocol", @@ -3193,7 +3195,10 @@ dependencies = [ name = "nu-json" version = "0.95.1" dependencies = [ + "fancy-regex", "linked-hash-map", + "nu-path", + "nu-test-support", "num-traits", "serde", "serde_json", @@ -3240,7 +3245,7 @@ dependencies = [ name = "nu-path" version = "0.95.1" dependencies = [ - "dirs-next", + "dirs", "omnipath", "pwd", ] @@ -3255,6 +3260,7 @@ dependencies = [ "nu-plugin-core", "nu-plugin-protocol", "nu-protocol", + "nu-utils", "serde", "thiserror", "typetag", @@ -3284,6 +3290,7 @@ dependencies = [ "nu-plugin-protocol", "nu-protocol", "nu-system", + "nu-utils", "serde", "typetag", "windows 0.54.0", @@ -3339,6 +3346,7 @@ dependencies = [ "convert_case", "fancy-regex", "indexmap", + "log", "lru", "miette", "nix", @@ -3765,9 +3773,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", @@ -3828,6 +3836,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" @@ -4988,8 +5002,7 @@ dependencies = [ [[package]] name = "reedline" version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf59e4c97b5049ba96b052cdb652368305a2eddcbce9bf1c16f9d003139eeea" +source = "git+https://github.com/nushell/reedline?branch=main#480059a3f52cf919341cda88e8c544edd846bc73" dependencies = [ "arboard", "chrono", @@ -5203,9 +5216,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", @@ -6466,9 +6479,9 @@ checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "ureq" -version = "2.9.7" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d11a831e3c0b56e438a28308e7c810799e3c118417f342d30ecec080105395cd" +checksum = "72139d247e5f97a3eff96229a7ae85ead5328a39efe76f8bf5a06313d505b6ea" dependencies = [ "base64 0.22.1", "encoding_rs", @@ -6625,9 +6638,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", diff --git a/Cargo.toml b/Cargo.toml index 573ace795f..335e2f0384 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,7 +83,7 @@ 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" dtparse = "2.0" encoding_rs = "0.8" fancy-regex = "0.13" @@ -120,7 +120,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 +145,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" @@ -164,7 +164,7 @@ trash = "3.3" umask = "2.1" unicode-segmentation = "1.11" unicode-width = "0.1" -ureq = { version = "2.9", default-features = false } +ureq = { version = "2.10", default-features = false } url = "2.2" uu_cp = "0.0.27" uu_mkdir = "0.0.27" @@ -173,7 +173,7 @@ 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" @@ -197,12 +197,11 @@ nu-protocol = { path = "./crates/nu-protocol", version = "0.95.1" } nu-std = { path = "./crates/nu-std", version = "0.95.1" } nu-system = { path = "./crates/nu-system", version = "0.95.1" } nu-utils = { path = "./crates/nu-utils", version = "0.95.1" } - 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 } @@ -230,9 +229,10 @@ 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 } rstest = { workspace = true, default-features = false } serial_test = "3.1" tempfile = { workspace = true } @@ -304,11 +304,11 @@ bench = false # To use a development version of a dependency please use a global override here # changing versions in each sub-crate of the workspace is tedious [patch.crates-io] -# reedline = { git = "https://github.com/nushell/reedline", branch = "main" } +reedline = { git = "https://github.com/nushell/reedline", branch = "main" } # nu-ansi-term = {git = "https://github.com/nushell/nu-ansi-term.git", branch = "main"} # Run all benchmarks with `cargo bench` # Run individual benchmarks like `cargo bench -- ` e.g. `cargo bench -- parse` [[bench]] name = "benchmarks" -harness = false \ No newline at end of file +harness = false diff --git a/benches/benchmarks.rs b/benches/benchmarks.rs index 7a23715c8d..4efb666507 100644 --- a/benches/benchmarks.rs +++ b/benches/benchmarks.rs @@ -4,11 +4,14 @@ use nu_plugin_protocol::{PluginCallResponse, PluginOutput}; use nu_protocol::{ engine::{EngineState, Stack}, - PipelineData, Span, Spanned, Value, + PipelineData, Signals, Span, Spanned, Value, }; use nu_std::load_standard_library; use nu_utils::{get_default_config, get_default_env}; -use std::rc::Rc; +use std::{ + rc::Rc, + sync::{atomic::AtomicBool, Arc}, +}; use std::hint::black_box; @@ -42,6 +45,10 @@ fn setup_stack_and_engine_from_command(command: &str) -> (Stack, EngineState) { }; let mut stack = Stack::new(); + + // Support running benchmarks with IR mode + stack.use_ir = std::env::var_os("NU_USE_IR").is_some(); + evaluate_commands( &commands, &mut engine, @@ -248,14 +255,12 @@ fn bench_eval_interleave(n: i32) -> impl IntoBenchmarks { ) } -fn bench_eval_interleave_with_ctrlc(n: i32) -> impl IntoBenchmarks { +fn bench_eval_interleave_with_interrupt(n: i32) -> impl IntoBenchmarks { let mut engine = setup_engine(); - engine.ctrlc = Some(std::sync::Arc::new(std::sync::atomic::AtomicBool::new( - false, - ))); + engine.set_signals(Signals::new(Arc::new(AtomicBool::new(false)))); let stack = Stack::new(); bench_command( - &format!("eval_interleave_with_ctrlc_{n}"), + &format!("eval_interleave_with_interrupt_{n}"), &format!("seq 1 {n} | wrap a | interleave {{ seq 1 {n} | wrap b }} | ignore"), stack, engine, @@ -443,9 +448,9 @@ tango_benchmarks!( bench_eval_interleave(100), bench_eval_interleave(1_000), bench_eval_interleave(10_000), - bench_eval_interleave_with_ctrlc(100), - bench_eval_interleave_with_ctrlc(1_000), - bench_eval_interleave_with_ctrlc(10_000), + bench_eval_interleave_with_interrupt(100), + bench_eval_interleave_with_interrupt(1_000), + bench_eval_interleave_with_interrupt(10_000), // For bench_eval_for(1), bench_eval_for(10), diff --git a/crates/nu-cli/README.md b/crates/nu-cli/README.md new file mode 100644 index 0000000000..e0eca3c9bd --- /dev/null +++ b/crates/nu-cli/README.md @@ -0,0 +1,7 @@ +This crate implements the core functionality of the interactive Nushell REPL and interfaces with `reedline`. +Currently implements the syntax highlighting and completions logic. +Furthermore includes a few commands that are specific to `reedline` + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-cli/src/commands/history/history_.rs b/crates/nu-cli/src/commands/history/history_.rs index 8b0714216e..cdf85eea72 100644 --- a/crates/nu-cli/src/commands/history/history_.rs +++ b/crates/nu-cli/src/commands/history/history_.rs @@ -47,7 +47,7 @@ impl Command for History { if let Some(config_path) = nu_path::config_dir() { let clear = call.has_flag(engine_state, stack, "clear")?; let long = call.has_flag(engine_state, stack, "long")?; - let ctrlc = engine_state.ctrlc.clone(); + let signals = engine_state.signals().clone(); let mut history_path = config_path; history_path.push("nushell"); @@ -107,7 +107,7 @@ impl Command for History { file: history_path.display().to_string(), span: head, })? - .into_pipeline_data(head, ctrlc)), + .into_pipeline_data(head, signals)), HistoryFileFormat::Sqlite => Ok(history_reader .and_then(|h| { h.search(SearchQuery::everything(SearchDirection::Forward, None)) @@ -122,7 +122,7 @@ impl Command for History { file: history_path.display().to_string(), span: head, })? - .into_pipeline_data(head, ctrlc)), + .into_pipeline_data(head, signals)), } } } else { diff --git a/crates/nu-cli/src/commands/keybindings_list.rs b/crates/nu-cli/src/commands/keybindings_list.rs index f4450c0c23..350df7b820 100644 --- a/crates/nu-cli/src/commands/keybindings_list.rs +++ b/crates/nu-cli/src/commands/keybindings_list.rs @@ -49,22 +49,24 @@ impl Command for KeybindingsList { fn run( &self, - _engine_state: &EngineState, - _stack: &mut Stack, + engine_state: &EngineState, + stack: &mut Stack, call: &Call, _input: PipelineData, ) -> Result { - let records = if call.named_len() == 0 { - let all_options = ["modifiers", "keycodes", "edits", "modes", "events"]; - all_options - .iter() - .flat_map(|argument| get_records(argument, call.head)) - .collect() - } else { - call.named_iter() - .flat_map(|(argument, _, _)| get_records(argument.item.as_str(), call.head)) - .collect() - }; + let all_options = ["modifiers", "keycodes", "edits", "modes", "events"]; + + let presence = all_options + .iter() + .map(|option| call.has_flag(engine_state, stack, option)) + .collect::, ShellError>>()?; + + let records = all_options + .iter() + .zip(presence) + .filter(|(_, present)| *present) + .flat_map(|(option, _)| get_records(option, call.head)) + .collect(); Ok(Value::list(records, call.head).into_pipeline_data()) } diff --git a/crates/nu-cli/src/completions/variable_completions.rs b/crates/nu-cli/src/completions/variable_completions.rs index ab04da13ea..72a69e942c 100644 --- a/crates/nu-cli/src/completions/variable_completions.rs +++ b/crates/nu-cli/src/completions/variable_completions.rs @@ -229,6 +229,8 @@ impl Completer for VariableCompletion { } } + output = sort_suggestions(&prefix_str, output, SortBy::Ascending); + output.dedup(); // TODO: Removes only consecutive duplicates, is it intended? output diff --git a/crates/nu-cli/src/eval_cmds.rs b/crates/nu-cli/src/eval_cmds.rs index 13141f6174..1459f1ef0d 100644 --- a/crates/nu-cli/src/eval_cmds.rs +++ b/crates/nu-cli/src/eval_cmds.rs @@ -53,9 +53,8 @@ pub fn evaluate_commands( // Parse the source code let (block, delta) = { if let Some(ref t_mode) = table_mode { - let mut config = engine_state.get_config().clone(); - config.table_mode = t_mode.coerce_str()?.parse().unwrap_or_default(); - engine_state.set_config(config); + Arc::make_mut(&mut engine_state.config).table_mode = + t_mode.coerce_str()?.parse().unwrap_or_default(); } let mut working_set = StateWorkingSet::new(engine_state); @@ -70,6 +69,11 @@ pub fn evaluate_commands( std::process::exit(1); } + if let Some(err) = working_set.compile_errors.first() { + report_error(&working_set, err); + // Not a fatal error, for now + } + (output, working_set.render()) }; diff --git a/crates/nu-cli/src/eval_file.rs b/crates/nu-cli/src/eval_file.rs index ff6ba36fe3..bac4378d1b 100644 --- a/crates/nu-cli/src/eval_file.rs +++ b/crates/nu-cli/src/eval_file.rs @@ -76,12 +76,21 @@ pub fn evaluate_file( trace!("parsing file: {}", file_path_str); let block = parse(&mut working_set, Some(file_path_str), &file, false); + if let Some(warning) = working_set.parse_warnings.first() { + report_error(&working_set, warning); + } + // If any parse errors were found, report the first error and exit. if let Some(err) = working_set.parse_errors.first() { report_error(&working_set, err); std::process::exit(1); } + if let Some(err) = working_set.compile_errors.first() { + report_error(&working_set, err); + // Not a fatal error, for now + } + // Look for blocks whose name starts with "main" and replace it with the filename. for block in working_set.delta.blocks.iter_mut().map(Arc::make_mut) { if block.signature.name == "main" { diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index 6f151adad1..d584802cfb 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod commands; mod completions; mod config_files; diff --git a/crates/nu-cli/src/menus/help_completions.rs b/crates/nu-cli/src/menus/help_completions.rs index c9c1b7bf94..ebe3fe616b 100644 --- a/crates/nu-cli/src/menus/help_completions.rs +++ b/crates/nu-cli/src/menus/help_completions.rs @@ -1,25 +1,31 @@ use nu_engine::documentation::get_flags_section; -use nu_protocol::{engine::EngineState, levenshtein_distance}; +use nu_protocol::{engine::EngineState, levenshtein_distance, Config}; use nu_utils::IgnoreCaseExt; use reedline::{Completer, Suggestion}; use std::{fmt::Write, sync::Arc}; -pub struct NuHelpCompleter(Arc); +pub struct NuHelpCompleter { + engine_state: Arc, + config: Arc, +} impl NuHelpCompleter { - pub fn new(engine_state: Arc) -> Self { - Self(engine_state) + pub fn new(engine_state: Arc, config: Arc) -> Self { + Self { + engine_state, + config, + } } fn completion_helper(&self, line: &str, pos: usize) -> Vec { let folded_line = line.to_folded_case(); let mut commands = self - .0 + .engine_state .get_decls_sorted(false) .into_iter() .filter_map(|(_, decl_id)| { - let decl = self.0.get_decl(decl_id); + let decl = self.engine_state.get_decl(decl_id); (decl.name().to_folded_case().contains(&folded_line) || decl.usage().to_folded_case().contains(&folded_line) || decl @@ -54,9 +60,12 @@ impl NuHelpCompleter { let _ = write!(long_desc, "Usage:\r\n > {}\r\n", sig.call_signature()); if !sig.named.is_empty() { - long_desc.push_str(&get_flags_section(Some(&*self.0.clone()), &sig, |v| { - v.to_parsable_string(", ", &self.0.config) - })) + long_desc.push_str(&get_flags_section( + Some(&self.engine_state), + Some(&self.config), + &sig, + |v| v.to_parsable_string(", ", &self.config), + )) } if !sig.required_positional.is_empty() @@ -71,7 +80,7 @@ impl NuHelpCompleter { let opt_suffix = if let Some(value) = &positional.default_value { format!( " (optional, default: {})", - &value.to_parsable_string(", ", &self.0.config), + &value.to_parsable_string(", ", &self.config), ) } else { (" (optional)").to_string() @@ -138,7 +147,8 @@ mod test { ) { let engine_state = nu_command::add_shell_command_context(nu_cmd_lang::create_default_context()); - let mut completer = NuHelpCompleter::new(engine_state.into()); + let config = engine_state.get_config().clone(); + let mut completer = NuHelpCompleter::new(engine_state.into(), config); let suggestions = completer.complete(line, end); assert_eq!( diff --git a/crates/nu-cli/src/nu_highlight.rs b/crates/nu-cli/src/nu_highlight.rs index 07084c6258..0b5f211838 100644 --- a/crates/nu-cli/src/nu_highlight.rs +++ b/crates/nu-cli/src/nu_highlight.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use nu_engine::command_prelude::*; use reedline::{Highlighter, StyledText}; @@ -32,14 +34,11 @@ impl Command for NuHighlight { ) -> Result { let head = call.head; - let ctrlc = engine_state.ctrlc.clone(); - let engine_state = std::sync::Arc::new(engine_state.clone()); - let config = engine_state.get_config().clone(); + let signals = engine_state.signals(); let highlighter = crate::NuHighlighter { - engine_state, - stack: std::sync::Arc::new(stack.clone()), - config, + engine_state: Arc::new(engine_state.clone()), + stack: Arc::new(stack.clone()), }; input.map( @@ -50,7 +49,7 @@ impl Command for NuHighlight { } Err(err) => Value::error(err, head), }, - ctrlc, + signals, ) } diff --git a/crates/nu-cli/src/reedline_config.rs b/crates/nu-cli/src/reedline_config.rs index dd5a3199dc..d5d3272bc1 100644 --- a/crates/nu-cli/src/reedline_config.rs +++ b/crates/nu-cli/src/reedline_config.rs @@ -77,13 +77,19 @@ pub(crate) fn add_menus( mut line_editor: Reedline, engine_state_ref: Arc, stack: &Stack, - config: &Config, + config: Arc, ) -> Result { //log::trace!("add_menus: config: {:#?}", &config); line_editor = line_editor.clear_menus(); for menu in &config.menus { - line_editor = add_menu(line_editor, menu, engine_state_ref.clone(), stack, config)? + line_editor = add_menu( + line_editor, + menu, + engine_state_ref.clone(), + stack, + config.clone(), + )? } // Checking if the default menus have been added from the config file @@ -100,7 +106,7 @@ pub(crate) fn add_menus( if !config .menus .iter() - .any(|menu| menu.name.to_expanded_string("", config) == name) + .any(|menu| menu.name.to_expanded_string("", &config) == name) { let (block, delta) = { let mut working_set = StateWorkingSet::new(&engine_state); @@ -137,7 +143,7 @@ pub(crate) fn add_menus( &menu, new_engine_state_ref.clone(), stack, - config, + config.clone(), )?; } } @@ -151,27 +157,27 @@ fn add_menu( menu: &ParsedMenu, engine_state: Arc, stack: &Stack, - config: &Config, + config: Arc, ) -> Result { let span = menu.menu_type.span(); if let Value::Record { val, .. } = &menu.menu_type { - let layout = extract_value("layout", val, span)?.to_expanded_string("", config); + let layout = extract_value("layout", val, span)?.to_expanded_string("", &config); match layout.as_str() { - "columnar" => add_columnar_menu(line_editor, menu, engine_state, stack, config), + "columnar" => add_columnar_menu(line_editor, menu, engine_state, stack, &config), "list" => add_list_menu(line_editor, menu, engine_state, stack, config), "ide" => add_ide_menu(line_editor, menu, engine_state, stack, config), "description" => add_description_menu(line_editor, menu, engine_state, stack, config), _ => Err(ShellError::UnsupportedConfigValue { expected: "columnar, list, ide or description".to_string(), - value: menu.menu_type.to_abbreviated_string(config), + value: menu.menu_type.to_abbreviated_string(&config), span: menu.menu_type.span(), }), } } else { Err(ShellError::UnsupportedConfigValue { expected: "only record type".to_string(), - value: menu.menu_type.to_abbreviated_string(config), + value: menu.menu_type.to_abbreviated_string(&config), span: menu.menu_type.span(), }) } @@ -282,9 +288,9 @@ pub(crate) fn add_list_menu( menu: &ParsedMenu, engine_state: Arc, stack: &Stack, - config: &Config, + config: Arc, ) -> Result { - let name = menu.name.to_expanded_string("", config); + let name = menu.name.to_expanded_string("", &config); let mut list_menu = ListMenu::default().with_name(&name); let span = menu.menu_type.span(); @@ -311,7 +317,7 @@ pub(crate) fn add_list_menu( } } - let marker = menu.marker.to_expanded_string("", config); + let marker = menu.marker.to_expanded_string("", &config); list_menu = list_menu.with_marker(&marker); let only_buffer_difference = menu.only_buffer_difference.as_bool()?; @@ -337,7 +343,7 @@ pub(crate) fn add_list_menu( } _ => Err(ShellError::UnsupportedConfigValue { expected: "block or omitted value".to_string(), - value: menu.source.to_abbreviated_string(config), + value: menu.source.to_abbreviated_string(&config), span: menu.source.span(), }), } @@ -349,10 +355,10 @@ pub(crate) fn add_ide_menu( menu: &ParsedMenu, engine_state: Arc, stack: &Stack, - config: &Config, + config: Arc, ) -> Result { let span = menu.menu_type.span(); - let name = menu.name.to_expanded_string("", config); + let name = menu.name.to_expanded_string("", &config); let mut ide_menu = IdeMenu::default().with_name(&name); if let Value::Record { val, .. } = &menu.menu_type { @@ -417,7 +423,7 @@ pub(crate) fn add_ide_menu( } else { return Err(ShellError::UnsupportedConfigValue { expected: "bool or record".to_string(), - value: border.to_abbreviated_string(config), + value: border.to_abbreviated_string(&config), span: border.span(), }); } @@ -441,7 +447,7 @@ pub(crate) fn add_ide_menu( _ => { return Err(ShellError::UnsupportedConfigValue { expected: "\"left\", \"right\" or \"prefer_right\"".to_string(), - value: description_mode.to_abbreviated_string(config), + value: description_mode.to_abbreviated_string(&config), span: description_mode.span(), }); } @@ -509,7 +515,7 @@ pub(crate) fn add_ide_menu( } } - let marker = menu.marker.to_expanded_string("", config); + let marker = menu.marker.to_expanded_string("", &config); ide_menu = ide_menu.with_marker(&marker); let only_buffer_difference = menu.only_buffer_difference.as_bool()?; @@ -535,7 +541,7 @@ pub(crate) fn add_ide_menu( } _ => Err(ShellError::UnsupportedConfigValue { expected: "block or omitted value".to_string(), - value: menu.source.to_abbreviated_string(config), + value: menu.source.to_abbreviated_string(&config), span, }), } @@ -547,9 +553,9 @@ pub(crate) fn add_description_menu( menu: &ParsedMenu, engine_state: Arc, stack: &Stack, - config: &Config, + config: Arc, ) -> Result { - let name = menu.name.to_expanded_string("", config); + let name = menu.name.to_expanded_string("", &config); let mut description_menu = DescriptionMenu::default().with_name(&name); let span = menu.menu_type.span(); @@ -608,7 +614,7 @@ pub(crate) fn add_description_menu( } } - let marker = menu.marker.to_expanded_string("", config); + let marker = menu.marker.to_expanded_string("", &config); description_menu = description_menu.with_marker(&marker); let only_buffer_difference = menu.only_buffer_difference.as_bool()?; @@ -617,7 +623,7 @@ pub(crate) fn add_description_menu( let span = menu.source.span(); match &menu.source { Value::Nothing { .. } => { - let completer = Box::new(NuHelpCompleter::new(engine_state)); + let completer = Box::new(NuHelpCompleter::new(engine_state, config)); Ok(line_editor.with_menu(ReedlineMenu::WithCompleter { menu: Box::new(description_menu), completer, @@ -638,7 +644,7 @@ pub(crate) fn add_description_menu( } _ => Err(ShellError::UnsupportedConfigValue { expected: "closure or omitted value".to_string(), - value: menu.source.to_abbreviated_string(config), + value: menu.source.to_abbreviated_string(&config), span: menu.source.span(), }), } diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index 251876022a..817950a445 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -43,7 +43,7 @@ use std::{ io::{self, IsTerminal, Write}, panic::{catch_unwind, AssertUnwindSafe}, path::{Path, PathBuf}, - sync::{atomic::Ordering, Arc}, + sync::Arc, time::{Duration, Instant}, }; use sysinfo::System; @@ -268,14 +268,14 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { if let Err(err) = engine_state.merge_env(&mut stack, cwd) { report_error_new(engine_state, &err); } + // Check whether $env.NU_USE_IR is set, so that the user can change it in the REPL + // Temporary while IR eval is optional + stack.use_ir = stack.has_env_var(engine_state, "NU_USE_IR"); perf!("merge env", start_time, use_color); start_time = std::time::Instant::now(); - // Reset the ctrl-c handler - if let Some(ctrlc) = &mut engine_state.ctrlc { - ctrlc.store(false, Ordering::SeqCst); - } - perf!("reset ctrlc", start_time, use_color); + engine_state.reset_signals(); + perf!("reset signals", start_time, use_color); start_time = std::time::Instant::now(); // Right before we start our prompt and take input from the user, @@ -297,7 +297,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { perf!("env-change hook", start_time, use_color); let engine_reference = Arc::new(engine_state.clone()); - let config = engine_state.get_config(); + let config = stack.get_config(engine_state); start_time = std::time::Instant::now(); // Find the configured cursor shapes for each mode @@ -323,7 +323,6 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { engine_state: engine_reference.clone(), // STACK-REFERENCE 1 stack: stack_arc.clone(), - config: config.clone(), })) .with_validator(Box::new(NuValidator { engine_state: engine_reference.clone(), @@ -336,6 +335,14 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) { .with_quick_completions(config.quick_completions) .with_partial_completions(config.partial_completions) .with_ansi_colors(config.use_ansi_coloring) + .with_cwd(Some( + engine_state + .cwd(None) + .map(|cwd| cwd.into_std_path_buf()) + .unwrap_or_default() + .to_string_lossy() + .to_string(), + )) .with_cursor_config(cursor_config); perf!("reedline builder", start_time, use_color); @@ -666,13 +673,14 @@ fn prepare_history_metadata( line_editor: &mut Reedline, ) { if !s.is_empty() && line_editor.has_last_command_context() { - #[allow(deprecated)] let result = line_editor .update_last_command_context(&|mut c| { c.start_timestamp = Some(chrono::Utc::now()); c.hostname = hostname.map(str::to_string); - - c.cwd = Some(StateWorkingSet::new(engine_state).get_cwd()); + c.cwd = engine_state + .cwd(None) + .ok() + .map(|path| path.to_string_lossy().to_string()); c }) .into_diagnostic(); diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index 41ef168390..08dcbb1346 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -6,7 +6,7 @@ use nu_parser::{flatten_block, parse, FlatShape}; use nu_protocol::{ ast::{Block, Expr, Expression, PipelineRedirection, RecordItem}, engine::{EngineState, Stack, StateWorkingSet}, - Config, Span, + Span, }; use reedline::{Highlighter, StyledText}; use std::sync::Arc; @@ -14,15 +14,14 @@ use std::sync::Arc; pub struct NuHighlighter { pub engine_state: Arc, pub stack: Arc, - pub config: Config, } impl Highlighter for NuHighlighter { fn highlight(&self, line: &str, _cursor: usize) -> StyledText { trace!("highlighting: {}", line); - let highlight_resolved_externals = - self.engine_state.get_config().highlight_resolved_externals; + let config = self.stack.get_config(&self.engine_state); + let highlight_resolved_externals = config.highlight_resolved_externals; let mut working_set = StateWorkingSet::new(&self.engine_state); let block = parse(&mut working_set, None, line.as_bytes(), false); let (shapes, global_span_offset) = { @@ -88,7 +87,7 @@ impl Highlighter for NuHighlighter { .to_string(); let mut add_colored_token = |shape: &FlatShape, text: String| { - output.push((get_shape_color(shape.as_str(), &self.config), text)); + output.push((get_shape_color(shape.as_str(), &config), text)); }; match shape.1 { @@ -128,9 +127,9 @@ impl Highlighter for NuHighlighter { let start = part.start - span.start; let end = part.end - span.start; let text = next_token[start..end].to_string(); - let mut style = get_shape_color(shape.as_str(), &self.config); + let mut style = get_shape_color(shape.as_str(), &config); if highlight { - style = get_matching_brackets_style(style, &self.config); + style = get_matching_brackets_style(style, &config); } output.push((style, text)); } diff --git a/crates/nu-cli/src/util.rs b/crates/nu-cli/src/util.rs index d3cf73056f..bcee53c9b0 100644 --- a/crates/nu-cli/src/util.rs +++ b/crates/nu-cli/src/util.rs @@ -262,6 +262,11 @@ fn evaluate_source( return Ok(Some(1)); } + if let Some(err) = working_set.compile_errors.first() { + report_error(&working_set, err); + // Not a fatal error, for now + } + (output, working_set.render()) }; diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index 03f37925f5..590979251e 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -107,7 +107,7 @@ fn subcommand_completer() -> NuCompleter { } #[test] -fn variables_dollar_sign_with_varialblecompletion() { +fn variables_dollar_sign_with_variablecompletion() { let (_, _, engine, stack) = new_engine(); let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); @@ -721,7 +721,7 @@ fn file_completion_quoted() { "`te#st.txt`".to_string(), "`te'st.txt`".to_string(), "`te(st).txt`".to_string(), - format!("`{}`", folder("test dir".into())), + format!("`{}`", folder("test dir")), ]; match_suggestions(expected_paths, suggestions); @@ -907,6 +907,11 @@ fn variables_completions() { // Match results match_suggestions(expected, suggestions); + + let suggestions = completer.complete("$", 1); + let expected: Vec = vec!["$actor".into(), "$env".into(), "$in".into(), "$nu".into()]; + + match_suggestions(expected, suggestions); } #[test] diff --git a/crates/nu-cli/tests/completions/support/completions_helpers.rs b/crates/nu-cli/tests/completions/support/completions_helpers.rs index 027ac9d997..623d6fbc5a 100644 --- a/crates/nu-cli/tests/completions/support/completions_helpers.rs +++ b/crates/nu-cli/tests/completions/support/completions_helpers.rs @@ -1,5 +1,6 @@ use nu_engine::eval_block; use nu_parser::parse; +use nu_path::{AbsolutePathBuf, PathBuf}; use nu_protocol::{ debugger::WithoutDebug, engine::{EngineState, Stack, StateWorkingSet}, @@ -7,14 +8,14 @@ use nu_protocol::{ }; use nu_test_support::fs; use reedline::Suggestion; -use std::path::{PathBuf, MAIN_SEPARATOR}; +use std::path::MAIN_SEPARATOR; fn create_default_context() -> EngineState { nu_command::add_shell_command_context(nu_cmd_lang::create_default_context()) } // creates a new engine with the current path into the completions fixtures folder -pub fn new_engine() -> (PathBuf, String, EngineState, Stack) { +pub fn new_engine() -> (AbsolutePathBuf, String, EngineState, Stack) { // Target folder inside assets let dir = fs::fixtures().join("completions"); let dir_str = dir @@ -69,7 +70,7 @@ pub fn new_engine() -> (PathBuf, String, EngineState, Stack) { } // creates a new engine with the current path into the completions fixtures folder -pub fn new_dotnu_engine() -> (PathBuf, String, EngineState, Stack) { +pub fn new_dotnu_engine() -> (AbsolutePathBuf, String, EngineState, Stack) { // Target folder inside assets let dir = fs::fixtures().join("dotnu_completions"); let dir_str = dir @@ -114,7 +115,7 @@ pub fn new_dotnu_engine() -> (PathBuf, String, EngineState, Stack) { (dir, dir_str, engine_state, stack) } -pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) { +pub fn new_quote_engine() -> (AbsolutePathBuf, String, EngineState, Stack) { // Target folder inside assets let dir = fs::fixtures().join("quoted_completions"); let dir_str = dir @@ -149,7 +150,7 @@ pub fn new_quote_engine() -> (PathBuf, String, EngineState, Stack) { (dir, dir_str, engine_state, stack) } -pub fn new_partial_engine() -> (PathBuf, String, EngineState, Stack) { +pub fn new_partial_engine() -> (AbsolutePathBuf, String, EngineState, Stack) { // Target folder inside assets let dir = fs::fixtures().join("partial_completions"); let dir_str = dir @@ -205,16 +206,15 @@ pub fn match_suggestions(expected: Vec, suggestions: Vec) { } // append the separator to the converted path -pub fn folder(path: PathBuf) -> String { +pub fn folder(path: impl Into) -> String { let mut converted_path = file(path); converted_path.push(MAIN_SEPARATOR); - converted_path } // convert a given path to string -pub fn file(path: PathBuf) -> String { - path.into_os_string().into_string().unwrap_or_default() +pub fn file(path: impl Into) -> String { + path.into().into_os_string().into_string().unwrap() } // merge_input executes the given input into the engine @@ -223,7 +223,7 @@ pub fn merge_input( input: &[u8], engine_state: &mut EngineState, stack: &mut Stack, - dir: PathBuf, + dir: AbsolutePathBuf, ) -> Result<(), ShellError> { let (block, delta) = { let mut working_set = StateWorkingSet::new(engine_state); diff --git a/crates/nu-cmd-base/README.md b/crates/nu-cmd-base/README.md new file mode 100644 index 0000000000..28d299928a --- /dev/null +++ b/crates/nu-cmd-base/README.md @@ -0,0 +1,5 @@ +Utilities used by the different `nu-command`/`nu-cmd-*` crates, should not contain any full `Command` implementations. + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-cmd-base/src/input_handler.rs b/crates/nu-cmd-base/src/input_handler.rs index d81193e190..7d61f90cb0 100644 --- a/crates/nu-cmd-base/src/input_handler.rs +++ b/crates/nu-cmd-base/src/input_handler.rs @@ -1,5 +1,5 @@ -use nu_protocol::{ast::CellPath, PipelineData, ShellError, Span, Value}; -use std::sync::{atomic::AtomicBool, Arc}; +use nu_protocol::{ast::CellPath, PipelineData, ShellError, Signals, Span, Value}; +use std::sync::Arc; pub trait CmdArgument { fn take_cell_paths(&mut self) -> Option>; @@ -40,7 +40,7 @@ pub fn operate( mut arg: A, input: PipelineData, span: Span, - ctrlc: Option>, + signals: &Signals, ) -> Result where A: CmdArgument + Send + Sync + 'static, @@ -55,7 +55,7 @@ where _ => cmd(&v, &arg, span), } }, - ctrlc, + signals, ), Some(column_paths) => { let arg = Arc::new(arg); @@ -79,7 +79,7 @@ where } v }, - ctrlc, + signals, ) } } diff --git a/crates/nu-cmd-base/src/lib.rs b/crates/nu-cmd-base/src/lib.rs index 250a57d741..c2608f8466 100644 --- a/crates/nu-cmd-base/src/lib.rs +++ b/crates/nu-cmd-base/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] pub mod formats; pub mod hook; pub mod input_handler; diff --git a/crates/nu-cmd-base/src/util.rs b/crates/nu-cmd-base/src/util.rs index 905c990a9e..e485243877 100644 --- a/crates/nu-cmd-base/src/util.rs +++ b/crates/nu-cmd-base/src/util.rs @@ -1,3 +1,4 @@ +use nu_path::AbsolutePathBuf; use nu_protocol::{ engine::{EngineState, Stack}, Range, ShellError, Span, Value, @@ -15,6 +16,7 @@ pub fn get_init_cwd() -> PathBuf { pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf { engine_state .cwd(Some(stack)) + .map(AbsolutePathBuf::into_std_path_buf) .unwrap_or(crate::util::get_init_cwd()) } diff --git a/crates/nu-cmd-extra/src/extra/bits/and.rs b/crates/nu-cmd-extra/src/extra/bits/and.rs index 538cf6e60f..234b4e5cc1 100644 --- a/crates/nu-cmd-extra/src/extra/bits/and.rs +++ b/crates/nu-cmd-extra/src/extra/bits/and.rs @@ -79,7 +79,7 @@ impl Command for BitsAnd { input.map( move |value| binary_op(&value, &target, little_endian, |(l, r)| l & r, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/bits/into.rs b/crates/nu-cmd-extra/src/extra/bits/into.rs index cf85f92ac5..9ff8bd0a05 100644 --- a/crates/nu-cmd-extra/src/extra/bits/into.rs +++ b/crates/nu-cmd-extra/src/extra/bits/into.rs @@ -1,6 +1,9 @@ +use std::io::{self, Read, Write}; + use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; use num_traits::ToPrimitive; pub struct Arguments { @@ -118,12 +121,43 @@ fn into_bits( let cell_paths = call.rest(engine_state, stack, 0)?; let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); - if let PipelineData::ByteStream(stream, ..) = input { - // TODO: in the future, we may want this to stream out, converting each to bytes - Ok(Value::binary(stream.into_bytes()?, head).into_pipeline_data()) + if let PipelineData::ByteStream(stream, metadata) = input { + Ok(PipelineData::ByteStream( + byte_stream_to_bits(stream, head), + metadata, + )) } else { let args = Arguments { cell_paths }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) + } +} + +fn byte_stream_to_bits(stream: ByteStream, head: Span) -> ByteStream { + if let Some(mut reader) = stream.reader() { + let mut is_first = true; + ByteStream::from_fn( + head, + Signals::empty(), + ByteStreamType::String, + move |buffer| { + let mut byte = [0]; + if reader.read(&mut byte[..]).err_span(head)? > 0 { + // Format the byte as bits + if is_first { + is_first = false; + } else { + buffer.push(b' '); + } + write!(buffer, "{:08b}", byte[0]).expect("format failed"); + Ok(true) + } else { + // EOF + Ok(false) + } + }, + ) + } else { + ByteStream::read(io::empty(), head, Signals::empty(), ByteStreamType::String) } } diff --git a/crates/nu-cmd-extra/src/extra/bits/not.rs b/crates/nu-cmd-extra/src/extra/bits/not.rs index e4c344f137..405cc79d7e 100644 --- a/crates/nu-cmd-extra/src/extra/bits/not.rs +++ b/crates/nu-cmd-extra/src/extra/bits/not.rs @@ -82,7 +82,7 @@ impl Command for BitsNot { number_size, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/bits/or.rs b/crates/nu-cmd-extra/src/extra/bits/or.rs index 2352d65c23..a0af2dc8d0 100644 --- a/crates/nu-cmd-extra/src/extra/bits/or.rs +++ b/crates/nu-cmd-extra/src/extra/bits/or.rs @@ -80,7 +80,7 @@ impl Command for BitsOr { input.map( move |value| binary_op(&value, &target, little_endian, |(l, r)| l | r, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/bits/rotate_left.rs b/crates/nu-cmd-extra/src/extra/bits/rotate_left.rs index cbd9d17eb5..5bb9e42f6b 100644 --- a/crates/nu-cmd-extra/src/extra/bits/rotate_left.rs +++ b/crates/nu-cmd-extra/src/extra/bits/rotate_left.rs @@ -86,7 +86,7 @@ impl Command for BitsRol { bits, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/bits/rotate_right.rs b/crates/nu-cmd-extra/src/extra/bits/rotate_right.rs index 0aea603ce1..31e17891a3 100644 --- a/crates/nu-cmd-extra/src/extra/bits/rotate_right.rs +++ b/crates/nu-cmd-extra/src/extra/bits/rotate_right.rs @@ -86,7 +86,7 @@ impl Command for BitsRor { bits, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/bits/shift_left.rs b/crates/nu-cmd-extra/src/extra/bits/shift_left.rs index 049408c24a..6a67a45e0e 100644 --- a/crates/nu-cmd-extra/src/extra/bits/shift_left.rs +++ b/crates/nu-cmd-extra/src/extra/bits/shift_left.rs @@ -88,7 +88,7 @@ impl Command for BitsShl { bits, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/bits/shift_right.rs b/crates/nu-cmd-extra/src/extra/bits/shift_right.rs index d66db68ee5..e45e10ac94 100644 --- a/crates/nu-cmd-extra/src/extra/bits/shift_right.rs +++ b/crates/nu-cmd-extra/src/extra/bits/shift_right.rs @@ -88,7 +88,7 @@ impl Command for BitsShr { bits, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/bits/xor.rs b/crates/nu-cmd-extra/src/extra/bits/xor.rs index 65c3be4e1a..4a71487137 100644 --- a/crates/nu-cmd-extra/src/extra/bits/xor.rs +++ b/crates/nu-cmd-extra/src/extra/bits/xor.rs @@ -80,7 +80,7 @@ impl Command for BitsXor { input.map( move |value| binary_op(&value, &target, little_endian, |(l, r)| l ^ r, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/conversions/fmt.rs b/crates/nu-cmd-extra/src/extra/conversions/fmt.rs index fec0745dac..3b682291e7 100644 --- a/crates/nu-cmd-extra/src/extra/conversions/fmt.rs +++ b/crates/nu-cmd-extra/src/extra/conversions/fmt.rs @@ -59,7 +59,7 @@ fn fmt( ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn action(input: &Value, _args: &CellPathOnlyArgs, span: Span) -> Value { diff --git a/crates/nu-cmd-extra/src/extra/filters/each_while.rs b/crates/nu-cmd-extra/src/extra/filters/each_while.rs index 58679c8eea..1f30e5b9b9 100644 --- a/crates/nu-cmd-extra/src/extra/filters/each_while.rs +++ b/crates/nu-cmd-extra/src/extra/filters/each_while.rs @@ -89,7 +89,7 @@ impl Command for EachWhile { } }) .fuse() - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } PipelineData::ByteStream(stream, ..) => { let span = stream.span(); @@ -107,7 +107,7 @@ impl Command for EachWhile { } }) .fuse() - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } else { Ok(PipelineData::Empty) } diff --git a/crates/nu-cmd-extra/src/extra/filters/update_cells.rs b/crates/nu-cmd-extra/src/extra/filters/update_cells.rs index c90e933410..b102c13c0d 100644 --- a/crates/nu-cmd-extra/src/extra/filters/update_cells.rs +++ b/crates/nu-cmd-extra/src/extra/filters/update_cells.rs @@ -108,7 +108,7 @@ impl Command for UpdateCells { columns, span: head, } - .into_pipeline_data(head, engine_state.ctrlc.clone()) + .into_pipeline_data(head, engine_state.signals().clone()) .set_metadata(metadata)) } } diff --git a/crates/nu-cmd-extra/src/extra/formats/to/html.rs b/crates/nu-cmd-extra/src/extra/formats/to/html.rs index 83d0b58b3f..f02f11cf84 100644 --- a/crates/nu-cmd-extra/src/extra/formats/to/html.rs +++ b/crates/nu-cmd-extra/src/extra/formats/to/html.rs @@ -239,7 +239,7 @@ fn to_html( let partial = call.has_flag(engine_state, stack, "partial")?; let list = call.has_flag(engine_state, stack, "list")?; let theme: Option> = call.get_flag(engine_state, stack, "theme")?; - let config = engine_state.get_config(); + let config = &stack.get_config(engine_state); let vec_of_values = input.into_iter().collect::>(); let headers = merge_descriptors(&vec_of_values); diff --git a/crates/nu-cmd-extra/src/extra/math/arccos.rs b/crates/nu-cmd-extra/src/extra/math/arccos.rs index 120fc4df98..5d0a659380 100644 --- a/crates/nu-cmd-extra/src/extra/math/arccos.rs +++ b/crates/nu-cmd-extra/src/extra/math/arccos.rs @@ -45,7 +45,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, use_degrees), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/math/arccosh.rs b/crates/nu-cmd-extra/src/extra/math/arccosh.rs index 30e0d2cfb6..532a388b3f 100644 --- a/crates/nu-cmd-extra/src/extra/math/arccosh.rs +++ b/crates/nu-cmd-extra/src/extra/math/arccosh.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/arcsin.rs b/crates/nu-cmd-extra/src/extra/math/arcsin.rs index a68e0648ef..a85c438367 100644 --- a/crates/nu-cmd-extra/src/extra/math/arcsin.rs +++ b/crates/nu-cmd-extra/src/extra/math/arcsin.rs @@ -45,7 +45,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, use_degrees), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/math/arcsinh.rs b/crates/nu-cmd-extra/src/extra/math/arcsinh.rs index 67addfdba2..91cb814a32 100644 --- a/crates/nu-cmd-extra/src/extra/math/arcsinh.rs +++ b/crates/nu-cmd-extra/src/extra/math/arcsinh.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/arctan.rs b/crates/nu-cmd-extra/src/extra/math/arctan.rs index 9c14203312..f52c8bd40c 100644 --- a/crates/nu-cmd-extra/src/extra/math/arctan.rs +++ b/crates/nu-cmd-extra/src/extra/math/arctan.rs @@ -45,7 +45,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, use_degrees), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/math/arctanh.rs b/crates/nu-cmd-extra/src/extra/math/arctanh.rs index 920e56eeb6..7791b56948 100644 --- a/crates/nu-cmd-extra/src/extra/math/arctanh.rs +++ b/crates/nu-cmd-extra/src/extra/math/arctanh.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/cos.rs b/crates/nu-cmd-extra/src/extra/math/cos.rs index 633c131b8b..252c7dbbd6 100644 --- a/crates/nu-cmd-extra/src/extra/math/cos.rs +++ b/crates/nu-cmd-extra/src/extra/math/cos.rs @@ -44,7 +44,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, use_degrees), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/math/cosh.rs b/crates/nu-cmd-extra/src/extra/math/cosh.rs index a772540b5c..e46d3c4df8 100644 --- a/crates/nu-cmd-extra/src/extra/math/cosh.rs +++ b/crates/nu-cmd-extra/src/extra/math/cosh.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/exp.rs b/crates/nu-cmd-extra/src/extra/math/exp.rs index b89d6f553f..d8f6a52899 100644 --- a/crates/nu-cmd-extra/src/extra/math/exp.rs +++ b/crates/nu-cmd-extra/src/extra/math/exp.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/ln.rs b/crates/nu-cmd-extra/src/extra/math/ln.rs index dd9782b467..694192bc8e 100644 --- a/crates/nu-cmd-extra/src/extra/math/ln.rs +++ b/crates/nu-cmd-extra/src/extra/math/ln.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/sin.rs b/crates/nu-cmd-extra/src/extra/math/sin.rs index 883007d1ed..0caedbabe7 100644 --- a/crates/nu-cmd-extra/src/extra/math/sin.rs +++ b/crates/nu-cmd-extra/src/extra/math/sin.rs @@ -44,7 +44,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, use_degrees), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/math/sinh.rs b/crates/nu-cmd-extra/src/extra/math/sinh.rs index c768dba739..d40db3bcb5 100644 --- a/crates/nu-cmd-extra/src/extra/math/sinh.rs +++ b/crates/nu-cmd-extra/src/extra/math/sinh.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/math/tan.rs b/crates/nu-cmd-extra/src/extra/math/tan.rs index e10807279d..97c5d2ff93 100644 --- a/crates/nu-cmd-extra/src/extra/math/tan.rs +++ b/crates/nu-cmd-extra/src/extra/math/tan.rs @@ -44,7 +44,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, use_degrees), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/math/tanh.rs b/crates/nu-cmd-extra/src/extra/math/tanh.rs index 4d09f93cf4..6679d04fe5 100644 --- a/crates/nu-cmd-extra/src/extra/math/tanh.rs +++ b/crates/nu-cmd-extra/src/extra/math/tanh.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-extra/src/extra/platform/ansi/gradient.rs b/crates/nu-cmd-extra/src/extra/platform/ansi/gradient.rs index 21c7e42a61..5934b57a5d 100644 --- a/crates/nu-cmd-extra/src/extra/platform/ansi/gradient.rs +++ b/crates/nu-cmd-extra/src/extra/platform/ansi/gradient.rs @@ -140,7 +140,7 @@ fn operate( ret } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-cmd-extra/src/extra/strings/encode_decode/hex.rs b/crates/nu-cmd-extra/src/extra/strings/encode_decode/hex.rs index 7628d6e240..be681b382a 100644 --- a/crates/nu-cmd-extra/src/extra/strings/encode_decode/hex.rs +++ b/crates/nu-cmd-extra/src/extra/strings/encode_decode/hex.rs @@ -88,7 +88,7 @@ pub fn operate( cell_paths, }; - general_operate(action, args, input, call.head, engine_state.ctrlc.clone()) + general_operate(action, args, input, call.head, engine_state.signals()) } fn action( diff --git a/crates/nu-cmd-extra/src/extra/strings/format/command.rs b/crates/nu-cmd-extra/src/extra/strings/format/command.rs index 50e89e61e3..ef9b6f2baf 100644 --- a/crates/nu-cmd-extra/src/extra/strings/format/command.rs +++ b/crates/nu-cmd-extra/src/extra/strings/format/command.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::{ast::PathMember, engine::StateWorkingSet, ListStream}; +use nu_protocol::{ast::PathMember, engine::StateWorkingSet, Config, ListStream}; #[derive(Clone)] pub struct FormatPattern; @@ -43,6 +43,8 @@ impl Command for FormatPattern { let it_id = working_set.add_variable(b"$it".to_vec(), call.head, Type::Any, false); stack.add_var(it_id, input_val.clone()); + let config = stack.get_config(engine_state); + match specified_pattern { Err(e) => Err(e), Ok(pattern) => { @@ -56,7 +58,7 @@ impl Command for FormatPattern { string_span.start + 1, )?; - format(input_val, &ops, engine_state, call.head) + format(input_val, &ops, engine_state, &config, call.head) } } } @@ -181,33 +183,30 @@ fn format( input_data: Value, format_operations: &[FormatOperation], engine_state: &EngineState, + config: &Config, head_span: Span, ) -> Result { let data_as_value = input_data; // We can only handle a Record or a List of Records match data_as_value { - Value::Record { .. } => { - match format_record(format_operations, &data_as_value, engine_state) { - Ok(value) => Ok(PipelineData::Value(Value::string(value, head_span), None)), - Err(value) => Err(value), - } - } + Value::Record { .. } => match format_record(format_operations, &data_as_value, config) { + Ok(value) => Ok(PipelineData::Value(Value::string(value, head_span), None)), + Err(value) => Err(value), + }, Value::List { vals, .. } => { let mut list = vec![]; for val in vals.iter() { match val { - Value::Record { .. } => { - match format_record(format_operations, val, engine_state) { - Ok(value) => { - list.push(Value::string(value, head_span)); - } - Err(value) => { - return Err(value); - } + Value::Record { .. } => match format_record(format_operations, val, config) { + Ok(value) => { + list.push(Value::string(value, head_span)); } - } + Err(value) => { + return Err(value); + } + }, Value::Error { error, .. } => return Err(*error.clone()), _ => { return Err(ShellError::OnlySupportsThisInputType { @@ -220,7 +219,7 @@ fn format( } } - Ok(ListStream::new(list.into_iter(), head_span, engine_state.ctrlc.clone()).into()) + Ok(ListStream::new(list.into_iter(), head_span, engine_state.signals().clone()).into()) } // Unwrapping this ShellError is a bit unfortunate. // Ideally, its Span would be preserved. @@ -237,9 +236,8 @@ fn format( fn format_record( format_operations: &[FormatOperation], data_as_value: &Value, - engine_state: &EngineState, + config: &Config, ) -> Result { - let config = engine_state.get_config(); let mut output = String::new(); for op in format_operations { diff --git a/crates/nu-cmd-extra/src/extra/strings/str_/case/mod.rs b/crates/nu-cmd-extra/src/extra/strings/str_/case/mod.rs index 980a1b83fc..bfa54921e2 100644 --- a/crates/nu-cmd-extra/src/extra/strings/str_/case/mod.rs +++ b/crates/nu-cmd-extra/src/extra/strings/str_/case/mod.rs @@ -44,7 +44,7 @@ where case_operation, cell_paths, }; - general_operate(action, args, input, call.head, engine_state.ctrlc.clone()) + general_operate(action, args, input, call.head, engine_state.signals()) } fn action(input: &Value, args: &Arguments, head: Span) -> Value diff --git a/crates/nu-cmd-extra/src/lib.rs b/crates/nu-cmd-extra/src/lib.rs index 2a3d70e6cd..f6f97c2f96 100644 --- a/crates/nu-cmd-extra/src/lib.rs +++ b/crates/nu-cmd-extra/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod example_test; pub mod extra; pub use extra::*; diff --git a/crates/nu-cmd-extra/tests/commands/bits/into.rs b/crates/nu-cmd-extra/tests/commands/bits/into.rs new file mode 100644 index 0000000000..b7e7700583 --- /dev/null +++ b/crates/nu-cmd-extra/tests/commands/bits/into.rs @@ -0,0 +1,13 @@ +use nu_test_support::nu; + +#[test] +fn byte_stream_into_bits() { + let result = nu!("[0x[01] 0x[02 03]] | bytes collect | into bits"); + assert_eq!("00000001 00000010 00000011", result.out); +} + +#[test] +fn byte_stream_into_bits_is_stream() { + let result = nu!("[0x[01] 0x[02 03]] | bytes collect | into bits | describe"); + assert_eq!("string (stream)", result.out); +} diff --git a/crates/nu-cmd-extra/tests/commands/bits/mod.rs b/crates/nu-cmd-extra/tests/commands/bits/mod.rs new file mode 100644 index 0000000000..0d4ee04b0d --- /dev/null +++ b/crates/nu-cmd-extra/tests/commands/bits/mod.rs @@ -0,0 +1 @@ +mod into; diff --git a/crates/nu-cmd-extra/tests/commands/mod.rs b/crates/nu-cmd-extra/tests/commands/mod.rs index 2354122e35..fd216cecb6 100644 --- a/crates/nu-cmd-extra/tests/commands/mod.rs +++ b/crates/nu-cmd-extra/tests/commands/mod.rs @@ -1 +1,2 @@ +mod bits; mod bytes; diff --git a/crates/nu-cmd-lang/src/core_commands/const_.rs b/crates/nu-cmd-lang/src/core_commands/const_.rs index f780c5ada9..5b3d03443a 100644 --- a/crates/nu-cmd-lang/src/core_commands/const_.rs +++ b/crates/nu-cmd-lang/src/core_commands/const_.rs @@ -46,6 +46,9 @@ impl Command for Const { call: &Call, _input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let var_id = if let Some(id) = call.positional_nth(0).and_then(|pos| pos.as_var()) { id } else { diff --git a/crates/nu-cmd-lang/src/core_commands/do_.rs b/crates/nu-cmd-lang/src/core_commands/do_.rs index adf13cc0bb..bf29a2159a 100644 --- a/crates/nu-cmd-lang/src/core_commands/do_.rs +++ b/crates/nu-cmd-lang/src/core_commands/do_.rs @@ -81,6 +81,10 @@ impl Command for Do { bind_args_to(&mut callee_stack, &block.signature, rest, head)?; let eval_block_with_early_return = get_eval_block_with_early_return(engine_state); + + // Applies to all block evaluation once set true + callee_stack.use_ir = caller_stack.has_env_var(engine_state, "NU_USE_IR"); + let result = eval_block_with_early_return(engine_state, &mut callee_stack, block, input); if has_env { diff --git a/crates/nu-cmd-lang/src/core_commands/error_make.rs b/crates/nu-cmd-lang/src/core_commands/error_make.rs index 987e083cbc..07efcd7885 100644 --- a/crates/nu-cmd-lang/src/core_commands/error_make.rs +++ b/crates/nu-cmd-lang/src/core_commands/error_make.rs @@ -56,16 +56,7 @@ impl Command for ErrorMake { Example { description: "Create a simple custom error", example: r#"error make {msg: "my custom error message"}"#, - result: Some(Value::error( - ShellError::GenericError { - error: "my custom error message".into(), - msg: "".into(), - span: None, - help: None, - inner: vec![], - }, - Span::unknown(), - )), + result: None, }, Example { description: "Create a more complex custom error", @@ -82,16 +73,7 @@ impl Command for ErrorMake { } help: "A help string, suggesting a fix to the user" # optional }"#, - result: Some(Value::error( - ShellError::GenericError { - error: "my custom error message".into(), - msg: "my custom label text".into(), - span: Some(Span::new(123, 456)), - help: Some("A help string, suggesting a fix to the user".into()), - inner: vec![], - }, - Span::unknown(), - )), + result: None, }, Example { description: diff --git a/crates/nu-cmd-lang/src/core_commands/for_.rs b/crates/nu-cmd-lang/src/core_commands/for_.rs index 9410be74c7..36df743e5f 100644 --- a/crates/nu-cmd-lang/src/core_commands/for_.rs +++ b/crates/nu-cmd-lang/src/core_commands/for_.rs @@ -1,5 +1,5 @@ use nu_engine::{command_prelude::*, get_eval_block, get_eval_expression}; -use nu_protocol::engine::CommandType; +use nu_protocol::{engine::CommandType, Signals}; #[derive(Clone)] pub struct For; @@ -48,6 +48,9 @@ impl Command for For { call: &Call, _input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let head = call.head; let var_id = call .positional_nth(0) @@ -72,7 +75,6 @@ impl Command for For { let value = eval_expression(engine_state, stack, keyword_expr)?; - let ctrlc = engine_state.ctrlc.clone(); let engine_state = engine_state.clone(); let block = engine_state.get_block(block_id); @@ -82,9 +84,7 @@ impl Command for For { match value { Value::List { vals, .. } => { for x in vals.into_iter() { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - break; - } + engine_state.signals().check(head)?; // with_env() is used here to ensure that each iteration uses // a different set of environment variables. @@ -116,7 +116,8 @@ impl Command for For { } } Value::Range { val, .. } => { - for x in val.into_range_iter(span, ctrlc) { + for x in val.into_range_iter(span, Signals::empty()) { + engine_state.signals().check(head)?; stack.add_var(var_id, x); match eval_block(&engine_state, stack, block, PipelineData::empty()) { diff --git a/crates/nu-cmd-lang/src/core_commands/if_.rs b/crates/nu-cmd-lang/src/core_commands/if_.rs index 738d901759..8667843770 100644 --- a/crates/nu-cmd-lang/src/core_commands/if_.rs +++ b/crates/nu-cmd-lang/src/core_commands/if_.rs @@ -60,6 +60,9 @@ impl Command for If { call: &Call, input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let cond = call.positional_nth(0).expect("checked through parser"); let then_block = call .positional_nth(1) @@ -99,6 +102,9 @@ impl Command for If { call: &Call, input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let cond = call.positional_nth(0).expect("checked through parser"); let then_block = call .positional_nth(1) diff --git a/crates/nu-cmd-lang/src/core_commands/let_.rs b/crates/nu-cmd-lang/src/core_commands/let_.rs index f2da628c31..46324ef39e 100644 --- a/crates/nu-cmd-lang/src/core_commands/let_.rs +++ b/crates/nu-cmd-lang/src/core_commands/let_.rs @@ -46,6 +46,9 @@ impl Command for Let { call: &Call, input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let var_id = call .positional_nth(0) .expect("checked through parser") diff --git a/crates/nu-cmd-lang/src/core_commands/loop_.rs b/crates/nu-cmd-lang/src/core_commands/loop_.rs index a9c642ca3c..f495c8d3ae 100644 --- a/crates/nu-cmd-lang/src/core_commands/loop_.rs +++ b/crates/nu-cmd-lang/src/core_commands/loop_.rs @@ -37,6 +37,10 @@ impl Command for Loop { call: &Call, _input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; + let head = call.head; let block_id = call .positional_nth(0) .expect("checked through parser") @@ -49,9 +53,7 @@ impl Command for Loop { let stack = &mut stack.push_redirection(None, None); loop { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - break; - } + engine_state.signals().check(head)?; match eval_block(engine_state, stack, block, PipelineData::empty()) { Err(ShellError::Break { .. }) => { diff --git a/crates/nu-cmd-lang/src/core_commands/match_.rs b/crates/nu-cmd-lang/src/core_commands/match_.rs index d28a59cbad..c3a3d61216 100644 --- a/crates/nu-cmd-lang/src/core_commands/match_.rs +++ b/crates/nu-cmd-lang/src/core_commands/match_.rs @@ -43,6 +43,9 @@ impl Command for Match { call: &Call, input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let value: Value = call.req(engine_state, stack, 0)?; let matches = call .positional_nth(1) diff --git a/crates/nu-cmd-lang/src/core_commands/mut_.rs b/crates/nu-cmd-lang/src/core_commands/mut_.rs index 5db3c929af..b729590027 100644 --- a/crates/nu-cmd-lang/src/core_commands/mut_.rs +++ b/crates/nu-cmd-lang/src/core_commands/mut_.rs @@ -46,6 +46,9 @@ impl Command for Mut { call: &Call, input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let var_id = call .positional_nth(0) .expect("checked through parser") diff --git a/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs b/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs index e8b51fb59b..d6d3ae745a 100644 --- a/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs +++ b/crates/nu-cmd-lang/src/core_commands/overlay/use_.rs @@ -65,9 +65,9 @@ impl Command for OverlayUse { name_arg.item = trim_quotes_str(&name_arg.item).to_string(); let maybe_origin_module_id = - if let Some(overlay_expr) = call.get_parser_info("overlay_expr") { + if let Some(overlay_expr) = call.get_parser_info(caller_stack, "overlay_expr") { if let Expr::Overlay(module_id) = &overlay_expr.expr { - module_id + *module_id } else { return Err(ShellError::NushellFailedSpanned { msg: "Not an overlay".to_string(), @@ -110,7 +110,7 @@ impl Command for OverlayUse { // a) adding a new overlay // b) refreshing an active overlay (the origin module changed) - let module = engine_state.get_module(*module_id); + let module = engine_state.get_module(module_id); // Evaluate the export-env block (if any) and keep its environment if let Some(block_id) = module.env_block { @@ -118,7 +118,7 @@ impl Command for OverlayUse { &name_arg.item, engine_state, caller_stack, - get_dirs_var_from_call(call), + get_dirs_var_from_call(caller_stack, call), )?; let block = engine_state.get_block(block_id); diff --git a/crates/nu-cmd-lang/src/core_commands/try_.rs b/crates/nu-cmd-lang/src/core_commands/try_.rs index f99825b88d..2309897a1b 100644 --- a/crates/nu-cmd-lang/src/core_commands/try_.rs +++ b/crates/nu-cmd-lang/src/core_commands/try_.rs @@ -47,6 +47,9 @@ impl Command for Try { call: &Call, input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; let try_block = call .positional_nth(0) .expect("checked through parser") diff --git a/crates/nu-cmd-lang/src/core_commands/use_.rs b/crates/nu-cmd-lang/src/core_commands/use_.rs index b0f3648304..7f544fa5d4 100644 --- a/crates/nu-cmd-lang/src/core_commands/use_.rs +++ b/crates/nu-cmd-lang/src/core_commands/use_.rs @@ -57,7 +57,7 @@ This command is a parser keyword. For details, check: let Some(Expression { expr: Expr::ImportPattern(import_pattern), .. - }) = call.get_parser_info("import_pattern") + }) = call.get_parser_info(caller_stack, "import_pattern") else { return Err(ShellError::GenericError { error: "Unexpected import".into(), @@ -68,6 +68,9 @@ This command is a parser keyword. For details, check: }); }; + // Necessary so that we can modify the stack. + let import_pattern = import_pattern.clone(); + if let Some(module_id) = import_pattern.head.id { // Add constants for var_id in &import_pattern.constants { @@ -99,7 +102,7 @@ This command is a parser keyword. For details, check: &module_arg_str, engine_state, caller_stack, - get_dirs_var_from_call(call), + get_dirs_var_from_call(caller_stack, call), )?; let maybe_parent = maybe_file_path .as_ref() diff --git a/crates/nu-cmd-lang/src/core_commands/while_.rs b/crates/nu-cmd-lang/src/core_commands/while_.rs index 646b95c82e..a67c47fcab 100644 --- a/crates/nu-cmd-lang/src/core_commands/while_.rs +++ b/crates/nu-cmd-lang/src/core_commands/while_.rs @@ -46,6 +46,10 @@ impl Command for While { call: &Call, _input: PipelineData, ) -> Result { + // This is compiled specially by the IR compiler. The code here is never used when + // running in IR mode. + let call = call.assert_ast_call()?; + let head = call.head; let cond = call.positional_nth(0).expect("checked through parser"); let block_id = call .positional_nth(1) @@ -59,9 +63,7 @@ impl Command for While { let stack = &mut stack.push_redirection(None, None); loop { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - break; - } + engine_state.signals().check(head)?; let result = eval_expression(engine_state, stack, cond)?; diff --git a/crates/nu-cmd-lang/src/lib.rs b/crates/nu-cmd-lang/src/lib.rs index 427a535d5d..41022cc231 100644 --- a/crates/nu-cmd-lang/src/lib.rs +++ b/crates/nu-cmd-lang/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod core_commands; mod default_context; pub mod example_support; diff --git a/crates/nu-cmd-plugin/src/commands/mod.rs b/crates/nu-cmd-plugin/src/commands/mod.rs index 3e927747f1..17ff32faa1 100644 --- a/crates/nu-cmd-plugin/src/commands/mod.rs +++ b/crates/nu-cmd-plugin/src/commands/mod.rs @@ -1,5 +1,3 @@ mod plugin; -mod register; pub use plugin::*; -pub use register::Register; diff --git a/crates/nu-cmd-plugin/src/commands/plugin/stop.rs b/crates/nu-cmd-plugin/src/commands/plugin/stop.rs index d74ee59a3f..f90eac8411 100644 --- a/crates/nu-cmd-plugin/src/commands/plugin/stop.rs +++ b/crates/nu-cmd-plugin/src/commands/plugin/stop.rs @@ -72,7 +72,7 @@ impl Command for PluginStop { error: format!("Failed to stop the `{}` plugin", name.item), msg: "couldn't find a plugin with this name".into(), span: Some(name.span), - help: Some("you may need to `register` the plugin first".into()), + help: Some("you may need to `plugin add` the plugin first".into()), inner: vec![], }) } diff --git a/crates/nu-cmd-plugin/src/commands/register.rs b/crates/nu-cmd-plugin/src/commands/register.rs deleted file mode 100644 index 2c10456db7..0000000000 --- a/crates/nu-cmd-plugin/src/commands/register.rs +++ /dev/null @@ -1,80 +0,0 @@ -use nu_engine::command_prelude::*; -use nu_protocol::engine::CommandType; - -#[derive(Clone)] -pub struct Register; - -impl Command for Register { - fn name(&self) -> &str { - "register" - } - - fn usage(&self) -> &str { - "Register a plugin." - } - - fn signature(&self) -> nu_protocol::Signature { - Signature::build("register") - .input_output_types(vec![(Type::Nothing, Type::Nothing)]) - .required( - "plugin", - SyntaxShape::Filepath, - "Path of executable for plugin.", - ) - .optional( - "signature", - SyntaxShape::Any, - "Block with signature description as json object.", - ) - .named( - "shell", - SyntaxShape::Filepath, - "path of shell used to run plugin (cmd, sh, python, etc)", - Some('s'), - ) - .category(Category::Plugin) - } - - fn extra_usage(&self) -> &str { - r#" -Deprecated in favor of `plugin add` and `plugin use`. - -This command is a parser keyword. For details, check: - https://www.nushell.sh/book/thinking_in_nu.html -"# - .trim() - } - - fn search_terms(&self) -> Vec<&str> { - vec!["add"] - } - - fn command_type(&self) -> CommandType { - CommandType::Keyword - } - - fn run( - &self, - _engine_state: &EngineState, - _stack: &mut Stack, - _call: &Call, - _input: PipelineData, - ) -> Result { - Ok(PipelineData::empty()) - } - - fn examples(&self) -> Vec { - vec![ - Example { - description: "Register `nu_plugin_query` plugin from ~/.cargo/bin/ dir", - example: r#"register ~/.cargo/bin/nu_plugin_query"#, - result: None, - }, - Example { - description: "Register `nu_plugin_query` plugin from `nu -c` (writes/updates $nu.plugin-path)", - example: r#"let plugin = ((which nu).path.0 | path dirname | path join 'nu_plugin_query'); nu -c $'register ($plugin); version'"#, - result: None, - }, - ] - } -} diff --git a/crates/nu-cmd-plugin/src/default_context.rs b/crates/nu-cmd-plugin/src/default_context.rs index 601dd52cfc..1ef6a905dd 100644 --- a/crates/nu-cmd-plugin/src/default_context.rs +++ b/crates/nu-cmd-plugin/src/default_context.rs @@ -18,7 +18,6 @@ pub fn add_plugin_command_context(mut engine_state: EngineState) -> EngineState PluginRm, PluginStop, PluginUse, - Register, ); working_set.render() diff --git a/crates/nu-color-config/README.md b/crates/nu-color-config/README.md new file mode 100644 index 0000000000..f03753685c --- /dev/null +++ b/crates/nu-color-config/README.md @@ -0,0 +1,5 @@ +Logic to resolve colors for syntax highlighting and output formatting + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-color-config/src/lib.rs b/crates/nu-color-config/src/lib.rs index 3bd4dc9e4d..77ec9d0e79 100644 --- a/crates/nu-color-config/src/lib.rs +++ b/crates/nu-color-config/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod color_config; mod matching_brackets_style; mod nu_style; diff --git a/crates/nu-color-config/src/style_computer.rs b/crates/nu-color-config/src/style_computer.rs index 91907c1428..edc8786a53 100644 --- a/crates/nu-color-config/src/style_computer.rs +++ b/crates/nu-color-config/src/style_computer.rs @@ -1,6 +1,6 @@ use crate::{color_record_to_nustyle, lookup_ansi_color_style, text_style::Alignment, TextStyle}; use nu_ansi_term::{Color, Style}; -use nu_engine::{env::get_config, ClosureEvalOnce}; +use nu_engine::ClosureEvalOnce; use nu_protocol::{ cli_error::CliError, engine::{Closure, EngineState, Stack, StateWorkingSet}, @@ -114,7 +114,7 @@ impl<'a> StyleComputer<'a> { // The main constructor. pub fn from_config(engine_state: &'a EngineState, stack: &'a Stack) -> StyleComputer<'a> { - let config = get_config(engine_state, stack); + let config = stack.get_config(engine_state); // Create the hashmap #[rustfmt::skip] diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index 9c49a69ff5..ea501578b6 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -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 } \ No newline at end of file +tempfile = { workspace = true } diff --git a/crates/nu-command/README.md b/crates/nu-command/README.md new file mode 100644 index 0000000000..1cbb8cd9f2 --- /dev/null +++ b/crates/nu-command/README.md @@ -0,0 +1,7 @@ +This crate contains the majority of our commands + +We allow ourselves to move some of the commands in `nu-command` to `nu-cmd-*` crates as needed. + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-command/src/bytes/add.rs b/crates/nu-command/src/bytes/add.rs index 8514718cfd..ab31f74b12 100644 --- a/crates/nu-command/src/bytes/add.rs +++ b/crates/nu-command/src/bytes/add.rs @@ -78,7 +78,7 @@ impl Command for BytesAdd { end, cell_paths, }; - operate(add, arg, input, call.head, engine_state.ctrlc.clone()) + operate(add, arg, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/at.rs b/crates/nu-command/src/bytes/at.rs index c5164c3550..5e95f4fd62 100644 --- a/crates/nu-command/src/bytes/at.rs +++ b/crates/nu-command/src/bytes/at.rs @@ -83,7 +83,7 @@ impl Command for BytesAt { cell_paths, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/build_.rs b/crates/nu-command/src/bytes/build_.rs index f6b1327621..9a3599a071 100644 --- a/crates/nu-command/src/bytes/build_.rs +++ b/crates/nu-command/src/bytes/build_.rs @@ -49,10 +49,8 @@ impl Command for BytesBuild { _input: PipelineData, ) -> Result { let mut output = vec![]; - for val in call.rest_iter_flattened(0, |expr| { - let eval_expression = get_eval_expression(engine_state); - eval_expression(engine_state, stack, expr) - })? { + let eval_expression = get_eval_expression(engine_state); + for val in call.rest_iter_flattened(engine_state, stack, eval_expression, 0)? { let val_span = val.span(); match val { Value::Binary { mut val, .. } => output.append(&mut val), diff --git a/crates/nu-command/src/bytes/collect.rs b/crates/nu-command/src/bytes/collect.rs index 74ea3e5d14..afb70bfb36 100644 --- a/crates/nu-command/src/bytes/collect.rs +++ b/crates/nu-command/src/bytes/collect.rs @@ -60,7 +60,12 @@ impl Command for BytesCollect { ) .flatten(); - let output = ByteStream::from_result_iter(iter, span, None, ByteStreamType::Binary); + let output = ByteStream::from_result_iter( + iter, + span, + engine_state.signals().clone(), + ByteStreamType::Binary, + ); Ok(PipelineData::ByteStream(output, metadata)) } diff --git a/crates/nu-command/src/bytes/ends_with.rs b/crates/nu-command/src/bytes/ends_with.rs index d6174a189c..8e3966716c 100644 --- a/crates/nu-command/src/bytes/ends_with.rs +++ b/crates/nu-command/src/bytes/ends_with.rs @@ -102,7 +102,7 @@ impl Command for BytesEndsWith { pattern, cell_paths, }; - operate(ends_with, arg, input, head, engine_state.ctrlc.clone()) + operate(ends_with, arg, input, head, engine_state.signals()) } } diff --git a/crates/nu-command/src/bytes/index_of.rs b/crates/nu-command/src/bytes/index_of.rs index bdf51b24d9..e10bd6c200 100644 --- a/crates/nu-command/src/bytes/index_of.rs +++ b/crates/nu-command/src/bytes/index_of.rs @@ -71,7 +71,7 @@ impl Command for BytesIndexOf { all: call.has_flag(engine_state, stack, "all")?, cell_paths, }; - operate(index_of, arg, input, call.head, engine_state.ctrlc.clone()) + operate(index_of, arg, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/length.rs b/crates/nu-command/src/bytes/length.rs index aaaf23e0a5..78b3d31eac 100644 --- a/crates/nu-command/src/bytes/length.rs +++ b/crates/nu-command/src/bytes/length.rs @@ -46,7 +46,7 @@ impl Command for BytesLen { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 1)?; let arg = CellPathOnlyArgs::from(cell_paths); - operate(length, arg, input, call.head, engine_state.ctrlc.clone()) + operate(length, arg, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/remove.rs b/crates/nu-command/src/bytes/remove.rs index 9afef07e8b..34d3c427f4 100644 --- a/crates/nu-command/src/bytes/remove.rs +++ b/crates/nu-command/src/bytes/remove.rs @@ -73,7 +73,7 @@ impl Command for BytesRemove { all: call.has_flag(engine_state, stack, "all")?, }; - operate(remove, arg, input, call.head, engine_state.ctrlc.clone()) + operate(remove, arg, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/replace.rs b/crates/nu-command/src/bytes/replace.rs index ab7ede7588..db2b6fb790 100644 --- a/crates/nu-command/src/bytes/replace.rs +++ b/crates/nu-command/src/bytes/replace.rs @@ -73,7 +73,7 @@ impl Command for BytesReplace { all: call.has_flag(engine_state, stack, "all")?, }; - operate(replace, arg, input, call.head, engine_state.ctrlc.clone()) + operate(replace, arg, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/reverse.rs b/crates/nu-command/src/bytes/reverse.rs index 171add213d..fd769eaa40 100644 --- a/crates/nu-command/src/bytes/reverse.rs +++ b/crates/nu-command/src/bytes/reverse.rs @@ -42,7 +42,7 @@ impl Command for BytesReverse { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let arg = CellPathOnlyArgs::from(cell_paths); - operate(reverse, arg, input, call.head, engine_state.ctrlc.clone()) + operate(reverse, arg, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/bytes/starts_with.rs b/crates/nu-command/src/bytes/starts_with.rs index 92cc16f02c..89cc4f7afc 100644 --- a/crates/nu-command/src/bytes/starts_with.rs +++ b/crates/nu-command/src/bytes/starts_with.rs @@ -79,7 +79,7 @@ impl Command for BytesStartsWith { pattern, cell_paths, }; - operate(starts_with, arg, input, head, engine_state.ctrlc.clone()) + operate(starts_with, arg, input, head, engine_state.signals()) } } diff --git a/crates/nu-command/src/conversions/fill.rs b/crates/nu-command/src/conversions/fill.rs index 6507e4a368..eaf8c05da1 100644 --- a/crates/nu-command/src/conversions/fill.rs +++ b/crates/nu-command/src/conversions/fill.rs @@ -165,7 +165,7 @@ fn fill( cell_paths, }; - operate(action, arg, input, call.head, engine_state.ctrlc.clone()) + operate(action, arg, input, call.head, engine_state.signals()) } fn action(input: &Value, args: &Arguments, span: Span) -> Value { diff --git a/crates/nu-command/src/conversions/into/binary.rs b/crates/nu-command/src/conversions/into/binary.rs index 8eb7715754..e5c34ae6bc 100644 --- a/crates/nu-command/src/conversions/into/binary.rs +++ b/crates/nu-command/src/conversions/into/binary.rs @@ -138,7 +138,7 @@ fn into_binary( cell_paths, compact: call.has_flag(engine_state, stack, "compact")?, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } } diff --git a/crates/nu-command/src/conversions/into/bool.rs b/crates/nu-command/src/conversions/into/bool.rs index b1d433cb93..0fcd33b4a3 100644 --- a/crates/nu-command/src/conversions/into/bool.rs +++ b/crates/nu-command/src/conversions/into/bool.rs @@ -107,7 +107,7 @@ fn into_bool( ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn string_to_boolean(s: &str, span: Span) -> Result { diff --git a/crates/nu-command/src/conversions/into/datetime.rs b/crates/nu-command/src/conversions/into/datetime.rs index b928bebe23..8dc0340ba1 100644 --- a/crates/nu-command/src/conversions/into/datetime.rs +++ b/crates/nu-command/src/conversions/into/datetime.rs @@ -141,7 +141,7 @@ impl Command for SubCommand { zone_options, cell_paths, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } } diff --git a/crates/nu-command/src/conversions/into/duration.rs b/crates/nu-command/src/conversions/into/duration.rs index 21494f3bcc..b459dd04b1 100644 --- a/crates/nu-command/src/conversions/into/duration.rs +++ b/crates/nu-command/src/conversions/into/duration.rs @@ -166,7 +166,7 @@ fn into_duration( ret } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/conversions/into/filesize.rs b/crates/nu-command/src/conversions/into/filesize.rs index 010c031b2a..5be167e30c 100644 --- a/crates/nu-command/src/conversions/into/filesize.rs +++ b/crates/nu-command/src/conversions/into/filesize.rs @@ -68,7 +68,7 @@ impl Command for SubCommand { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/conversions/into/float.rs b/crates/nu-command/src/conversions/into/float.rs index 9ccd7ea03f..43556bb5ef 100644 --- a/crates/nu-command/src/conversions/into/float.rs +++ b/crates/nu-command/src/conversions/into/float.rs @@ -49,7 +49,7 @@ impl Command for SubCommand { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/conversions/into/glob.rs b/crates/nu-command/src/conversions/into/glob.rs index e5d03093f4..ffc3655330 100644 --- a/crates/nu-command/src/conversions/into/glob.rs +++ b/crates/nu-command/src/conversions/into/glob.rs @@ -87,7 +87,7 @@ fn glob_helper( Ok(Value::glob(stream.into_string()?, false, head).into_pipeline_data()) } else { let args = Arguments { cell_paths }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } } diff --git a/crates/nu-command/src/conversions/into/int.rs b/crates/nu-command/src/conversions/into/int.rs index a3f1c92a4f..d4bfa61639 100644 --- a/crates/nu-command/src/conversions/into/int.rs +++ b/crates/nu-command/src/conversions/into/int.rs @@ -158,7 +158,7 @@ impl Command for SubCommand { signed, cell_paths, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/conversions/into/record.rs b/crates/nu-command/src/conversions/into/record.rs index e867f06e15..1f332d9c85 100644 --- a/crates/nu-command/src/conversions/into/record.rs +++ b/crates/nu-command/src/conversions/into/record.rs @@ -125,7 +125,7 @@ fn into_record( ), }, Value::Range { val, .. } => Value::record( - val.into_range_iter(span, engine_state.ctrlc.clone()) + val.into_range_iter(span, engine_state.signals().clone()) .enumerate() .map(|(idx, val)| (format!("{idx}"), val)) .collect(), diff --git a/crates/nu-command/src/conversions/into/string.rs b/crates/nu-command/src/conversions/into/string.rs index 7c7d69cf4e..2b35272b3e 100644 --- a/crates/nu-command/src/conversions/into/string.rs +++ b/crates/nu-command/src/conversions/into/string.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; use nu_protocol::{into_code, Config}; @@ -7,7 +9,7 @@ use num_format::ToFormattedString; struct Arguments { decimals_value: Option, cell_paths: Option>, - config: Config, + config: Arc, } impl CmdArgument for Arguments { @@ -174,13 +176,13 @@ fn string_helper( }) } } else { - let config = engine_state.get_config().clone(); + let config = stack.get_config(engine_state); let args = Arguments { decimals_value, cell_paths, config, }; - operate(action, args, input, head, engine_state.ctrlc.clone()) + operate(action, args, input, head, engine_state.signals()) } } diff --git a/crates/nu-command/src/conversions/into/value.rs b/crates/nu-command/src/conversions/into/value.rs index 4bf7e68f53..e8787e75bb 100644 --- a/crates/nu-command/src/conversions/into/value.rs +++ b/crates/nu-command/src/conversions/into/value.rs @@ -57,14 +57,12 @@ impl Command for IntoValue { call: &Call, input: PipelineData, ) -> Result { - let engine_state = engine_state.clone(); let metadata = input.metadata(); - let ctrlc = engine_state.ctrlc.clone(); let span = call.head; - let display_as_filesizes = call.has_flag(&engine_state, stack, "prefer-filesizes")?; + let display_as_filesizes = call.has_flag(engine_state, stack, "prefer-filesizes")?; // the columns to update - let columns: Option = call.get_flag(&engine_state, stack, "columns")?; + let columns: Option = call.get_flag(engine_state, stack, "columns")?; let columns: Option> = match columns { Some(val) => Some( val.into_list()? @@ -81,7 +79,7 @@ impl Command for IntoValue { display_as_filesizes, span, } - .into_pipeline_data(span, ctrlc) + .into_pipeline_data(span, engine_state.signals().clone()) .set_metadata(metadata)) } } diff --git a/crates/nu-command/src/database/commands/into_sqlite.rs b/crates/nu-command/src/database/commands/into_sqlite.rs index f3a2e3622a..88a6114ab3 100644 --- a/crates/nu-command/src/database/commands/into_sqlite.rs +++ b/crates/nu-command/src/database/commands/into_sqlite.rs @@ -2,13 +2,8 @@ use crate::database::values::sqlite::{open_sqlite_db, values_to_sql}; use nu_engine::command_prelude::*; use itertools::Itertools; -use std::{ - path::Path, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, -}; +use nu_protocol::Signals; +use std::path::Path; pub const DEFAULT_TABLE_NAME: &str = "main"; @@ -188,23 +183,18 @@ fn operate( let file_name: Spanned = call.req(engine_state, stack, 0)?; let table_name: Option> = call.get_flag(engine_state, stack, "table-name")?; let table = Table::new(&file_name, table_name)?; - let ctrl_c = engine_state.ctrlc.clone(); - - match action(input, table, span, ctrl_c) { - Ok(val) => Ok(val.into_pipeline_data()), - Err(e) => Err(e), - } + Ok(action(input, table, span, engine_state.signals())?.into_pipeline_data()) } fn action( input: PipelineData, table: Table, span: Span, - ctrl_c: Option>, + signals: &Signals, ) -> Result { match input { PipelineData::ListStream(stream, _) => { - insert_in_transaction(stream.into_iter(), span, table, ctrl_c) + insert_in_transaction(stream.into_iter(), span, table, signals) } PipelineData::Value( Value::List { @@ -212,9 +202,9 @@ fn action( internal_span, }, _, - ) => insert_in_transaction(vals.into_iter(), internal_span, table, ctrl_c), + ) => insert_in_transaction(vals.into_iter(), internal_span, table, signals), PipelineData::Value(val, _) => { - insert_in_transaction(std::iter::once(val), span, table, ctrl_c) + insert_in_transaction(std::iter::once(val), span, table, signals) } _ => Err(ShellError::OnlySupportsThisInputType { exp_input_type: "list".into(), @@ -229,7 +219,7 @@ fn insert_in_transaction( stream: impl Iterator, span: Span, mut table: Table, - ctrl_c: Option>, + signals: &Signals, ) -> Result { let mut stream = stream.peekable(); let first_val = match stream.peek() { @@ -251,17 +241,15 @@ fn insert_in_transaction( let tx = table.try_init(&first_val)?; for stream_value in stream { - if let Some(ref ctrlc) = ctrl_c { - if ctrlc.load(Ordering::Relaxed) { - tx.rollback().map_err(|e| ShellError::GenericError { - error: "Failed to rollback SQLite transaction".into(), - msg: e.to_string(), - span: None, - help: None, - inner: Vec::new(), - })?; - return Err(ShellError::InterruptedByUser { span: None }); - } + if let Err(err) = signals.check(span) { + tx.rollback().map_err(|e| ShellError::GenericError { + error: "Failed to rollback SQLite transaction".into(), + msg: e.to_string(), + span: None, + help: None, + inner: Vec::new(), + })?; + return Err(err); } let val = stream_value.as_record()?; diff --git a/crates/nu-command/src/database/values/sqlite.rs b/crates/nu-command/src/database/values/sqlite.rs index 483da7672e..f253ba1cbd 100644 --- a/crates/nu-command/src/database/values/sqlite.rs +++ b/crates/nu-command/src/database/values/sqlite.rs @@ -2,7 +2,7 @@ use super::definitions::{ db_column::DbColumn, db_constraint::DbConstraint, db_foreignkey::DbForeignKey, db_index::DbIndex, db_table::DbTable, }; -use nu_protocol::{CustomValue, PipelineData, Record, ShellError, Span, Spanned, Value}; +use nu_protocol::{CustomValue, PipelineData, Record, ShellError, Signals, Span, Spanned, Value}; use rusqlite::{ types::ValueRef, Connection, DatabaseName, Error as SqliteError, OpenFlags, Row, Statement, ToSql, @@ -12,7 +12,6 @@ use std::{ fs::File, io::Read, path::{Path, PathBuf}, - sync::{atomic::AtomicBool, Arc}, }; const SQLITE_MAGIC_BYTES: &[u8] = "SQLite format 3\0".as_bytes(); @@ -24,25 +23,21 @@ pub struct SQLiteDatabase { // 1) YAGNI, 2) it's not obvious how cloning a connection could work, 3) state // management gets tricky quick. Revisit this approach if we find a compelling use case. pub path: PathBuf, - #[serde(skip)] + #[serde(skip, default = "Signals::empty")] // this understandably can't be serialized. think that's OK, I'm not aware of a // reason why a CustomValue would be serialized outside of a plugin - ctrlc: Option>, + signals: Signals, } impl SQLiteDatabase { - pub fn new(path: &Path, ctrlc: Option>) -> Self { + pub fn new(path: &Path, signals: Signals) -> Self { Self { path: PathBuf::from(path), - ctrlc, + signals, } } - pub fn try_from_path( - path: &Path, - span: Span, - ctrlc: Option>, - ) -> Result { + pub fn try_from_path(path: &Path, span: Span, signals: Signals) -> Result { let mut file = File::open(path).map_err(|e| ShellError::ReadingFile { msg: e.to_string(), span, @@ -56,7 +51,7 @@ impl SQLiteDatabase { }) .and_then(|_| { if buf == SQLITE_MAGIC_BYTES { - Ok(SQLiteDatabase::new(path, ctrlc)) + Ok(SQLiteDatabase::new(path, signals)) } else { Err(ShellError::ReadingFile { msg: "Not a SQLite file".into(), @@ -72,7 +67,7 @@ impl SQLiteDatabase { Value::Custom { val, .. } => match val.as_any().downcast_ref::() { Some(db) => Ok(Self { path: db.path.clone(), - ctrlc: db.ctrlc.clone(), + signals: db.signals.clone(), }), None => Err(ShellError::CantConvert { to_type: "database".into(), @@ -107,16 +102,8 @@ impl SQLiteDatabase { call_span: Span, ) -> Result { let conn = open_sqlite_db(&self.path, call_span)?; - - let stream = run_sql_query(conn, sql, params, self.ctrlc.clone()).map_err(|e| { - ShellError::GenericError { - error: "Failed to query SQLite database".into(), - msg: e.to_string(), - span: Some(sql.span), - help: None, - inner: vec![], - } - })?; + let stream = run_sql_query(conn, sql, params, &self.signals) + .map_err(|e| e.into_shell_error(sql.span, "Failed to query SQLite database"))?; Ok(stream) } @@ -352,12 +339,7 @@ impl SQLiteDatabase { impl CustomValue for SQLiteDatabase { fn clone_value(&self, span: Span) -> Value { - let cloned = SQLiteDatabase { - path: self.path.clone(), - ctrlc: self.ctrlc.clone(), - }; - - Value::custom(Box::new(cloned), span) + Value::custom(Box::new(self.clone()), span) } fn type_name(&self) -> String { @@ -366,13 +348,8 @@ impl CustomValue for SQLiteDatabase { fn to_base_value(&self, span: Span) -> Result { let db = open_sqlite_db(&self.path, span)?; - read_entire_sqlite_db(db, span, self.ctrlc.clone()).map_err(|e| ShellError::GenericError { - error: "Failed to read from SQLite database".into(), - msg: e.to_string(), - span: Some(span), - help: None, - inner: vec![], - }) + read_entire_sqlite_db(db, span, &self.signals) + .map_err(|e| e.into_shell_error(span, "Failed to read from SQLite database")) } fn as_any(&self) -> &dyn std::any::Any { @@ -396,20 +373,12 @@ impl CustomValue for SQLiteDatabase { fn follow_path_string( &self, _self_span: Span, - _column_name: String, + column_name: String, path_span: Span, ) -> Result { let db = open_sqlite_db(&self.path, path_span)?; - - read_single_table(db, _column_name, path_span, self.ctrlc.clone()).map_err(|e| { - ShellError::GenericError { - error: "Failed to read from SQLite database".into(), - msg: e.to_string(), - span: Some(path_span), - help: None, - inner: vec![], - } - }) + read_single_table(db, column_name, path_span, &self.signals) + .map_err(|e| e.into_shell_error(path_span, "Failed to read from SQLite database")) } fn typetag_name(&self) -> &'static str { @@ -426,12 +395,12 @@ pub fn open_sqlite_db(path: &Path, call_span: Span) -> Result, params: NuSqlParams, - ctrlc: Option>, -) -> Result { + signals: &Signals, +) -> Result { let stmt = conn.prepare(&sql.item)?; - - prepared_statement_to_nu_list(stmt, params, sql.span, ctrlc) + prepared_statement_to_nu_list(stmt, params, sql.span, signals) } // This is taken from to text local_into_string but tweaks it a bit so that certain formatting does not happen @@ -534,23 +502,56 @@ pub fn nu_value_to_params(value: Value) -> Result { } } +#[derive(Debug)] +enum SqliteOrShellError { + SqliteError(SqliteError), + ShellError(ShellError), +} + +impl From for SqliteOrShellError { + fn from(error: SqliteError) -> Self { + Self::SqliteError(error) + } +} + +impl From for SqliteOrShellError { + fn from(error: ShellError) -> Self { + Self::ShellError(error) + } +} + +impl SqliteOrShellError { + fn into_shell_error(self, span: Span, msg: &str) -> ShellError { + match self { + Self::SqliteError(err) => ShellError::GenericError { + error: msg.into(), + msg: err.to_string(), + span: Some(span), + help: None, + inner: Vec::new(), + }, + Self::ShellError(err) => err, + } + } +} + fn read_single_table( conn: Connection, table_name: String, call_span: Span, - ctrlc: Option>, -) -> Result { + signals: &Signals, +) -> Result { // TODO: Should use params here? let stmt = conn.prepare(&format!("SELECT * FROM [{table_name}]"))?; - prepared_statement_to_nu_list(stmt, NuSqlParams::default(), call_span, ctrlc) + prepared_statement_to_nu_list(stmt, NuSqlParams::default(), call_span, signals) } fn prepared_statement_to_nu_list( mut stmt: Statement, params: NuSqlParams, call_span: Span, - ctrlc: Option>, -) -> Result { + signals: &Signals, +) -> Result { let column_names = stmt .column_names() .into_iter() @@ -576,11 +577,7 @@ fn prepared_statement_to_nu_list( let mut row_values = vec![]; for row_result in row_results { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - // return whatever we have so far, let the caller decide whether to use it - return Ok(Value::list(row_values, call_span)); - } - + signals.check(call_span)?; if let Ok(row_value) = row_result { row_values.push(row_value); } @@ -606,11 +603,7 @@ fn prepared_statement_to_nu_list( let mut row_values = vec![]; for row_result in row_results { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - // return whatever we have so far, let the caller decide whether to use it - return Ok(Value::list(row_values, call_span)); - } - + signals.check(call_span)?; if let Ok(row_value) = row_result { row_values.push(row_value); } @@ -626,8 +619,8 @@ fn prepared_statement_to_nu_list( fn read_entire_sqlite_db( conn: Connection, call_span: Span, - ctrlc: Option>, -) -> Result { + signals: &Signals, +) -> Result { let mut tables = Record::new(); let mut get_table_names = @@ -638,12 +631,8 @@ fn read_entire_sqlite_db( let table_name: String = row?; // TODO: Should use params here? let table_stmt = conn.prepare(&format!("select * from [{table_name}]"))?; - let rows = prepared_statement_to_nu_list( - table_stmt, - NuSqlParams::default(), - call_span, - ctrlc.clone(), - )?; + let rows = + prepared_statement_to_nu_list(table_stmt, NuSqlParams::default(), call_span, signals)?; tables.push(table_name, rows); } @@ -710,7 +699,7 @@ mod test { #[test] fn can_read_empty_db() { let db = open_connection_in_memory().unwrap(); - let converted_db = read_entire_sqlite_db(db, Span::test_data(), None).unwrap(); + let converted_db = read_entire_sqlite_db(db, Span::test_data(), &Signals::empty()).unwrap(); let expected = Value::test_record(Record::new()); @@ -730,7 +719,7 @@ mod test { [], ) .unwrap(); - let converted_db = read_entire_sqlite_db(db, Span::test_data(), None).unwrap(); + let converted_db = read_entire_sqlite_db(db, Span::test_data(), &Signals::empty()).unwrap(); let expected = Value::test_record(record! { "person" => Value::test_list(vec![]), @@ -759,7 +748,7 @@ mod test { db.execute("INSERT INTO item (id, name) VALUES (456, 'foo bar')", []) .unwrap(); - let converted_db = read_entire_sqlite_db(db, span, None).unwrap(); + let converted_db = read_entire_sqlite_db(db, span, &Signals::empty()).unwrap(); let expected = Value::test_record(record! { "item" => Value::test_list( diff --git a/crates/nu-command/src/date/humanize.rs b/crates/nu-command/src/date/humanize.rs index 2815571520..d8542a540c 100644 --- a/crates/nu-command/src/date/humanize.rs +++ b/crates/nu-command/src/date/humanize.rs @@ -50,7 +50,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map(move |value| helper(value, head), engine_state.ctrlc.clone()) + input.map(move |value| helper(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/date/list_timezone.rs b/crates/nu-command/src/date/list_timezone.rs index 6f9267947d..56f7fe5376 100644 --- a/crates/nu-command/src/date/list_timezone.rs +++ b/crates/nu-command/src/date/list_timezone.rs @@ -40,7 +40,7 @@ impl Command for SubCommand { head, ) }) - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/date/to_record.rs b/crates/nu-command/src/date/to_record.rs index f9c0ceff1b..c0b09c040b 100644 --- a/crates/nu-command/src/date/to_record.rs +++ b/crates/nu-command/src/date/to_record.rs @@ -40,7 +40,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map(move |value| helper(value, head), engine_state.ctrlc.clone()) + input.map(move |value| helper(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/date/to_table.rs b/crates/nu-command/src/date/to_table.rs index 36c3f4a94a..7ce8bc171b 100644 --- a/crates/nu-command/src/date/to_table.rs +++ b/crates/nu-command/src/date/to_table.rs @@ -40,7 +40,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map(move |value| helper(value, head), engine_state.ctrlc.clone()) + input.map(move |value| helper(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/date/to_timezone.rs b/crates/nu-command/src/date/to_timezone.rs index 5f41d287ae..3d08d7271b 100644 --- a/crates/nu-command/src/date/to_timezone.rs +++ b/crates/nu-command/src/date/to_timezone.rs @@ -55,7 +55,7 @@ impl Command for SubCommand { } input.map( move |value| helper(value, head, &timezone), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/debug/debug_.rs b/crates/nu-command/src/debug/debug_.rs index c766081410..80a9067e49 100644 --- a/crates/nu-command/src/debug/debug_.rs +++ b/crates/nu-command/src/debug/debug_.rs @@ -33,7 +33,7 @@ impl Command for Debug { input: PipelineData, ) -> Result { let head = call.head; - let config = engine_state.get_config().clone(); + let config = stack.get_config(engine_state); let raw = call.has_flag(engine_state, stack, "raw")?; // Should PipelineData::Empty result in an error here? @@ -46,7 +46,7 @@ impl Command for Debug { Value::string(x.to_expanded_string(", ", &config), head) } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/debug/explain.rs b/crates/nu-command/src/debug/explain.rs index b451d6916a..710e37935a 100644 --- a/crates/nu-command/src/debug/explain.rs +++ b/crates/nu-command/src/debug/explain.rs @@ -1,6 +1,6 @@ use nu_engine::{command_prelude::*, get_eval_expression}; use nu_protocol::{ - ast::{Argument, Block, Expr, Expression}, + ast::{self, Argument, Block, Expr, Expression}, engine::Closure, }; @@ -106,7 +106,7 @@ pub fn get_pipeline_elements( fn get_arguments( engine_state: &EngineState, stack: &mut Stack, - call: &Call, + call: &ast::Call, eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result, ) -> Vec { let mut arg_value = vec![]; diff --git a/crates/nu-command/src/debug/metadata.rs b/crates/nu-command/src/debug/metadata.rs index 543e598e28..245c150cea 100644 --- a/crates/nu-command/src/debug/metadata.rs +++ b/crates/nu-command/src/debug/metadata.rs @@ -28,6 +28,10 @@ impl Command for Metadata { .category(Category::Debug) } + fn requires_ast_for_arguments(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -35,7 +39,7 @@ impl Command for Metadata { call: &Call, input: PipelineData, ) -> Result { - let arg = call.positional_nth(0); + let arg = call.positional_nth(stack, 0); let head = call.head; match arg { diff --git a/crates/nu-command/src/debug/metadata_set.rs b/crates/nu-command/src/debug/metadata_set.rs index e4ee97a76b..96f50cfdba 100644 --- a/crates/nu-command/src/debug/metadata_set.rs +++ b/crates/nu-command/src/debug/metadata_set.rs @@ -48,7 +48,7 @@ impl Command for MetadataSet { let ds_fp: Option = call.get_flag(engine_state, stack, "datasource-filepath")?; let ds_ls = call.has_flag(engine_state, stack, "datasource-ls")?; let content_type: Option = call.get_flag(engine_state, stack, "content-type")?; - + let signals = engine_state.signals().clone(); let metadata = input .metadata() .clone() @@ -58,19 +58,15 @@ impl Command for MetadataSet { match (ds_fp, ds_ls) { (Some(path), false) => Ok(input.into_pipeline_data_with_metadata( head, - engine_state.ctrlc.clone(), + signals, metadata.with_data_source(DataSource::FilePath(path.into())), )), (None, true) => Ok(input.into_pipeline_data_with_metadata( head, - engine_state.ctrlc.clone(), + signals, metadata.with_data_source(DataSource::Ls), )), - _ => Ok(input.into_pipeline_data_with_metadata( - head, - engine_state.ctrlc.clone(), - metadata, - )), + _ => Ok(input.into_pipeline_data_with_metadata(head, signals, metadata)), } } diff --git a/crates/nu-command/src/debug/mod.rs b/crates/nu-command/src/debug/mod.rs index f19ddab916..ec18c2be87 100644 --- a/crates/nu-command/src/debug/mod.rs +++ b/crates/nu-command/src/debug/mod.rs @@ -10,6 +10,7 @@ mod profile; mod timeit; mod view; mod view_files; +mod view_ir; mod view_source; mod view_span; @@ -25,5 +26,6 @@ pub use profile::DebugProfile; pub use timeit::TimeIt; pub use view::View; pub use view_files::ViewFiles; +pub use view_ir::ViewIr; pub use view_source::ViewSource; pub use view_span::ViewSpan; diff --git a/crates/nu-command/src/debug/profile.rs b/crates/nu-command/src/debug/profile.rs index 0cef979094..e8254563df 100644 --- a/crates/nu-command/src/debug/profile.rs +++ b/crates/nu-command/src/debug/profile.rs @@ -1,5 +1,8 @@ use nu_engine::{command_prelude::*, ClosureEvalOnce}; -use nu_protocol::{debugger::Profiler, engine::Closure}; +use nu_protocol::{ + debugger::{Profiler, ProfilerOptions}, + engine::Closure, +}; #[derive(Clone)] pub struct DebugProfile; @@ -28,6 +31,7 @@ impl Command for DebugProfile { Some('v'), ) .switch("expr", "Collect expression types", Some('x')) + .switch("instructions", "Collect IR instructions", Some('i')) .switch("lines", "Collect line numbers", Some('l')) .named( "max-depth", @@ -91,19 +95,23 @@ confusing the id/parent_id hierarchy. The --expr flag is helpful for investigati let collect_expanded_source = call.has_flag(engine_state, stack, "expanded-source")?; let collect_values = call.has_flag(engine_state, stack, "values")?; let collect_exprs = call.has_flag(engine_state, stack, "expr")?; + let collect_instructions = call.has_flag(engine_state, stack, "instructions")?; let collect_lines = call.has_flag(engine_state, stack, "lines")?; let max_depth = call .get_flag(engine_state, stack, "max-depth")? .unwrap_or(2); let profiler = Profiler::new( - max_depth, - collect_spans, - true, - collect_expanded_source, - collect_values, - collect_exprs, - collect_lines, + ProfilerOptions { + max_depth, + collect_spans, + collect_source: true, + collect_expanded_source, + collect_values, + collect_exprs, + collect_instructions, + collect_lines, + }, call.span(), ); diff --git a/crates/nu-command/src/debug/timeit.rs b/crates/nu-command/src/debug/timeit.rs index a445679b81..7a48644a6d 100644 --- a/crates/nu-command/src/debug/timeit.rs +++ b/crates/nu-command/src/debug/timeit.rs @@ -32,6 +32,10 @@ impl Command for TimeIt { vec!["timing", "timer", "benchmark", "measure"] } + fn requires_ast_for_arguments(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -39,13 +43,14 @@ impl Command for TimeIt { call: &Call, input: PipelineData, ) -> Result { - let command_to_run = call.positional_nth(0); + // reset outdest, so the command can write to stdout and stderr. + let stack = &mut stack.push_redirection(None, None); + + let command_to_run = call.positional_nth(stack, 0); // Get the start time after all other computation has been done. let start_time = Instant::now(); - // reset outdest, so the command can write to stdout and stderr. - let stack = &mut stack.push_redirection(None, None); if let Some(command_to_run) = command_to_run { if let Some(block_id) = command_to_run.as_block() { let eval_block = get_eval_block(engine_state); @@ -53,7 +58,8 @@ impl Command for TimeIt { eval_block(engine_state, stack, block, input)? } else { let eval_expression_with_input = get_eval_expression_with_input(engine_state); - eval_expression_with_input(engine_state, stack, command_to_run, input)?.0 + let expression = &command_to_run.clone(); + eval_expression_with_input(engine_state, stack, expression, input)?.0 } } else { PipelineData::empty() diff --git a/crates/nu-command/src/debug/view_ir.rs b/crates/nu-command/src/debug/view_ir.rs new file mode 100644 index 0000000000..72136079fd --- /dev/null +++ b/crates/nu-command/src/debug/view_ir.rs @@ -0,0 +1,171 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct ViewIr; + +impl Command for ViewIr { + fn name(&self) -> &str { + "view ir" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required( + "target", + SyntaxShape::Any, + "The name or block to view compiled code for.", + ) + .switch( + "json", + "Dump the raw block data as JSON (unstable).", + Some('j'), + ) + .switch( + "decl-id", + "Integer is a declaration ID rather than a block ID.", + Some('d'), + ) + .input_output_type(Type::Nothing, Type::String) + .category(Category::Debug) + } + + fn usage(&self) -> &str { + "View the compiled IR code for a block of code." + } + + fn extra_usage(&self) -> &str { + " +The target can be a closure, the name of a custom command, or an internal block +ID. Closure literals within IR dumps often reference the block by ID (e.g. +`closure(3231)`), so this provides an easy way to read the IR of any embedded +closures. + +The --decl-id option is provided to use a declaration ID instead, which can be +found on `call` instructions. This is sometimes better than using the name, as +the declaration may not be in scope. +" + .trim() + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let target: Value = call.req(engine_state, stack, 0)?; + let json = call.has_flag(engine_state, stack, "json")?; + let is_decl_id = call.has_flag(engine_state, stack, "decl-id")?; + + let block_id = match target { + Value::Closure { ref val, .. } => val.block_id, + // Decl by name + Value::String { ref val, .. } => { + if let Some(decl_id) = engine_state.find_decl(val.as_bytes(), &[]) { + let decl = engine_state.get_decl(decl_id); + decl.block_id().ok_or_else(|| ShellError::GenericError { + error: format!("Can't view IR for `{val}`"), + msg: "not a custom command".into(), + span: Some(target.span()), + help: Some("internal commands don't have Nushell source code".into()), + inner: vec![], + })? + } else { + return Err(ShellError::GenericError { + error: format!("Can't view IR for `{val}`"), + msg: "can't find a command with this name".into(), + span: Some(target.span()), + help: None, + inner: vec![], + }); + } + } + // Decl by ID - IR dump always shows name of decl, but sometimes it isn't in scope + Value::Int { val, .. } if is_decl_id => { + let decl_id = val + .try_into() + .ok() + .filter(|id| *id < engine_state.num_decls()) + .ok_or_else(|| ShellError::IncorrectValue { + msg: "not a valid decl id".into(), + val_span: target.span(), + call_span: call.head, + })?; + let decl = engine_state.get_decl(decl_id); + decl.block_id().ok_or_else(|| ShellError::GenericError { + error: format!("Can't view IR for `{}`", decl.name()), + msg: "not a custom command".into(), + span: Some(target.span()), + help: Some("internal commands don't have Nushell source code".into()), + inner: vec![], + })? + } + // Block by ID - often shows up in IR + Value::Int { val, .. } => val.try_into().map_err(|_| ShellError::IncorrectValue { + msg: "not a valid block id".into(), + val_span: target.span(), + call_span: call.head, + })?, + // Pass through errors + Value::Error { error, .. } => return Err(*error), + _ => { + return Err(ShellError::TypeMismatch { + err_message: "expected closure, string, or int".into(), + span: call.head, + }) + } + }; + + let Some(block) = engine_state.try_get_block(block_id) else { + return Err(ShellError::GenericError { + error: format!("Unknown block ID: {block_id}"), + msg: "ensure the block ID is correct and try again".into(), + span: Some(target.span()), + help: None, + inner: vec![], + }); + }; + + let ir_block = block + .ir_block + .as_ref() + .ok_or_else(|| ShellError::GenericError { + error: "Can't view IR for this block".into(), + msg: "block is missing compiled representation".into(), + span: block.span, + help: Some("the IrBlock is probably missing due to a compilation error".into()), + inner: vec![], + })?; + + let formatted = if json { + let formatted_instructions = ir_block + .instructions + .iter() + .map(|instruction| { + instruction + .display(engine_state, &ir_block.data) + .to_string() + }) + .collect::>(); + + serde_json::to_string_pretty(&serde_json::json!({ + "block_id": block_id, + "span": block.span, + "ir_block": ir_block, + "formatted_instructions": formatted_instructions, + })) + .map_err(|err| ShellError::GenericError { + error: "JSON serialization failed".into(), + msg: err.to_string(), + span: Some(call.head), + help: None, + inner: vec![], + })? + } else { + format!("{}", ir_block.display(engine_state)) + }; + + Ok(Value::string(formatted, call.head).into_pipeline_data()) + } +} diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 847d2349ed..4adcfe9214 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -31,6 +31,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { All, Any, Append, + Chunks, Columns, Compact, Default, @@ -154,6 +155,7 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { TimeIt, View, ViewFiles, + ViewIr, ViewSource, ViewSpan, }; @@ -289,7 +291,6 @@ pub fn add_shell_command_context(mut engine_state: EngineState) -> EngineState { ToText, ToToml, ToTsv, - Touch, Upsert, Where, ToXml, diff --git a/crates/nu-command/src/env/config/config_env.rs b/crates/nu-command/src/env/config/config_env.rs index 32bd7bba90..777b6f3aac 100644 --- a/crates/nu-command/src/env/config/config_env.rs +++ b/crates/nu-command/src/env/config/config_env.rs @@ -60,12 +60,13 @@ impl Command for ConfigEnv { let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?; let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; let cwd = engine_state.cwd(Some(stack))?; - let editor_executable = - crate::which(&editor_name, &paths, &cwd).ok_or(ShellError::ExternalCommand { + let editor_executable = crate::which(&editor_name, &paths, cwd.as_ref()).ok_or( + ShellError::ExternalCommand { label: format!("`{editor_name}` not found"), help: "Failed to find the editor executable".into(), span: call.head, - })?; + }, + )?; let Some(env_path) = engine_state.get_config_path("env-path") else { return Err(ShellError::GenericError { diff --git a/crates/nu-command/src/env/config/config_nu.rs b/crates/nu-command/src/env/config/config_nu.rs index 08695ca5bb..80f0bbc012 100644 --- a/crates/nu-command/src/env/config/config_nu.rs +++ b/crates/nu-command/src/env/config/config_nu.rs @@ -64,12 +64,13 @@ impl Command for ConfigNu { let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?; let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; let cwd = engine_state.cwd(Some(stack))?; - let editor_executable = - crate::which(&editor_name, &paths, &cwd).ok_or(ShellError::ExternalCommand { + let editor_executable = crate::which(&editor_name, &paths, cwd.as_ref()).ok_or( + ShellError::ExternalCommand { label: format!("`{editor_name}` not found"), help: "Failed to find the editor executable".into(), span: call.head, - })?; + }, + )?; let Some(config_path) = engine_state.get_config_path("config-path") else { return Err(ShellError::GenericError { diff --git a/crates/nu-command/src/env/export_env.rs b/crates/nu-command/src/env/export_env.rs index 00f2c73ef4..7b583c9959 100644 --- a/crates/nu-command/src/env/export_env.rs +++ b/crates/nu-command/src/env/export_env.rs @@ -33,6 +33,10 @@ impl Command for ExportEnv { CommandType::Keyword } + fn requires_ast_for_arguments(&self) -> bool { + true + } + fn run( &self, engine_state: &EngineState, @@ -41,7 +45,7 @@ impl Command for ExportEnv { input: PipelineData, ) -> Result { let block_id = call - .positional_nth(0) + .positional_nth(caller_stack, 0) .expect("checked through parser") .as_block() .expect("internal error: missing block"); diff --git a/crates/nu-command/src/env/source_env.rs b/crates/nu-command/src/env/source_env.rs index 0d8b118e8d..1813a92f1f 100644 --- a/crates/nu-command/src/env/source_env.rs +++ b/crates/nu-command/src/env/source_env.rs @@ -56,7 +56,7 @@ impl Command for SourceEnv { &source_filename.item, engine_state, caller_stack, - get_dirs_var_from_call(call), + get_dirs_var_from_call(caller_stack, call), )? { PathBuf::from(&path) } else { diff --git a/crates/nu-command/src/filesystem/cd.rs b/crates/nu-command/src/filesystem/cd.rs index 4e09d900d2..23faf6f67b 100644 --- a/crates/nu-command/src/filesystem/cd.rs +++ b/crates/nu-command/src/filesystem/cd.rs @@ -1,5 +1,6 @@ use nu_cmd_base::util::get_init_cwd; use nu_engine::command_prelude::*; +use nu_path::AbsolutePathBuf; use nu_utils::filesystem::{have_permission, PermissionResult}; #[derive(Clone)] @@ -43,7 +44,10 @@ impl Command for Cd { // If getting PWD failed, default to the initial directory. This way, the // user can use `cd` to recover PWD to a good state. - let cwd = engine_state.cwd(Some(stack)).unwrap_or(get_init_cwd()); + let cwd = engine_state + .cwd(Some(stack)) + .map(AbsolutePathBuf::into_std_path_buf) + .unwrap_or(get_init_cwd()); let path_val = { if let Some(path) = path_val { diff --git a/crates/nu-command/src/filesystem/du.rs b/crates/nu-command/src/filesystem/du.rs index c48a1f7ac1..d34892ace9 100644 --- a/crates/nu-command/src/filesystem/du.rs +++ b/crates/nu-command/src/filesystem/du.rs @@ -3,10 +3,9 @@ use crate::{DirBuilder, DirInfo, FileInfo}; #[allow(deprecated)] use nu_engine::{command_prelude::*, current_dir}; use nu_glob::Pattern; -use nu_protocol::NuGlob; +use nu_protocol::{NuGlob, Signals}; use serde::Deserialize; use std::path::Path; -use std::sync::{atomic::AtomicBool, Arc}; #[derive(Clone)] pub struct Du; @@ -103,7 +102,7 @@ impl Command for Du { let current_dir = current_dir(engine_state, stack)?; let paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; - let paths = if call.rest_iter(0).count() == 0 { + let paths = if !call.has_positional_args(stack, 0) { None } else { Some(paths) @@ -120,8 +119,8 @@ impl Command for Du { min_size, }; Ok( - du_for_one_pattern(args, ¤t_dir, tag, engine_state.ctrlc.clone())? - .into_pipeline_data(tag, engine_state.ctrlc.clone()), + du_for_one_pattern(args, ¤t_dir, tag, engine_state.signals())? + .into_pipeline_data(tag, engine_state.signals().clone()), ) } Some(paths) => { @@ -139,7 +138,7 @@ impl Command for Du { args, ¤t_dir, tag, - engine_state.ctrlc.clone(), + engine_state.signals(), )?) } @@ -147,7 +146,7 @@ impl Command for Du { Ok(result_iters .into_iter() .flatten() - .into_pipeline_data(tag, engine_state.ctrlc.clone())) + .into_pipeline_data(tag, engine_state.signals().clone())) } } } @@ -164,8 +163,8 @@ impl Command for Du { fn du_for_one_pattern( args: DuArgs, current_dir: &Path, - call_span: Span, - ctrl_c: Option>, + span: Span, + signals: &Signals, ) -> Result + Send, ShellError> { let exclude = args.exclude.map_or(Ok(None), move |x| { Pattern::new(x.item.as_ref()) @@ -178,7 +177,7 @@ fn du_for_one_pattern( let include_files = args.all; let mut paths = match args.path { - Some(p) => nu_engine::glob_from(&p, current_dir, call_span, None), + Some(p) => nu_engine::glob_from(&p, current_dir, span, None), // The * pattern should never fail. None => nu_engine::glob_from( &Spanned { @@ -186,7 +185,7 @@ fn du_for_one_pattern( span: Span::unknown(), }, current_dir, - call_span, + span, None, ), } @@ -205,7 +204,7 @@ fn du_for_one_pattern( let min_size = args.min_size.map(|f| f.item as u64); let params = DirBuilder { - tag: call_span, + tag: span, min: min_size, deref, exclude, @@ -217,13 +216,13 @@ fn du_for_one_pattern( match p { Ok(a) => { if a.is_dir() { - output.push(DirInfo::new(a, ¶ms, max_depth, ctrl_c.clone()).into()); - } else if let Ok(v) = FileInfo::new(a, deref, call_span) { + output.push(DirInfo::new(a, ¶ms, max_depth, span, signals)?.into()); + } else if let Ok(v) = FileInfo::new(a, deref, span) { output.push(v.into()); } } Err(e) => { - output.push(Value::error(e, call_span)); + output.push(Value::error(e, span)); } } } diff --git a/crates/nu-command/src/filesystem/glob.rs b/crates/nu-command/src/filesystem/glob.rs index b10e8893a0..212c6ceb92 100644 --- a/crates/nu-command/src/filesystem/glob.rs +++ b/crates/nu-command/src/filesystem/glob.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use std::sync::{atomic::AtomicBool, Arc}; +use nu_protocol::Signals; use wax::{Glob as WaxGlob, WalkBehavior, WalkEntry}; #[derive(Clone)] @@ -125,7 +125,6 @@ impl Command for Glob { call: &Call, _input: PipelineData, ) -> Result { - let ctrlc = engine_state.ctrlc.clone(); let span = call.head; let glob_pattern: Spanned = call.req(engine_state, stack, 0)?; let depth = call.get_flag(engine_state, stack, "depth")?; @@ -216,7 +215,14 @@ impl Command for Glob { inner: vec![], })? .flatten(); - glob_to_value(ctrlc, glob_results, no_dirs, no_files, no_symlinks, span) + glob_to_value( + engine_state.signals(), + glob_results, + no_dirs, + no_files, + no_symlinks, + span, + ) } else { let glob_results = glob .walk_with_behavior( @@ -227,12 +233,19 @@ impl Command for Glob { }, ) .flatten(); - glob_to_value(ctrlc, glob_results, no_dirs, no_files, no_symlinks, span) + glob_to_value( + engine_state.signals(), + glob_results, + no_dirs, + no_files, + no_symlinks, + span, + ) }?; Ok(result .into_iter() - .into_pipeline_data(span, engine_state.ctrlc.clone())) + .into_pipeline_data(span, engine_state.signals().clone())) } } @@ -252,7 +265,7 @@ fn convert_patterns(columns: &[Value]) -> Result, ShellError> { } fn glob_to_value<'a>( - ctrlc: Option>, + signals: &Signals, glob_results: impl Iterator>, no_dirs: bool, no_files: bool, @@ -261,10 +274,7 @@ fn glob_to_value<'a>( ) -> Result, ShellError> { let mut result: Vec = Vec::new(); for entry in glob_results { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { - result.clear(); - return Err(ShellError::InterruptedByUser { span: None }); - } + signals.check(span)?; let file_type = entry.file_type(); if !(no_dirs && file_type.is_dir() diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index a3ab1eca54..807e4f3409 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -6,14 +6,13 @@ use nu_engine::glob_from; use nu_engine::{command_prelude::*, env::current_dir}; use nu_glob::MatchOptions; use nu_path::expand_to_real_path; -use nu_protocol::{DataSource, NuGlob, PipelineMetadata}; +use nu_protocol::{DataSource, NuGlob, PipelineMetadata, Signals}; use pathdiff::diff_paths; #[cfg(unix)] use std::os::unix::fs::PermissionsExt; use std::{ path::PathBuf, - sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; @@ -93,7 +92,6 @@ impl Command for Ls { let du = call.has_flag(engine_state, stack, "du")?; let directory = call.has_flag(engine_state, stack, "directory")?; let use_mime_type = call.has_flag(engine_state, stack, "mime-type")?; - let ctrl_c = engine_state.ctrlc.clone(); let call_span = call.head; #[allow(deprecated)] let cwd = current_dir(engine_state, stack)?; @@ -110,16 +108,16 @@ impl Command for Ls { }; let pattern_arg = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; - let input_pattern_arg = if call.rest_iter(0).count() == 0 { + let input_pattern_arg = if !call.has_positional_args(stack, 0) { None } else { Some(pattern_arg) }; match input_pattern_arg { - None => Ok(ls_for_one_pattern(None, args, ctrl_c.clone(), cwd)? + None => Ok(ls_for_one_pattern(None, args, engine_state.signals(), cwd)? .into_pipeline_data_with_metadata( call_span, - ctrl_c, + engine_state.signals().clone(), PipelineMetadata { data_source: DataSource::Ls, content_type: None, @@ -131,7 +129,7 @@ impl Command for Ls { result_iters.push(ls_for_one_pattern( Some(pat), args, - ctrl_c.clone(), + engine_state.signals(), cwd.clone(), )?) } @@ -143,7 +141,7 @@ impl Command for Ls { .flatten() .into_pipeline_data_with_metadata( call_span, - ctrl_c, + engine_state.signals().clone(), PipelineMetadata { data_source: DataSource::Ls, content_type: None, @@ -215,7 +213,7 @@ impl Command for Ls { fn ls_for_one_pattern( pattern_arg: Option>, args: Args, - ctrl_c: Option>, + signals: &Signals, cwd: PathBuf, ) -> Result + Send>, ShellError> { let Args { @@ -342,7 +340,7 @@ fn ls_for_one_pattern( let mut hidden_dirs = vec![]; - let one_ctrl_c = ctrl_c.clone(); + let signals = signals.clone(); Ok(Box::new(paths_peek.filter_map(move |x| match x { Ok(path) => { let metadata = match std::fs::symlink_metadata(&path) { @@ -412,7 +410,7 @@ fn ls_for_one_pattern( call_span, long, du, - one_ctrl_c.clone(), + &signals, use_mime_type, ); match entry { @@ -474,7 +472,6 @@ fn path_contains_hidden_folder(path: &Path, folders: &[PathBuf]) -> bool { #[cfg(unix)] use std::os::unix::fs::FileTypeExt; use std::path::Path; -use std::sync::atomic::AtomicBool; pub fn get_file_type(md: &std::fs::Metadata, display_name: &str, use_mime_type: bool) -> String { let ft = md.file_type(); @@ -523,7 +520,7 @@ pub(crate) fn dir_entry_dict( span: Span, long: bool, du: bool, - ctrl_c: Option>, + signals: &Signals, use_mime_type: bool, ) -> Result { #[cfg(windows)] @@ -618,7 +615,7 @@ pub(crate) fn dir_entry_dict( if md.is_dir() { if du { let params = DirBuilder::new(Span::new(0, 2), None, false, None, false); - let dir_size = DirInfo::new(filename, ¶ms, None, ctrl_c).get_size(); + let dir_size = DirInfo::new(filename, ¶ms, None, span, signals)?.get_size(); Value::filesize(dir_size as i64, span) } else { diff --git a/crates/nu-command/src/filesystem/open.rs b/crates/nu-command/src/filesystem/open.rs index 9000359450..0351d1d9b2 100644 --- a/crates/nu-command/src/filesystem/open.rs +++ b/crates/nu-command/src/filesystem/open.rs @@ -1,7 +1,7 @@ use super::util::get_rest_for_glob_pattern; #[allow(deprecated)] use nu_engine::{command_prelude::*, current_dir, get_eval_block}; -use nu_protocol::{ByteStream, DataSource, NuGlob, PipelineMetadata}; +use nu_protocol::{ast, ByteStream, DataSource, NuGlob, PipelineMetadata}; use std::path::Path; #[cfg(feature = "sqlite")] @@ -51,13 +51,12 @@ impl Command for Open { ) -> Result { let raw = call.has_flag(engine_state, stack, "raw")?; let call_span = call.head; - let ctrlc = engine_state.ctrlc.clone(); #[allow(deprecated)] let cwd = current_dir(engine_state, stack)?; let mut paths = get_rest_for_glob_pattern(engine_state, stack, call, 0)?; let eval_block = get_eval_block(engine_state); - if paths.is_empty() && call.rest_iter(0).next().is_none() { + if paths.is_empty() && !call.has_positional_args(stack, 0) { // try to use path from pipeline input if there were no positional or spread args let (filename, span) = match input { PipelineData::Value(val, ..) => { @@ -122,8 +121,12 @@ impl Command for Open { } else { #[cfg(feature = "sqlite")] if !raw { - let res = SQLiteDatabase::try_from_path(path, arg_span, ctrlc.clone()) - .map(|db| db.into_value(call.head).into_pipeline_data()); + let res = SQLiteDatabase::try_from_path( + path, + arg_span, + engine_state.signals().clone(), + ) + .map(|db| db.into_value(call.head).into_pipeline_data()); if res.is_ok() { return res; @@ -144,7 +147,7 @@ impl Command for Open { }; let stream = PipelineData::ByteStream( - ByteStream::file(file, call_span, ctrlc.clone()), + ByteStream::file(file, call_span, engine_state.signals().clone()), Some(PipelineMetadata { data_source: DataSource::FilePath(path.to_path_buf()), content_type: None, @@ -177,7 +180,8 @@ impl Command for Open { let block = engine_state.get_block(block_id); eval_block(engine_state, stack, block, stream) } else { - decl.run(engine_state, stack, &Call::new(call_span), stream) + let call = ast::Call::new(call_span); + decl.run(engine_state, stack, &(&call).into(), stream) }; output.push(command_output.map_err(|inner| { ShellError::GenericError{ @@ -203,7 +207,7 @@ impl Command for Open { Ok(output .into_iter() .flatten() - .into_pipeline_data(call_span, ctrlc)) + .into_pipeline_data(call_span, engine_state.signals().clone())) } } diff --git a/crates/nu-command/src/filesystem/rm.rs b/crates/nu-command/src/filesystem/rm.rs index 9696ae0c2f..9eb4fabace 100644 --- a/crates/nu-command/src/filesystem/rm.rs +++ b/crates/nu-command/src/filesystem/rm.rs @@ -170,7 +170,7 @@ fn rm( } let span = call.head; - let rm_always_trash = engine_state.get_config().rm_always_trash; + let rm_always_trash = stack.get_config(engine_state).rm_always_trash; if !TRASH_SUPPORTED { if rm_always_trash { @@ -451,12 +451,7 @@ fn rm( }); for result in iter { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - return Err(ShellError::InterruptedByUser { - span: Some(call.head), - }); - } - + engine_state.signals().check(call.head)?; match result { Ok(None) => {} Ok(Some(msg)) => eprintln!("{msg}"), diff --git a/crates/nu-command/src/filesystem/save.rs b/crates/nu-command/src/filesystem/save.rs index 1cfbbc67b4..be5073ef20 100644 --- a/crates/nu-command/src/filesystem/save.rs +++ b/crates/nu-command/src/filesystem/save.rs @@ -4,16 +4,13 @@ use nu_engine::get_eval_block; use nu_engine::{command_prelude::*, current_dir}; use nu_path::expand_path_with; use nu_protocol::{ - ast::{Expr, Expression}, - byte_stream::copy_with_interrupt, - process::ChildPipe, - ByteStreamSource, DataSource, OutDest, PipelineMetadata, + ast, byte_stream::copy_with_signals, process::ChildPipe, ByteStreamSource, DataSource, OutDest, + PipelineMetadata, Signals, }; use std::{ fs::File, io::{self, BufRead, BufReader, Read, Write}, path::{Path, PathBuf}, - sync::{atomic::AtomicBool, Arc}, thread, }; @@ -70,24 +67,6 @@ impl Command for Save { let append = call.has_flag(engine_state, stack, "append")?; let force = call.has_flag(engine_state, stack, "force")?; let progress = call.has_flag(engine_state, stack, "progress")?; - let out_append = if let Some(Expression { - expr: Expr::Bool(out_append), - .. - }) = call.get_parser_info("out-append") - { - *out_append - } else { - false - }; - let err_append = if let Some(Expression { - expr: Expr::Bool(err_append), - .. - }) = call.get_parser_info("err-append") - { - *err_append - } else { - false - }; let span = call.head; #[allow(deprecated)] @@ -110,40 +89,33 @@ impl Command for Save { PipelineData::ByteStream(stream, metadata) => { check_saving_to_source_file(metadata.as_ref(), &path, stderr_path.as_ref())?; - let (file, stderr_file) = get_files( - &path, - stderr_path.as_ref(), - append, - out_append, - err_append, - force, - )?; + let (file, stderr_file) = get_files(&path, stderr_path.as_ref(), append, force)?; let size = stream.known_size(); - let ctrlc = engine_state.ctrlc.clone(); + let signals = engine_state.signals(); match stream.into_source() { ByteStreamSource::Read(read) => { - stream_to_file(read, size, ctrlc, file, span, progress)?; + stream_to_file(read, size, signals, file, span, progress)?; } ByteStreamSource::File(source) => { - stream_to_file(source, size, ctrlc, file, span, progress)?; + stream_to_file(source, size, signals, file, span, progress)?; } ByteStreamSource::Child(mut child) => { fn write_or_consume_stderr( stderr: ChildPipe, file: Option, span: Span, - ctrlc: Option>, + signals: &Signals, progress: bool, ) -> Result<(), ShellError> { if let Some(file) = file { match stderr { ChildPipe::Pipe(pipe) => { - stream_to_file(pipe, None, ctrlc, file, span, progress) + stream_to_file(pipe, None, signals, file, span, progress) } ChildPipe::Tee(tee) => { - stream_to_file(tee, None, ctrlc, file, span, progress) + stream_to_file(tee, None, signals, file, span, progress) } }? } else { @@ -163,14 +135,14 @@ impl Command for Save { // delegate a thread to redirect stderr to result. let handler = stderr .map(|stderr| { - let ctrlc = ctrlc.clone(); + let signals = signals.clone(); thread::Builder::new().name("stderr saver".into()).spawn( move || { write_or_consume_stderr( stderr, stderr_file, span, - ctrlc, + &signals, progress, ) }, @@ -181,10 +153,10 @@ impl Command for Save { let res = match stdout { ChildPipe::Pipe(pipe) => { - stream_to_file(pipe, None, ctrlc, file, span, progress) + stream_to_file(pipe, None, signals, file, span, progress) } ChildPipe::Tee(tee) => { - stream_to_file(tee, None, ctrlc, file, span, progress) + stream_to_file(tee, None, signals, file, span, progress) } }; if let Some(h) = handler { @@ -202,7 +174,7 @@ impl Command for Save { stderr, stderr_file, span, - ctrlc, + signals, progress, )?; } @@ -222,14 +194,7 @@ impl Command for Save { stderr_path.as_ref(), )?; - let (mut file, _) = get_files( - &path, - stderr_path.as_ref(), - append, - out_append, - err_append, - force, - )?; + let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?; for val in ls { file.write_all(&value_to_bytes(val)?) .map_err(|err| ShellError::IOError { @@ -259,14 +224,7 @@ impl Command for Save { input_to_bytes(input, Path::new(&path.item), raw, engine_state, stack, span)?; // Only open file after successful conversion - let (mut file, _) = get_files( - &path, - stderr_path.as_ref(), - append, - out_append, - err_append, - force, - )?; + let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?; file.write_all(&bytes).map_err(|err| ShellError::IOError { msg: err.to_string(), @@ -398,7 +356,8 @@ fn convert_to_extension( let eval_block = get_eval_block(engine_state); eval_block(engine_state, stack, block, input) } else { - decl.run(engine_state, stack, &Call::new(span), input) + let call = ast::Call::new(span); + decl.run(engine_state, stack, &(&call).into(), input) } } else { Ok(input) @@ -474,19 +433,17 @@ fn get_files( path: &Spanned, stderr_path: Option<&Spanned>, append: bool, - out_append: bool, - err_append: bool, force: bool, ) -> Result<(File, Option), ShellError> { // First check both paths - let (path, path_span) = prepare_path(path, append || out_append, force)?; + let (path, path_span) = prepare_path(path, append, force)?; let stderr_path_and_span = stderr_path .as_ref() - .map(|stderr_path| prepare_path(stderr_path, append || err_append, force)) + .map(|stderr_path| prepare_path(stderr_path, append, force)) .transpose()?; // Only if both files can be used open and possibly truncate them - let file = open_file(path, path_span, append || out_append)?; + let file = open_file(path, path_span, append)?; let stderr_file = stderr_path_and_span .map(|(stderr_path, stderr_path_span)| { @@ -499,7 +456,7 @@ fn get_files( inner: vec![], }) } else { - open_file(stderr_path, stderr_path_span, append || err_append) + open_file(stderr_path, stderr_path_span, append) } }) .transpose()?; @@ -510,7 +467,7 @@ fn get_files( fn stream_to_file( source: impl Read, known_size: Option, - ctrlc: Option>, + signals: &Signals, mut file: File, span: Span, progress: bool, @@ -526,9 +483,9 @@ fn stream_to_file( let mut reader = BufReader::new(source); let res = loop { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { + if let Err(err) = signals.check(span) { bar.abandoned_msg("# Cancelled #".to_owned()); - return Ok(()); + return Err(err); } match reader.fill_buf() { @@ -555,7 +512,7 @@ fn stream_to_file( Ok(()) } } else { - copy_with_interrupt(source, file, span, ctrlc.as_deref())?; + copy_with_signals(source, file, span, signals)?; Ok(()) } } diff --git a/crates/nu-command/src/filesystem/util.rs b/crates/nu-command/src/filesystem/util.rs index 1b755875bd..de32d204a0 100644 --- a/crates/nu-command/src/filesystem/util.rs +++ b/crates/nu-command/src/filesystem/util.rs @@ -1,6 +1,6 @@ use dialoguer::Input; use nu_engine::{command_prelude::*, get_eval_expression}; -use nu_protocol::{ast::Expr, FromValue, NuGlob}; +use nu_protocol::{FromValue, NuGlob}; use std::{ error::Error, path::{Path, PathBuf}, @@ -92,42 +92,19 @@ pub fn is_older(src: &Path, dst: &Path) -> Option { /// Get rest arguments from given `call`, starts with `starting_pos`. /// -/// It's similar to `call.rest`, except that it always returns NuGlob. And if input argument has -/// Type::Glob, the NuGlob is unquoted, which means it's required to expand. +/// It's similar to `call.rest`, except that it always returns NuGlob. pub fn get_rest_for_glob_pattern( engine_state: &EngineState, stack: &mut Stack, call: &Call, starting_pos: usize, ) -> Result>, ShellError> { - let mut output = vec![]; let eval_expression = get_eval_expression(engine_state); - for result in call.rest_iter_flattened(starting_pos, |expr| { - let result = eval_expression(engine_state, stack, expr); - match result { - Err(e) => Err(e), - Ok(result) => { - let span = result.span(); - // convert from string to quoted string if expr is a variable - // or string interpolation - match result { - Value::String { val, .. } - if matches!( - &expr.expr, - Expr::FullCellPath(_) | Expr::StringInterpolation(_) - ) => - { - // should not expand if given input type is not glob. - Ok(Value::glob(val, expr.ty != Type::Glob, span)) - } - other => Ok(other), - } - } - } - })? { - output.push(FromValue::from_value(result)?); - } - - Ok(output) + call.rest_iter_flattened(engine_state, stack, eval_expression, starting_pos)? + .into_iter() + // This used to be much more complex, but I think `FromValue` should be able to handle the + // nuance here. + .map(FromValue::from_value) + .collect() } diff --git a/crates/nu-command/src/filesystem/watch.rs b/crates/nu-command/src/filesystem/watch.rs index fda542c8a8..c9817de250 100644 --- a/crates/nu-command/src/filesystem/watch.rs +++ b/crates/nu-command/src/filesystem/watch.rs @@ -143,7 +143,6 @@ impl Command for Watch { None => RecursiveMode::Recursive, }; - let ctrlc_ref = &engine_state.ctrlc.clone(); let (tx, rx) = channel(); let mut debouncer = match new_debouncer(debounce_duration, None, tx) { @@ -256,7 +255,7 @@ impl Command for Watch { } Err(RecvTimeoutError::Timeout) => {} } - if nu_utils::ctrl_c::was_pressed(ctrlc_ref) { + if engine_state.signals().interrupted() { break; } } diff --git a/crates/nu-command/src/filters/append.rs b/crates/nu-command/src/filters/append.rs index af5bd49283..2064bdf1e8 100644 --- a/crates/nu-command/src/filters/append.rs +++ b/crates/nu-command/src/filters/append.rs @@ -116,7 +116,7 @@ only unwrap the outer list, and leave the variable's contents untouched."# Ok(input .into_iter() .chain(other.into_pipeline_data()) - .into_pipeline_data_with_metadata(call.head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(call.head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/chunks.rs b/crates/nu-command/src/filters/chunks.rs new file mode 100644 index 0000000000..11a59c2aa9 --- /dev/null +++ b/crates/nu-command/src/filters/chunks.rs @@ -0,0 +1,153 @@ +use nu_engine::command_prelude::*; +use nu_protocol::ListStream; + +#[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 { + 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 { + 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(), + })?; + + if size == 0 { + return Err(ShellError::IncorrectValue { + msg: "`chunk_size` cannot be zero".into(), + val_span: chunk_size.span(), + call_span: head, + }); + } + + match input { + PipelineData::Value(Value::List { vals, .. }, metadata) => { + let chunks = ChunksIter::new(vals, size, 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| ChunksIter::new(iter, size, head)); + Ok(PipelineData::ListStream(stream, metadata)) + } + input => Err(input.unsupported_input_error("list", head)), + } + } +} + +struct ChunksIter> { + iter: I, + size: usize, + span: Span, +} + +impl> ChunksIter { + fn new(iter: impl IntoIterator, size: usize, span: Span) -> Self { + Self { + iter: iter.into_iter(), + size, + span, + } + } +} + +impl> Iterator for ChunksIter { + type Item = Value; + + fn next(&mut self) -> Option { + 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 {}) + } +} diff --git a/crates/nu-command/src/filters/compact.rs b/crates/nu-command/src/filters/compact.rs index 9a4e968e9b..f43d738ed1 100644 --- a/crates/nu-command/src/filters/compact.rs +++ b/crates/nu-command/src/filters/compact.rs @@ -140,7 +140,7 @@ pub fn compact( _ => true, } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) .map(|m| m.set_metadata(metadata)) } diff --git a/crates/nu-command/src/filters/default.rs b/crates/nu-command/src/filters/default.rs index 3eaa8d342e..9b08ce6331 100644 --- a/crates/nu-command/src/filters/default.rs +++ b/crates/nu-command/src/filters/default.rs @@ -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), @@ -80,8 +80,6 @@ fn default( let value: Value = call.req(engine_state, stack, 0)?; let column: Option> = call.opt(engine_state, stack, 1)?; - let ctrlc = engine_state.ctrlc.clone(); - if let Some(column) = column { input .map( @@ -109,21 +107,13 @@ fn default( } _ => item, }, - ctrlc, + engine_state.signals(), ) .map(|x| x.set_metadata(metadata)) } else if input.is_nothing() { Ok(value.into_pipeline_data()) } else { - input - .map( - move |item| match item { - Value::Nothing { .. } => value.clone(), - x => x, - }, - ctrlc, - ) - .map(|x| x.set_metadata(metadata)) + Ok(input) } } diff --git a/crates/nu-command/src/filters/drop/column.rs b/crates/nu-command/src/filters/drop/column.rs index 94c0308ea8..f168e6c2ca 100644 --- a/crates/nu-command/src/filters/drop/column.rs +++ b/crates/nu-command/src/filters/drop/column.rs @@ -102,7 +102,11 @@ fn drop_cols( Err(e) => Value::error(e, head), } })) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } else { Ok(PipelineData::Empty) } diff --git a/crates/nu-command/src/filters/drop/nth.rs b/crates/nu-command/src/filters/drop/nth.rs index a530de5aa1..a76c4f0d92 100644 --- a/crates/nu-command/src/filters/drop/nth.rs +++ b/crates/nu-command/src/filters/drop/nth.rs @@ -156,7 +156,7 @@ impl Command for DropNth { .take(start) .into_pipeline_data_with_metadata( head, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), metadata, )) } @@ -177,7 +177,7 @@ impl Command for DropNth { rows, current: 0, } - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/each.rs b/crates/nu-command/src/filters/each.rs index a074f63abb..fa1bf390b8 100644 --- a/crates/nu-command/src/filters/each.rs +++ b/crates/nu-command/src/filters/each.rs @@ -140,7 +140,7 @@ with 'transpose' first."# } } }) - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } PipelineData::ByteStream(stream, ..) => { if let Some(chunks) = stream.chunks() { @@ -171,7 +171,7 @@ with 'transpose' first."# } } }) - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } else { Ok(PipelineData::Empty) } @@ -185,7 +185,7 @@ with 'transpose' first."# .and_then(|x| { x.filter( move |x| if !keep_empty { !x.is_nothing() } else { true }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) }) .map(|data| data.set_metadata(metadata)) diff --git a/crates/nu-command/src/filters/enumerate.rs b/crates/nu-command/src/filters/enumerate.rs index 1034780657..df0f4e2fca 100644 --- a/crates/nu-command/src/filters/enumerate.rs +++ b/crates/nu-command/src/filters/enumerate.rs @@ -52,7 +52,6 @@ impl Command for Enumerate { ) -> Result { let head = call.head; let metadata = input.metadata(); - let ctrlc = engine_state.ctrlc.clone(); Ok(input .into_iter() @@ -66,7 +65,7 @@ impl Command for Enumerate { head, ) }) - .into_pipeline_data_with_metadata(head, ctrlc, metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/every.rs b/crates/nu-command/src/filters/every.rs index 1202b4f7c9..664be33bed 100644 --- a/crates/nu-command/src/filters/every.rs +++ b/crates/nu-command/src/filters/every.rs @@ -78,7 +78,7 @@ impl Command for Every { None } }) - .into_pipeline_data_with_metadata(call.head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(call.head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/filter.rs b/crates/nu-command/src/filters/filter.rs index 1ba1508839..f2efaa3af3 100644 --- a/crates/nu-command/src/filters/filter.rs +++ b/crates/nu-command/src/filters/filter.rs @@ -72,7 +72,7 @@ a variable. On the other hand, the "row condition" syntax is not supported."# } } }) - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } PipelineData::ByteStream(stream, ..) => { if let Some(chunks) = stream.chunks() { @@ -97,7 +97,7 @@ a variable. On the other hand, the "row condition" syntax is not supported."# } } }) - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } else { Ok(PipelineData::Empty) } @@ -117,7 +117,7 @@ a variable. On the other hand, the "row condition" syntax is not supported."# Some(Value::error(err, span)) } } - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } } .map(|data| data.set_metadata(metadata)) diff --git a/crates/nu-command/src/filters/find.rs b/crates/nu-command/src/filters/find.rs index af626c1e75..9ed0c28a3c 100644 --- a/crates/nu-command/src/filters/find.rs +++ b/crates/nu-command/src/filters/find.rs @@ -213,8 +213,7 @@ fn find_with_regex( input: PipelineData, ) -> Result { let span = call.head; - let ctrlc = engine_state.ctrlc.clone(); - let config = engine_state.get_config().clone(); + let config = stack.get_config(engine_state); let insensitive = call.has_flag(engine_state, stack, "ignore-case")?; let multiline = call.has_flag(engine_state, stack, "multiline")?; @@ -246,7 +245,7 @@ fn find_with_regex( Value::List { vals, .. } => values_match_find(vals, &re, &config, invert), _ => false, }, - ctrlc, + engine_state.signals(), ) } @@ -349,18 +348,16 @@ fn find_with_rest_and_highlight( input: PipelineData, ) -> Result { let span = call.head; - let ctrlc = engine_state.ctrlc.clone(); - let engine_state = engine_state.clone(); - let config = engine_state.get_config().clone(); - let filter_config = engine_state.get_config().clone(); - let invert = call.has_flag(&engine_state, stack, "invert")?; - let terms = call.rest::(&engine_state, stack, 0)?; + let config = stack.get_config(engine_state); + let filter_config = config.clone(); + let invert = call.has_flag(engine_state, stack, "invert")?; + let terms = call.rest::(engine_state, stack, 0)?; let lower_terms = terms .iter() .map(|v| Value::string(v.to_expanded_string("", &config).to_lowercase(), span)) .collect::>(); - let style_computer = StyleComputer::from_config(&engine_state, stack); + let style_computer = StyleComputer::from_config(engine_state, stack); // Currently, search results all use the same style. // Also note that this sample string is passed into user-written code (the closure that may or may not be // defined for "string"). @@ -369,7 +366,7 @@ fn find_with_rest_and_highlight( style_computer.compute("search_result", &Value::string("search result", span)); let cols_to_search_in_map: Vec<_> = call - .get_flag(&engine_state, stack, "columns")? + .get_flag(engine_state, stack, "columns")? .unwrap_or_default(); let cols_to_search_in_filter = cols_to_search_in_map.clone(); @@ -401,7 +398,7 @@ fn find_with_rest_and_highlight( _ => x, } }, - ctrlc.clone(), + engine_state.signals(), )? .filter( move |value| { @@ -414,7 +411,7 @@ fn find_with_rest_and_highlight( invert, ) }, - ctrlc, + engine_state.signals(), ), PipelineData::ListStream(stream, metadata) => { let stream = stream.modify(|iter| { diff --git a/crates/nu-command/src/filters/first.rs b/crates/nu-command/src/filters/first.rs index 97c5159b97..8f2e8db1b9 100644 --- a/crates/nu-command/src/filters/first.rs +++ b/crates/nu-command/src/filters/first.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::Signals; use std::io::Read; #[derive(Clone)] @@ -133,8 +134,7 @@ fn first_helper( } } Value::Range { val, .. } => { - let ctrlc = engine_state.ctrlc.clone(); - let mut iter = val.into_range_iter(span, ctrlc.clone()); + let mut iter = val.into_range_iter(span, Signals::empty()); if return_single_element { if let Some(v) = iter.next() { Ok(v.into_pipeline_data()) @@ -142,9 +142,11 @@ fn first_helper( Err(ShellError::AccessEmptyContent { span: head }) } } else { - Ok(iter - .take(rows) - .into_pipeline_data_with_metadata(span, ctrlc, metadata)) + Ok(iter.take(rows).into_pipeline_data_with_metadata( + span, + engine_state.signals().clone(), + metadata, + )) } } // Propagate errors by explicitly matching them before the final case. @@ -189,7 +191,7 @@ fn first_helper( ByteStream::read( reader.take(rows as u64), head, - None, + Signals::empty(), ByteStreamType::Binary, ), metadata, diff --git a/crates/nu-command/src/filters/flatten.rs b/crates/nu-command/src/filters/flatten.rs index ec86677af4..b4faec9589 100644 --- a/crates/nu-command/src/filters/flatten.rs +++ b/crates/nu-command/src/filters/flatten.rs @@ -127,7 +127,7 @@ fn flatten( input .flat_map( move |item| flat_value(&columns, item, flatten_all), - engine_state.ctrlc.clone(), + engine_state.signals(), ) .map(|x| x.set_metadata(metadata)) } diff --git a/crates/nu-command/src/filters/get.rs b/crates/nu-command/src/filters/get.rs index 07f0ea9440..9f7d76277d 100644 --- a/crates/nu-command/src/filters/get.rs +++ b/crates/nu-command/src/filters/get.rs @@ -62,7 +62,6 @@ If multiple cell paths are given, this will produce a list of values."# let mut rest: Vec = call.rest(engine_state, stack, 1)?; let ignore_errors = call.has_flag(engine_state, stack, "ignore-errors")?; let sensitive = call.has_flag(engine_state, stack, "sensitive")?; - let ctrlc = engine_state.ctrlc.clone(); let metadata = input.metadata(); if ignore_errors { @@ -89,7 +88,9 @@ If multiple cell paths are given, this will produce a list of values."# output.push(val?); } - Ok(output.into_iter().into_pipeline_data(span, ctrlc)) + Ok(output + .into_iter() + .into_pipeline_data(span, engine_state.signals().clone())) } .map(|x| x.set_metadata(metadata)) } diff --git a/crates/nu-command/src/filters/group.rs b/crates/nu-command/src/filters/group.rs index 821f35f34e..60c720e325 100644 --- a/crates/nu-command/src/filters/group.rs +++ b/crates/nu-command/src/filters/group.rs @@ -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,11 +54,19 @@ impl Command for Group { input: PipelineData, ) -> Result { let head = call.head; - let group_size: Spanned = call.req(engine_state, stack, 0)?; - let ctrlc = engine_state.ctrlc.clone(); - let metadata = input.metadata(); - //FIXME: add in support for external redirection when engine-q supports it generally + 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 = call.req(engine_state, stack, 0)?; + let metadata = input.metadata(); let each_group_iterator = EachGroupIterator { group_size: group_size.item, @@ -66,7 +74,11 @@ impl Command for Group { span: head, }; - Ok(each_group_iterator.into_pipeline_data_with_metadata(head, ctrlc, metadata)) + Ok(each_group_iterator.into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } } diff --git a/crates/nu-command/src/filters/insert.rs b/crates/nu-command/src/filters/insert.rs index 5f1380b2ac..3b47c4e6a9 100644 --- a/crates/nu-command/src/filters/insert.rs +++ b/crates/nu-command/src/filters/insert.rs @@ -222,7 +222,11 @@ fn insert( Ok(pre_elems .into_iter() .chain(stream) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } else if let Value::Closure { val, .. } = replacement { let mut closure = ClosureEval::new(engine_state, stack, *val); let stream = stream.map(move |mut value| { diff --git a/crates/nu-command/src/filters/interleave.rs b/crates/nu-command/src/filters/interleave.rs index 85a92741a1..9890fede1e 100644 --- a/crates/nu-command/src/filters/interleave.rs +++ b/crates/nu-command/src/filters/interleave.rs @@ -147,7 +147,7 @@ interleave // Now that threads are writing to the channel, we just return it as a stream Ok(rx .into_iter() - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } } diff --git a/crates/nu-command/src/filters/items.rs b/crates/nu-command/src/filters/items.rs index ed30486bee..04a4c6d672 100644 --- a/crates/nu-command/src/filters/items.rs +++ b/crates/nu-command/src/filters/items.rs @@ -67,7 +67,7 @@ impl Command for Items { } } }) - .into_pipeline_data(head, engine_state.ctrlc.clone())) + .into_pipeline_data(head, engine_state.signals().clone())) } Value::Error { error, .. } => Err(*error), other => Err(ShellError::OnlySupportsThisInputType { diff --git a/crates/nu-command/src/filters/last.rs b/crates/nu-command/src/filters/last.rs index efb300db01..bb5a75fecd 100644 --- a/crates/nu-command/src/filters/last.rs +++ b/crates/nu-command/src/filters/last.rs @@ -99,14 +99,10 @@ impl Command for Last { let mut buf = VecDeque::new(); for row in iterator { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - return Err(ShellError::InterruptedByUser { span: Some(head) }); - } - + engine_state.signals().check(head)?; if buf.len() == rows { buf.pop_front(); } - buf.push_back(row); } diff --git a/crates/nu-command/src/filters/lines.rs b/crates/nu-command/src/filters/lines.rs index 0b037dcaac..fc957e1c0e 100644 --- a/crates/nu-command/src/filters/lines.rs +++ b/crates/nu-command/src/filters/lines.rs @@ -26,7 +26,6 @@ impl Command for Lines { input: PipelineData, ) -> Result { let head = call.head; - let ctrlc = engine_state.ctrlc.clone(); let skip_empty = call.has_flag(engine_state, stack, "skip-empty")?; let span = input.span().unwrap_or(call.head); @@ -91,7 +90,7 @@ impl Command for Lines { Ok(line) => Value::string(line, head), Err(err) => Value::error(err, head), }) - .into_pipeline_data(head, ctrlc)) + .into_pipeline_data(head, engine_state.signals().clone())) } else { Ok(PipelineData::empty()) } diff --git a/crates/nu-command/src/filters/merge.rs b/crates/nu-command/src/filters/merge.rs index 8a3e613c56..191ed1706c 100644 --- a/crates/nu-command/src/filters/merge.rs +++ b/crates/nu-command/src/filters/merge.rs @@ -88,7 +88,6 @@ repeating this process with row 1, and so on."# let head = call.head; let merge_value: Value = call.req(engine_state, stack, 0)?; let metadata = input.metadata(); - let ctrlc = engine_state.ctrlc.clone(); match (&input, merge_value) { // table (list of records) @@ -110,7 +109,11 @@ repeating this process with row 1, and so on."# (Err(error), _) => Value::error(error, head), }); - Ok(res.into_pipeline_data_with_metadata(head, ctrlc, metadata)) + Ok(res.into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } // record ( diff --git a/crates/nu-command/src/filters/mod.rs b/crates/nu-command/src/filters/mod.rs index f43bdb10b2..4ffb0fc6db 100644 --- a/crates/nu-command/src/filters/mod.rs +++ b/crates/nu-command/src/filters/mod.rs @@ -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; diff --git a/crates/nu-command/src/filters/move_.rs b/crates/nu-command/src/filters/move_.rs index d93368f291..6e826af050 100644 --- a/crates/nu-command/src/filters/move_.rs +++ b/crates/nu-command/src/filters/move_.rs @@ -144,7 +144,6 @@ impl Command for Move { }; let metadata = input.metadata(); - let ctrlc = engine_state.ctrlc.clone(); match input { PipelineData::Value(Value::List { .. }, ..) | PipelineData::ListStream { .. } => { @@ -158,7 +157,11 @@ impl Command for Move { Err(error) => Value::error(error, head), }); - Ok(res.into_pipeline_data_with_metadata(head, ctrlc, metadata)) + Ok(res.into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } PipelineData::Value(Value::Record { val, .. }, ..) => { Ok(move_record_columns(&val, &columns, &before_or_after, head)? diff --git a/crates/nu-command/src/filters/par_each.rs b/crates/nu-command/src/filters/par_each.rs index af72895df6..958a7fbc76 100644 --- a/crates/nu-command/src/filters/par_each.rs +++ b/crates/nu-command/src/filters/par_each.rs @@ -1,6 +1,6 @@ use super::utils::chain_error_with_input; use nu_engine::{command_prelude::*, ClosureEvalOnce}; -use nu_protocol::engine::Closure; +use nu_protocol::{engine::Closure, Signals}; use rayon::prelude::*; #[derive(Clone)] @@ -158,12 +158,11 @@ impl Command for ParEach { }) .collect::>(); - apply_order(vec).into_pipeline_data(span, engine_state.ctrlc.clone()) + apply_order(vec).into_pipeline_data(span, engine_state.signals().clone()) })), Value::Range { val, .. } => Ok(create_pool(max_threads)?.install(|| { - let ctrlc = engine_state.ctrlc.clone(); let vec = val - .into_range_iter(span, ctrlc.clone()) + .into_range_iter(span, Signals::empty()) .enumerate() .par_bridge() .map(move |(index, value)| { @@ -184,7 +183,7 @@ impl Command for ParEach { }) .collect::>(); - apply_order(vec).into_pipeline_data(span, ctrlc) + apply_order(vec).into_pipeline_data(span, engine_state.signals().clone()) })), // This match allows non-iterables to be accepted, // which is currently considered undesirable (Nov 2022). @@ -212,7 +211,7 @@ impl Command for ParEach { }) .collect::>(); - apply_order(vec).into_pipeline_data(head, engine_state.ctrlc.clone()) + apply_order(vec).into_pipeline_data(head, engine_state.signals().clone()) })), PipelineData::ByteStream(stream, ..) => { if let Some(chunks) = stream.chunks() { @@ -236,14 +235,14 @@ impl Command for ParEach { }) .collect::>(); - apply_order(vec).into_pipeline_data(head, engine_state.ctrlc.clone()) + apply_order(vec).into_pipeline_data(head, engine_state.signals().clone()) })) } else { Ok(PipelineData::empty()) } } } - .and_then(|x| x.filter(|v| !v.is_nothing(), engine_state.ctrlc.clone())) + .and_then(|x| x.filter(|v| !v.is_nothing(), engine_state.signals())) .map(|data| data.set_metadata(metadata)) } } diff --git a/crates/nu-command/src/filters/prepend.rs b/crates/nu-command/src/filters/prepend.rs index f017420595..eeae0ef656 100644 --- a/crates/nu-command/src/filters/prepend.rs +++ b/crates/nu-command/src/filters/prepend.rs @@ -117,7 +117,7 @@ only unwrap the outer list, and leave the variable's contents untouched."# .into_pipeline_data() .into_iter() .chain(input) - .into_pipeline_data_with_metadata(call.head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(call.head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/range.rs b/crates/nu-command/src/filters/range.rs index c89a0b11f2..0d4e703bb1 100644 --- a/crates/nu-command/src/filters/range.rs +++ b/crates/nu-command/src/filters/range.rs @@ -106,7 +106,7 @@ impl Command for Range { Ok(PipelineData::Value(Value::nothing(head), None)) } else { let iter = v.into_iter().skip(from).take(to - from + 1); - Ok(iter.into_pipeline_data(head, engine_state.ctrlc.clone())) + Ok(iter.into_pipeline_data(head, engine_state.signals().clone())) } } else { let from = start as usize; @@ -116,7 +116,7 @@ impl Command for Range { Ok(PipelineData::Value(Value::nothing(head), None)) } else { let iter = input.into_iter().skip(from).take(to - from + 1); - Ok(iter.into_pipeline_data(head, engine_state.ctrlc.clone())) + Ok(iter.into_pipeline_data(head, engine_state.signals().clone())) } } .map(|x| x.set_metadata(metadata)) diff --git a/crates/nu-command/src/filters/reduce.rs b/crates/nu-command/src/filters/reduce.rs index fc808ca9af..87fbe3b23e 100644 --- a/crates/nu-command/src/filters/reduce.rs +++ b/crates/nu-command/src/filters/reduce.rs @@ -107,10 +107,7 @@ impl Command for Reduce { let mut closure = ClosureEval::new(engine_state, stack, closure); for value in iter { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - break; - } - + engine_state.signals().check(head)?; acc = closure .add_arg(value) .add_arg(acc) diff --git a/crates/nu-command/src/filters/rename.rs b/crates/nu-command/src/filters/rename.rs index b803bd8567..1e7ce34028 100644 --- a/crates/nu-command/src/filters/rename.rs +++ b/crates/nu-command/src/filters/rename.rs @@ -221,7 +221,7 @@ fn rename( ), } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) .map(|data| data.set_metadata(metadata)) } diff --git a/crates/nu-command/src/filters/reverse.rs b/crates/nu-command/src/filters/reverse.rs index f40e5f3be4..18891637ab 100644 --- a/crates/nu-command/src/filters/reverse.rs +++ b/crates/nu-command/src/filters/reverse.rs @@ -63,7 +63,7 @@ impl Command for Reverse { let metadata = input.metadata(); let values = input.into_iter_strict(head)?.collect::>(); let iter = values.into_iter().rev(); - Ok(iter.into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + Ok(iter.into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/select.rs b/crates/nu-command/src/filters/select.rs index 3514ac6be7..2fe4da3c36 100644 --- a/crates/nu-command/src/filters/select.rs +++ b/crates/nu-command/src/filters/select.rs @@ -215,7 +215,11 @@ fn select( rows: unique_rows.into_iter().peekable(), current: 0, } - .into_pipeline_data_with_metadata(call_span, engine_state.ctrlc.clone(), metadata) + .into_pipeline_data_with_metadata( + call_span, + engine_state.signals().clone(), + metadata, + ) } else { input }; @@ -236,7 +240,7 @@ fn select( //FIXME: improve implementation to not clone match input_val.clone().follow_cell_path(&path.members, false) { Ok(fetcher) => { - record.push(path.to_string().replace('.', "_"), fetcher); + record.push(path.to_string(), fetcher); if !columns_with_value.contains(&path) { columns_with_value.push(path); } @@ -255,7 +259,7 @@ fn select( Ok(output.into_iter().into_pipeline_data_with_metadata( call_span, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), metadata, )) } @@ -267,7 +271,7 @@ fn select( // FIXME: remove clone match v.clone().follow_cell_path(&cell_path.members, false) { Ok(result) => { - record.push(cell_path.to_string().replace('.', "_"), result); + record.push(cell_path.to_string(), result); } Err(e) => return Err(e), } @@ -291,7 +295,7 @@ fn select( //FIXME: improve implementation to not clone match x.clone().follow_cell_path(&path.members, false) { Ok(value) => { - record.push(path.to_string().replace('.', "_"), value); + record.push(path.to_string(), value); } Err(e) => return Err(e), } @@ -304,7 +308,7 @@ fn select( Ok(values.into_pipeline_data_with_metadata( call_span, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), metadata, )) } diff --git a/crates/nu-command/src/filters/shuffle.rs b/crates/nu-command/src/filters/shuffle.rs index 9e023b86c6..ec4bf8c454 100644 --- a/crates/nu-command/src/filters/shuffle.rs +++ b/crates/nu-command/src/filters/shuffle.rs @@ -33,7 +33,11 @@ impl Command for Shuffle { let mut values = input.into_iter_strict(call.head)?.collect::>(); values.shuffle(&mut thread_rng()); let iter = values.into_iter(); - Ok(iter.into_pipeline_data_with_metadata(call.head, engine_state.ctrlc.clone(), metadata)) + Ok(iter.into_pipeline_data_with_metadata( + call.head, + engine_state.signals().clone(), + metadata, + )) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/filters/skip/skip_.rs b/crates/nu-command/src/filters/skip/skip_.rs index d293a743da..b64f438858 100644 --- a/crates/nu-command/src/filters/skip/skip_.rs +++ b/crates/nu-command/src/filters/skip/skip_.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::Signals; use std::io::{self, Read}; #[derive(Clone)] @@ -90,8 +91,6 @@ impl Command for Skip { } None => 1, }; - - let ctrlc = engine_state.ctrlc.clone(); let input_span = input.span().unwrap_or(call.head); match input { PipelineData::ByteStream(stream, metadata) => { @@ -102,7 +101,12 @@ impl Command for Skip { io::copy(&mut (&mut reader).take(n as u64), &mut io::sink()) .err_span(span)?; Ok(PipelineData::ByteStream( - ByteStream::read(reader, call.head, None, ByteStreamType::Binary), + ByteStream::read( + reader, + call.head, + Signals::empty(), + ByteStreamType::Binary, + ), metadata, )) } else { @@ -124,7 +128,11 @@ impl Command for Skip { _ => Ok(input .into_iter_strict(call.head)? .skip(n) - .into_pipeline_data_with_metadata(input_span, ctrlc, metadata)), + .into_pipeline_data_with_metadata( + input_span, + engine_state.signals().clone(), + metadata, + )), } } } diff --git a/crates/nu-command/src/filters/skip/skip_until.rs b/crates/nu-command/src/filters/skip/skip_until.rs index bb36785e00..72cae739af 100644 --- a/crates/nu-command/src/filters/skip/skip_until.rs +++ b/crates/nu-command/src/filters/skip/skip_until.rs @@ -89,7 +89,7 @@ impl Command for SkipUntil { .map(|cond| cond.is_false()) .unwrap_or(false) }) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/skip/skip_while.rs b/crates/nu-command/src/filters/skip/skip_while.rs index 2747ea6f97..ea9c12bf6a 100644 --- a/crates/nu-command/src/filters/skip/skip_while.rs +++ b/crates/nu-command/src/filters/skip/skip_while.rs @@ -94,7 +94,7 @@ impl Command for SkipWhile { .map(|cond| cond.is_true()) .unwrap_or(false) }) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/sort.rs b/crates/nu-command/src/filters/sort.rs index 965b997355..179235302d 100644 --- a/crates/nu-command/src/filters/sort.rs +++ b/crates/nu-command/src/filters/sort.rs @@ -173,7 +173,7 @@ impl Command for Sort { let iter = vec.into_iter(); Ok(iter.into_pipeline_data_with_metadata( head, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), metadata, )) } diff --git a/crates/nu-command/src/filters/sort_by.rs b/crates/nu-command/src/filters/sort_by.rs index f3f715bc9d..e832255a26 100644 --- a/crates/nu-command/src/filters/sort_by.rs +++ b/crates/nu-command/src/filters/sort_by.rs @@ -100,7 +100,7 @@ impl Command for SortBy { } let iter = vec.into_iter(); - Ok(iter.into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + Ok(iter.into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/take/take_.rs b/crates/nu-command/src/filters/take/take_.rs index 7ebe22f914..1876244bdd 100644 --- a/crates/nu-command/src/filters/take/take_.rs +++ b/crates/nu-command/src/filters/take/take_.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::Signals; use std::io::Read; #[derive(Clone)] @@ -46,7 +47,6 @@ impl Command for Take { let head = call.head; let rows_desired: usize = call.req(engine_state, stack, 0)?; - let ctrlc = engine_state.ctrlc.clone(); let metadata = input.metadata(); match input { @@ -56,15 +56,23 @@ impl Command for Take { Value::List { vals, .. } => Ok(vals .into_iter() .take(rows_desired) - .into_pipeline_data_with_metadata(head, ctrlc, metadata)), + .into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )), Value::Binary { val, .. } => { let slice: Vec = val.into_iter().take(rows_desired).collect(); Ok(PipelineData::Value(Value::binary(slice, span), metadata)) } Value::Range { val, .. } => Ok(val - .into_range_iter(span, ctrlc.clone()) + .into_range_iter(span, Signals::empty()) .take(rows_desired) - .into_pipeline_data_with_metadata(head, ctrlc, metadata)), + .into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )), // Propagate errors by explicitly matching them before the final case. Value::Error { error, .. } => Err(*error), other => Err(ShellError::OnlySupportsThisInputType { @@ -87,7 +95,7 @@ impl Command for Take { ByteStream::read( reader.take(rows_desired as u64), head, - None, + Signals::empty(), ByteStreamType::Binary, ), metadata, diff --git a/crates/nu-command/src/filters/take/take_until.rs b/crates/nu-command/src/filters/take/take_until.rs index 0df2407cb1..c8544d364a 100644 --- a/crates/nu-command/src/filters/take/take_until.rs +++ b/crates/nu-command/src/filters/take/take_until.rs @@ -11,13 +11,10 @@ impl Command for TakeUntil { fn signature(&self) -> Signature { Signature::build(self.name()) - .input_output_types(vec![ - (Type::table(), Type::table()), - ( - Type::List(Box::new(Type::Any)), - Type::List(Box::new(Type::Any)), - ), - ]) + .input_output_types(vec![( + Type::List(Box::new(Type::Any)), + Type::List(Box::new(Type::Any)), + )]) .required( "predicate", SyntaxShape::Closure(Some(vec![SyntaxShape::Any, SyntaxShape::Int])), @@ -85,7 +82,7 @@ impl Command for TakeUntil { .map(|cond| cond.is_false()) .unwrap_or(false) }) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/take/take_while.rs b/crates/nu-command/src/filters/take/take_while.rs index 7c282ac38a..b8045080ea 100644 --- a/crates/nu-command/src/filters/take/take_while.rs +++ b/crates/nu-command/src/filters/take/take_while.rs @@ -85,7 +85,7 @@ impl Command for TakeWhile { .map(|cond| cond.is_true()) .unwrap_or(false) }) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/filters/tee.rs b/crates/nu-command/src/filters/tee.rs index 2251f6646a..663b2f19af 100644 --- a/crates/nu-command/src/filters/tee.rs +++ b/crates/nu-command/src/filters/tee.rs @@ -1,15 +1,11 @@ use nu_engine::{command_prelude::*, get_eval_block_with_early_return}; use nu_protocol::{ - byte_stream::copy_with_interrupt, engine::Closure, process::ChildPipe, ByteStream, - ByteStreamSource, OutDest, PipelineMetadata, + byte_stream::copy_with_signals, engine::Closure, process::ChildPipe, ByteStream, + ByteStreamSource, OutDest, PipelineMetadata, Signals, }; use std::{ io::{self, Read, Write}, - sync::{ - atomic::AtomicBool, - mpsc::{self, Sender}, - Arc, - }, + sync::mpsc::{self, Sender}, thread::{self, JoinHandle}, }; @@ -103,12 +99,11 @@ use it in your pipeline."# if let PipelineData::ByteStream(stream, metadata) = input { let span = stream.span(); - let ctrlc = engine_state.ctrlc.clone(); let type_ = stream.type_(); let info = StreamInfo { span, - ctrlc: ctrlc.clone(), + signals: engine_state.signals().clone(), type_, metadata: metadata.clone(), }; @@ -123,7 +118,7 @@ use it in your pipeline."# let tee = IoTee::new(read, tee_thread); Ok(PipelineData::ByteStream( - ByteStream::read(tee, span, ctrlc, type_), + ByteStream::read(tee, span, engine_state.signals().clone(), type_), metadata, )) } @@ -136,7 +131,7 @@ use it in your pipeline."# let tee = IoTee::new(file, tee_thread); Ok(PipelineData::ByteStream( - ByteStream::read(tee, span, ctrlc, type_), + ByteStream::read(tee, span, engine_state.signals().clone(), type_), metadata, )) } @@ -234,19 +229,19 @@ use it in your pipeline."# } let span = input.span().unwrap_or(head); - let ctrlc = engine_state.ctrlc.clone(); let metadata = input.metadata(); let metadata_clone = metadata.clone(); + let signals = engine_state.signals().clone(); Ok(tee(input.into_iter(), move |rx| { - let input = rx.into_pipeline_data_with_metadata(span, ctrlc, metadata_clone); + let input = rx.into_pipeline_data_with_metadata(span, signals, metadata_clone); eval_block(input) }) .err_span(call.head)? .map(move |result| result.unwrap_or_else(|err| Value::error(err, closure_span))) .into_pipeline_data_with_metadata( span, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), metadata, )) } @@ -386,8 +381,13 @@ fn spawn_tee( let thread = thread::Builder::new() .name("tee".into()) .spawn(move || { - // We don't use ctrlc here because we assume it already has it on the other side - let stream = ByteStream::from_iter(receiver.into_iter(), info.span, None, info.type_); + // We use Signals::empty() here because we assume there already is a Signals on the other side + let stream = ByteStream::from_iter( + receiver.into_iter(), + info.span, + Signals::empty(), + info.type_, + ); eval_block(PipelineData::ByteStream(stream, info.metadata)) }) .err_span(info.span)?; @@ -398,13 +398,13 @@ fn spawn_tee( #[derive(Clone)] struct StreamInfo { span: Span, - ctrlc: Option>, + signals: Signals, type_: ByteStreamType, metadata: Option, } fn copy(src: impl Read, dest: impl Write, info: &StreamInfo) -> Result<(), ShellError> { - copy_with_interrupt(src, dest, info.span, info.ctrlc.as_deref())?; + copy_with_signals(src, dest, info.span, &info.signals)?; Ok(()) } @@ -421,11 +421,11 @@ fn copy_on_thread( info: &StreamInfo, ) -> Result>, ShellError> { let span = info.span; - let ctrlc = info.ctrlc.clone(); + let signals = info.signals.clone(); thread::Builder::new() .name("stderr copier".into()) .spawn(move || { - copy_with_interrupt(src, dest, span, ctrlc.as_deref())?; + copy_with_signals(src, dest, span, &signals)?; Ok(()) }) .map_err(|e| e.into_spanned(span).into()) diff --git a/crates/nu-command/src/filters/transpose.rs b/crates/nu-command/src/filters/transpose.rs index 49caa56d18..4a14c1aea2 100644 --- a/crates/nu-command/src/filters/transpose.rs +++ b/crates/nu-command/src/filters/transpose.rs @@ -149,31 +149,30 @@ pub fn transpose( if !args.rest.is_empty() && args.header_row { return Err(ShellError::IncompatibleParametersSingle { msg: "Can not provide header names and use `--header-row`".into(), - span: call.get_named_arg("header-row").expect("has flag").span, + span: call.get_flag_span(stack, "header-row").expect("has flag"), }); } if !args.header_row && args.keep_all { return Err(ShellError::IncompatibleParametersSingle { msg: "Can only be used with `--header-row`(`-r`)".into(), - span: call.get_named_arg("keep-all").expect("has flag").span, + span: call.get_flag_span(stack, "keep-all").expect("has flag"), }); } if !args.header_row && args.keep_last { return Err(ShellError::IncompatibleParametersSingle { msg: "Can only be used with `--header-row`(`-r`)".into(), - span: call.get_named_arg("keep-last").expect("has flag").span, + span: call.get_flag_span(stack, "keep-last").expect("has flag"), }); } if args.keep_all && args.keep_last { return Err(ShellError::IncompatibleParameters { left_message: "can't use `--keep-last` at the same time".into(), - left_span: call.get_named_arg("keep-last").expect("has flag").span, + left_span: call.get_flag_span(stack, "keep-last").expect("has flag"), right_message: "because of `--keep-all`".into(), - right_span: call.get_named_arg("keep-all").expect("has flag").span, + right_span: call.get_flag_span(stack, "keep-all").expect("has flag"), }); } - let ctrlc = engine_state.ctrlc.clone(); let metadata = input.metadata(); let input: Vec<_> = input.into_iter().collect(); @@ -284,7 +283,11 @@ pub fn transpose( metadata, )) } else { - Ok(result_data.into_pipeline_data_with_metadata(name, ctrlc, metadata)) + Ok(result_data.into_pipeline_data_with_metadata( + name, + engine_state.signals().clone(), + metadata, + )) } } diff --git a/crates/nu-command/src/filters/uniq.rs b/crates/nu-command/src/filters/uniq.rs index 7d71d868f0..99abb4e152 100644 --- a/crates/nu-command/src/filters/uniq.rs +++ b/crates/nu-command/src/filters/uniq.rs @@ -241,18 +241,18 @@ pub fn uniq( item_mapper: Box ValueCounter>, metadata: Option, ) -> Result { - let ctrlc = engine_state.ctrlc.clone(); let head = call.head; let flag_show_count = call.has_flag(engine_state, stack, "count")?; let flag_show_repeated = call.has_flag(engine_state, stack, "repeated")?; let flag_ignore_case = call.has_flag(engine_state, stack, "ignore-case")?; let flag_only_uniques = call.has_flag(engine_state, stack, "unique")?; + let signals = engine_state.signals().clone(); let uniq_values = input .into_iter() .enumerate() .map_while(|(index, item)| { - if nu_utils::ctrl_c::was_pressed(&ctrlc) { + if signals.interrupted() { return None; } Some(item_mapper(ItemMapperState { diff --git a/crates/nu-command/src/filters/update.rs b/crates/nu-command/src/filters/update.rs index e724ae77ad..d5ef0825ec 100644 --- a/crates/nu-command/src/filters/update.rs +++ b/crates/nu-command/src/filters/update.rs @@ -187,7 +187,11 @@ fn update( Ok(pre_elems .into_iter() .chain(stream) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } else if let Value::Closure { val, .. } = replacement { let mut closure = ClosureEval::new(engine_state, stack, *val); let stream = stream.map(move |mut value| { diff --git a/crates/nu-command/src/filters/upsert.rs b/crates/nu-command/src/filters/upsert.rs index e3678972fb..af2e6c74b1 100644 --- a/crates/nu-command/src/filters/upsert.rs +++ b/crates/nu-command/src/filters/upsert.rs @@ -247,7 +247,11 @@ fn upsert( Ok(pre_elems .into_iter() .chain(stream) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } else if let Value::Closure { val, .. } = replacement { let mut closure = ClosureEval::new(engine_state, stack, *val); let stream = stream.map(move |mut value| { diff --git a/crates/nu-command/src/filters/utils.rs b/crates/nu-command/src/filters/utils.rs index 8d9b1300f6..4c67667e8e 100644 --- a/crates/nu-command/src/filters/utils.rs +++ b/crates/nu-command/src/filters/utils.rs @@ -1,7 +1,6 @@ use nu_engine::{CallExt, ClosureEval}; use nu_protocol::{ - ast::Call, - engine::{Closure, EngineState, Stack}, + engine::{Call, Closure, EngineState, Stack}, IntoPipelineData, PipelineData, ShellError, Span, Value, }; @@ -32,10 +31,7 @@ pub fn boolean_fold( let mut closure = ClosureEval::new(engine_state, stack, closure); for value in input { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - break; - } - + engine_state.signals().check(head)?; let pred = closure.run_with_value(value)?.into_value(head)?.is_true(); if pred == accumulator { diff --git a/crates/nu-command/src/filters/values.rs b/crates/nu-command/src/filters/values.rs index f6ff8cda2e..9a08f1168c 100644 --- a/crates/nu-command/src/filters/values.rs +++ b/crates/nu-command/src/filters/values.rs @@ -134,7 +134,7 @@ fn values( head: Span, input: PipelineData, ) -> Result { - let ctrlc = engine_state.ctrlc.clone(); + let signals = engine_state.signals().clone(); let metadata = input.metadata(); match input { PipelineData::Empty => Ok(PipelineData::Empty), @@ -144,7 +144,7 @@ fn values( Value::List { vals, .. } => match get_values(&vals, head, span) { Ok(cols) => Ok(cols .into_iter() - .into_pipeline_data_with_metadata(head, ctrlc, metadata)), + .into_pipeline_data_with_metadata(head, signals, metadata)), Err(err) => Err(err), }, Value::Custom { val, .. } => { @@ -152,7 +152,7 @@ fn values( match get_values(&[input_as_base_value], head, span) { Ok(cols) => Ok(cols .into_iter() - .into_pipeline_data_with_metadata(head, ctrlc, metadata)), + .into_pipeline_data_with_metadata(head, signals, metadata)), Err(err) => Err(err), } } @@ -160,7 +160,7 @@ fn values( .values() .cloned() .collect::>() - .into_pipeline_data_with_metadata(head, ctrlc, metadata)), + .into_pipeline_data_with_metadata(head, signals, metadata)), // Propagate errors Value::Error { error, .. } => Err(*error), other => Err(ShellError::OnlySupportsThisInputType { @@ -176,7 +176,7 @@ fn values( match get_values(&vals, head, head) { Ok(cols) => Ok(cols .into_iter() - .into_pipeline_data_with_metadata(head, ctrlc, metadata)), + .into_pipeline_data_with_metadata(head, signals, metadata)), Err(err) => Err(err), } } diff --git a/crates/nu-command/src/filters/where_.rs b/crates/nu-command/src/filters/where_.rs index 922879d09b..0a8c8c9ef3 100644 --- a/crates/nu-command/src/filters/where_.rs +++ b/crates/nu-command/src/filters/where_.rs @@ -70,7 +70,7 @@ not supported."# Err(err) => Some(Value::error(err, head)), } }) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/filters/window.rs b/crates/nu-command/src/filters/window.rs index 5b386b9f84..3c470f478d 100644 --- a/crates/nu-command/src/filters/window.rs +++ b/crates/nu-command/src/filters/window.rs @@ -113,7 +113,6 @@ impl Command for Window { ) -> Result { let head = call.head; let group_size: Spanned = call.req(engine_state, stack, 0)?; - let ctrlc = engine_state.ctrlc.clone(); let metadata = input.metadata(); let stride: Option = call.get_flag(engine_state, stack, "stride")?; let remainder = call.has_flag(engine_state, stack, "remainder")?; @@ -131,7 +130,11 @@ impl Command for Window { remainder, }; - Ok(each_group_iterator.into_pipeline_data_with_metadata(head, ctrlc, metadata)) + Ok(each_group_iterator.into_pipeline_data_with_metadata( + head, + engine_state.signals().clone(), + metadata, + )) } } diff --git a/crates/nu-command/src/filters/wrap.rs b/crates/nu-command/src/filters/wrap.rs index 52a0fb22c3..dfe1c11d03 100644 --- a/crates/nu-command/src/filters/wrap.rs +++ b/crates/nu-command/src/filters/wrap.rs @@ -42,7 +42,7 @@ impl Command for Wrap { | PipelineData::ListStream { .. } => Ok(input .into_iter() .map(move |x| Value::record(record! { name.clone() => x }, span)) - .into_pipeline_data_with_metadata(span, engine_state.ctrlc.clone(), metadata)), + .into_pipeline_data_with_metadata(span, engine_state.signals().clone(), metadata)), PipelineData::ByteStream(stream, ..) => Ok(Value::record( record! { name => stream.into_value()? }, span, diff --git a/crates/nu-command/src/filters/zip.rs b/crates/nu-command/src/filters/zip.rs index 9d81451ed4..59e2b4ac98 100644 --- a/crates/nu-command/src/filters/zip.rs +++ b/crates/nu-command/src/filters/zip.rs @@ -112,7 +112,7 @@ impl Command for Zip { .into_iter() .zip(other) .map(move |(x, y)| Value::list(vec![x, y], head)) - .into_pipeline_data_with_metadata(head, engine_state.ctrlc.clone(), metadata)) + .into_pipeline_data_with_metadata(head, engine_state.signals().clone(), metadata)) } } diff --git a/crates/nu-command/src/formats/from/delimited.rs b/crates/nu-command/src/formats/from/delimited.rs index 8f84890645..0fea7e082b 100644 --- a/crates/nu-command/src/formats/from/delimited.rs +++ b/crates/nu-command/src/formats/from/delimited.rs @@ -1,5 +1,5 @@ use csv::{ReaderBuilder, Trim}; -use nu_protocol::{ByteStream, ListStream, PipelineData, ShellError, Span, Value}; +use nu_protocol::{ByteStream, ListStream, PipelineData, ShellError, Signals, Span, Value}; fn from_csv_error(err: csv::Error, span: Span) -> ShellError { ShellError::DelimiterError { @@ -25,7 +25,7 @@ fn from_delimited_stream( let input_reader = if let Some(stream) = input.reader() { stream } else { - return Ok(ListStream::new(std::iter::empty(), span, None)); + return Ok(ListStream::new(std::iter::empty(), span, Signals::empty())); }; let mut reader = ReaderBuilder::new() @@ -83,7 +83,7 @@ fn from_delimited_stream( Value::record(columns.zip(values).collect(), span) }); - Ok(ListStream::new(iter, span, None)) + Ok(ListStream::new(iter, span, Signals::empty())) } pub(super) struct DelimitedReaderConfig { @@ -106,7 +106,7 @@ pub(super) fn from_delimited_data( PipelineData::Empty => Ok(PipelineData::Empty), PipelineData::Value(value, metadata) => { let string = value.into_string()?; - let byte_stream = ByteStream::read_string(string, name, None); + let byte_stream = ByteStream::read_string(string, name, Signals::empty()); Ok(PipelineData::ListStream( from_delimited_stream(config, byte_stream, name)?, metadata, diff --git a/crates/nu-command/src/formats/from/json.rs b/crates/nu-command/src/formats/from/json.rs index a1b43abb0a..6252afff4c 100644 --- a/crates/nu-command/src/formats/from/json.rs +++ b/crates/nu-command/src/formats/from/json.rs @@ -1,10 +1,7 @@ -use std::{ - io::{BufRead, Cursor}, - sync::{atomic::AtomicBool, Arc}, -}; +use std::io::{BufRead, Cursor}; use nu_engine::command_prelude::*; -use nu_protocol::{ListStream, PipelineMetadata}; +use nu_protocol::{ListStream, PipelineMetadata, Signals}; #[derive(Clone)] pub struct FromJson; @@ -80,7 +77,12 @@ impl Command for FromJson { match input { PipelineData::Value(Value::String { val, .. }, metadata) => { Ok(PipelineData::ListStream( - read_json_lines(Cursor::new(val), span, strict, engine_state.ctrlc.clone()), + read_json_lines( + Cursor::new(val), + span, + strict, + engine_state.signals().clone(), + ), update_metadata(metadata), )) } @@ -89,7 +91,7 @@ impl Command for FromJson { { if let Some(reader) = stream.reader() { Ok(PipelineData::ListStream( - read_json_lines(reader, span, strict, None), + read_json_lines(reader, span, strict, Signals::empty()), update_metadata(metadata), )) } else { @@ -127,7 +129,7 @@ fn read_json_lines( input: impl BufRead + Send + 'static, span: Span, strict: bool, - interrupt: Option>, + signals: Signals, ) -> ListStream { let iter = input .lines() @@ -142,7 +144,7 @@ fn read_json_lines( }) .map(move |result| result.unwrap_or_else(|err| Value::error(err, span))); - ListStream::new(iter, span, interrupt) + ListStream::new(iter, span, signals) } fn convert_nujson_to_value(value: nu_json::Value, span: Span) -> Value { diff --git a/crates/nu-command/src/formats/from/msgpack.rs b/crates/nu-command/src/formats/from/msgpack.rs index 4d8ea5e320..d3d0d710f1 100644 --- a/crates/nu-command/src/formats/from/msgpack.rs +++ b/crates/nu-command/src/formats/from/msgpack.rs @@ -5,12 +5,12 @@ use std::{ error::Error, io::{self, Cursor, ErrorKind}, string::FromUtf8Error, - sync::{atomic::AtomicBool, Arc}, }; use byteorder::{BigEndian, ReadBytesExt}; use chrono::{TimeZone, Utc}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; use rmp::decode::{self as mp, ValueReadError}; /// Max recursion depth @@ -111,7 +111,7 @@ MessagePack: https://msgpack.org/ let opts = Opts { span: call.head, objects, - ctrlc: engine_state.ctrlc.clone(), + signals: engine_state.signals().clone(), }; match input { // Deserialize from a byte buffer @@ -227,7 +227,7 @@ impl From for ShellError { pub(crate) struct Opts { pub span: Span, pub objects: bool, - pub ctrlc: Option>, + pub signals: Signals, } /// Read single or multiple values into PipelineData @@ -238,7 +238,7 @@ pub(crate) fn read_msgpack( let Opts { span, objects, - ctrlc, + signals, } = opts; if objects { // Make an iterator that reads multiple values from the reader @@ -262,7 +262,7 @@ pub(crate) fn read_msgpack( None } }) - .into_pipeline_data(span, ctrlc)) + .into_pipeline_data(span, signals)) } else { // Read a single value and then make sure it's EOF let result = read_value(&mut input, span, 0)?; diff --git a/crates/nu-command/src/formats/from/msgpackz.rs b/crates/nu-command/src/formats/from/msgpackz.rs index 7960f3f97a..c98b72cdd6 100644 --- a/crates/nu-command/src/formats/from/msgpackz.rs +++ b/crates/nu-command/src/formats/from/msgpackz.rs @@ -41,7 +41,7 @@ impl Command for FromMsgpackz { let opts = Opts { span, objects, - ctrlc: engine_state.ctrlc.clone(), + signals: engine_state.signals().clone(), }; match input { // Deserialize from a byte buffer diff --git a/crates/nu-command/src/formats/from/toml.rs b/crates/nu-command/src/formats/from/toml.rs index e1ddca3164..266911f50a 100644 --- a/crates/nu-command/src/formats/from/toml.rs +++ b/crates/nu-command/src/formats/from/toml.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use std::str::FromStr; +use toml::value::{Datetime, Offset}; #[derive(Clone)] pub struct FromToml; @@ -56,6 +56,54 @@ b = [1, 2]' | from toml", } } +fn convert_toml_datetime_to_value(dt: &Datetime, span: Span) -> Value { + match &dt.clone() { + toml::value::Datetime { + date: Some(_), + time: _, + offset: _, + } => (), + _ => return Value::string(dt.to_string(), span), + } + + let date = match dt.date { + Some(date) => { + chrono::NaiveDate::from_ymd_opt(date.year.into(), date.month.into(), date.day.into()) + } + None => Some(chrono::NaiveDate::default()), + }; + + let time = match dt.time { + Some(time) => chrono::NaiveTime::from_hms_nano_opt( + time.hour.into(), + time.minute.into(), + time.second.into(), + time.nanosecond, + ), + None => Some(chrono::NaiveTime::default()), + }; + + let tz = match dt.offset { + Some(offset) => match offset { + Offset::Z => chrono::FixedOffset::east_opt(0), + Offset::Custom { minutes: min } => chrono::FixedOffset::east_opt(min as i32 * 60), + }, + None => chrono::FixedOffset::east_opt(0), + }; + + let datetime = match (date, time, tz) { + (Some(date), Some(time), Some(tz)) => chrono::NaiveDateTime::new(date, time) + .and_local_timezone(tz) + .earliest(), + _ => None, + }; + + match datetime { + Some(datetime) => Value::date(datetime, span), + None => Value::string(dt.to_string(), span), + } +} + fn convert_toml_to_value(value: &toml::Value, span: Span) -> Value { match value { toml::Value::Array(array) => { @@ -76,13 +124,7 @@ fn convert_toml_to_value(value: &toml::Value, span: Span) -> Value { span, ), toml::Value::String(s) => Value::string(s.clone(), span), - toml::Value::Datetime(d) => match chrono::DateTime::from_str(&d.to_string()) { - Ok(nushell_date) => Value::date(nushell_date, span), - // in the unlikely event that parsing goes wrong, this function still returns a valid - // nushell date (however the default one). This decision was made to make the output of - // this function uniform amongst all eventualities - Err(_) => Value::date(chrono::DateTime::default(), span), - }, + toml::Value::Datetime(dt) => convert_toml_datetime_to_value(dt, span), } } @@ -113,32 +155,6 @@ mod tests { test_examples(FromToml {}) } - #[test] - fn from_toml_creates_nushell_date() { - let toml_date = toml::Value::Datetime(Datetime { - date: Option::from(toml::value::Date { - year: 1980, - month: 10, - day: 12, - }), - time: Option::from(toml::value::Time { - hour: 10, - minute: 12, - second: 44, - nanosecond: 0, - }), - offset: Option::from(toml::value::Offset::Custom { minutes: 120 }), - }); - - let span = Span::test_data(); - let reference_date = Value::date(Default::default(), Span::test_data()); - - let result = convert_toml_to_value(&toml_date, span); - - //positive test (from toml returns a nushell date) - assert_eq!(result.get_type(), reference_date.get_type()); - } - #[test] fn from_toml_creates_correct_date() { let toml_date = toml::Value::Datetime(Datetime { @@ -206,4 +222,113 @@ mod tests { assert!(result.is_err()); } + + #[test] + fn convert_toml_datetime_to_value_date_time_offset() { + let toml_date = Datetime { + date: Option::from(toml::value::Date { + year: 2000, + month: 1, + day: 1, + }), + time: Option::from(toml::value::Time { + hour: 12, + minute: 12, + second: 12, + nanosecond: 0, + }), + offset: Option::from(toml::value::Offset::Custom { minutes: 120 }), + }; + + let span = Span::test_data(); + let reference_date = Value::date( + chrono::FixedOffset::east_opt(60 * 120) + .unwrap() + .with_ymd_and_hms(2000, 1, 1, 12, 12, 12) + .unwrap(), + span, + ); + + let result = convert_toml_datetime_to_value(&toml_date, span); + + assert_eq!(result, reference_date); + } + + #[test] + fn convert_toml_datetime_to_value_date_time() { + let toml_date = Datetime { + date: Option::from(toml::value::Date { + year: 2000, + month: 1, + day: 1, + }), + time: Option::from(toml::value::Time { + hour: 12, + minute: 12, + second: 12, + nanosecond: 0, + }), + offset: None, + }; + + let span = Span::test_data(); + let reference_date = Value::date( + chrono::FixedOffset::east_opt(0) + .unwrap() + .with_ymd_and_hms(2000, 1, 1, 12, 12, 12) + .unwrap(), + span, + ); + + let result = convert_toml_datetime_to_value(&toml_date, span); + + assert_eq!(result, reference_date); + } + + #[test] + fn convert_toml_datetime_to_value_date() { + let toml_date = Datetime { + date: Option::from(toml::value::Date { + year: 2000, + month: 1, + day: 1, + }), + time: None, + offset: None, + }; + + let span = Span::test_data(); + let reference_date = Value::date( + chrono::FixedOffset::east_opt(0) + .unwrap() + .with_ymd_and_hms(2000, 1, 1, 0, 0, 0) + .unwrap(), + span, + ); + + let result = convert_toml_datetime_to_value(&toml_date, span); + + assert_eq!(result, reference_date); + } + + #[test] + fn convert_toml_datetime_to_value_only_time() { + let toml_date = Datetime { + date: None, + time: Option::from(toml::value::Time { + hour: 12, + minute: 12, + second: 12, + nanosecond: 0, + }), + offset: None, + }; + + let span = Span::test_data(); + let reference_date = Value::string(toml_date.to_string(), span); + + let result = convert_toml_datetime_to_value(&toml_date, span); + + assert_eq!(result, reference_date); + } } diff --git a/crates/nu-command/src/formats/to/delimited.rs b/crates/nu-command/src/formats/to/delimited.rs index a7a2480a34..34bb06b8a2 100644 --- a/crates/nu-command/src/formats/to/delimited.rs +++ b/crates/nu-command/src/formats/to/delimited.rs @@ -1,7 +1,7 @@ use csv::WriterBuilder; use nu_cmd_base::formats::to::delimited::merge_descriptors; use nu_protocol::{ - ByteStream, ByteStreamType, Config, PipelineData, ShellError, Span, Spanned, Value, + ByteStream, ByteStreamType, Config, PipelineData, ShellError, Signals, Span, Spanned, Value, }; use std::{iter, sync::Arc}; @@ -128,37 +128,42 @@ pub fn to_delimited_data( // If we're configured to generate a header, we generate it first, then set this false let mut is_header = !noheaders; - let stream = ByteStream::from_fn(head, None, ByteStreamType::String, move |buffer| { - let mut wtr = WriterBuilder::new() - .delimiter(separator) - .from_writer(buffer); + let stream = ByteStream::from_fn( + head, + Signals::empty(), + ByteStreamType::String, + move |buffer| { + let mut wtr = WriterBuilder::new() + .delimiter(separator) + .from_writer(buffer); - if is_header { - // Unless we are configured not to write a header, we write the header row now, once, - // before everything else. - wtr.write_record(&columns) - .map_err(|err| make_csv_error(err, format_name, head))?; - is_header = false; - Ok(true) - } else if let Some(row) = iter.next() { - // Write each column of a normal row, in order - let record = row.into_record()?; - for column in &columns { - let field = record - .get(column) - .map(|v| to_string_tagged_value(v, &config, format_name)) - .unwrap_or(Ok(String::new()))?; - wtr.write_field(field) + if is_header { + // Unless we are configured not to write a header, we write the header row now, once, + // before everything else. + wtr.write_record(&columns) .map_err(|err| make_csv_error(err, format_name, head))?; + is_header = false; + Ok(true) + } else if let Some(row) = iter.next() { + // Write each column of a normal row, in order + let record = row.into_record()?; + for column in &columns { + let field = record + .get(column) + .map(|v| to_string_tagged_value(v, &config, format_name)) + .unwrap_or(Ok(String::new()))?; + wtr.write_field(field) + .map_err(|err| make_csv_error(err, format_name, head))?; + } + // End the row + wtr.write_record(iter::empty::()) + .map_err(|err| make_csv_error(err, format_name, head))?; + Ok(true) + } else { + Ok(false) } - // End the row - wtr.write_record(iter::empty::()) - .map_err(|err| make_csv_error(err, format_name, head))?; - Ok(true) - } else { - Ok(false) - } - }); + }, + ); Ok(PipelineData::ByteStream(stream, metadata)) } diff --git a/crates/nu-command/src/formats/to/md.rs b/crates/nu-command/src/formats/to/md.rs index e2acdd703e..737a328cd3 100644 --- a/crates/nu-command/src/formats/to/md.rs +++ b/crates/nu-command/src/formats/to/md.rs @@ -70,8 +70,8 @@ impl Command for ToMd { let head = call.head; let pretty = call.has_flag(engine_state, stack, "pretty")?; let per_element = call.has_flag(engine_state, stack, "per-element")?; - let config = engine_state.get_config(); - to_md(input, pretty, per_element, config, head) + let config = stack.get_config(engine_state); + to_md(input, pretty, per_element, &config, head) } } diff --git a/crates/nu-command/src/formats/to/msgpack.rs b/crates/nu-command/src/formats/to/msgpack.rs index bfeb428e3e..02515e26fe 100644 --- a/crates/nu-command/src/formats/to/msgpack.rs +++ b/crates/nu-command/src/formats/to/msgpack.rs @@ -5,7 +5,7 @@ use std::io; use byteorder::{BigEndian, WriteBytesExt}; use nu_engine::command_prelude::*; -use nu_protocol::{ast::PathMember, Spanned}; +use nu_protocol::{ast::PathMember, Signals, Spanned}; use rmp::encode as mp; /// Max recursion depth @@ -189,7 +189,7 @@ pub(crate) fn write_value( // Convert range to list write_value( out, - &Value::list(val.into_range_iter(span, None).collect(), span), + &Value::list(val.into_range_iter(span, Signals::empty()).collect(), span), depth, )?; } diff --git a/crates/nu-command/src/formats/to/text.rs b/crates/nu-command/src/formats/to/text.rs index 1aa1114bce..33ca19f8f2 100644 --- a/crates/nu-command/src/formats/to/text.rs +++ b/crates/nu-command/src/formats/to/text.rs @@ -31,18 +31,19 @@ impl Command for ToText { fn run( &self, engine_state: &EngineState, - _stack: &mut Stack, + stack: &mut Stack, call: &Call, input: PipelineData, ) -> Result { let span = call.head; let input = input.try_expand_range()?; + let config = stack.get_config(engine_state); match input { PipelineData::Empty => Ok(Value::string(String::new(), span) .into_pipeline_data_with_metadata(update_metadata(None))), PipelineData::Value(value, ..) => { - let str = local_into_string(value, LINE_ENDING, engine_state.get_config()); + let str = local_into_string(value, LINE_ENDING, &config); Ok( Value::string(str, span) .into_pipeline_data_with_metadata(update_metadata(None)), @@ -50,7 +51,6 @@ impl Command for ToText { } PipelineData::ListStream(stream, meta) => { let span = stream.span(); - let config = engine_state.get_config().clone(); let iter = stream.into_inner().map(move |value| { let mut str = local_into_string(value, LINE_ENDING, &config); str.push_str(LINE_ENDING); @@ -60,7 +60,7 @@ impl Command for ToText { ByteStream::from_iter( iter, span, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), ByteStreamType::String, ), update_metadata(meta), diff --git a/crates/nu-command/src/generators/cal.rs b/crates/nu-command/src/generators/cal.rs index a257f3ab7c..018d9370dd 100644 --- a/crates/nu-command/src/generators/cal.rs +++ b/crates/nu-command/src/generators/cal.rs @@ -1,7 +1,7 @@ use chrono::{Datelike, Local, NaiveDate}; use nu_color_config::StyleComputer; use nu_engine::command_prelude::*; -use nu_protocol::ast::{Expr, Expression}; +use nu_protocol::ast::{self, Expr, Expression}; use std::collections::VecDeque; @@ -143,7 +143,7 @@ pub fn cal( style_computer, )?; - let mut table_no_index = Call::new(Span::unknown()); + let mut table_no_index = ast::Call::new(Span::unknown()); table_no_index.add_named(( Spanned { item: "index".to_string(), @@ -160,7 +160,12 @@ pub fn cal( let cal_table_output = Value::list(calendar_vec_deque.into_iter().collect(), tag).into_pipeline_data(); if !arguments.as_table { - crate::Table.run(engine_state, stack, &table_no_index, cal_table_output) + crate::Table.run( + engine_state, + stack, + &(&table_no_index).into(), + cal_table_output, + ) } else { Ok(cal_table_output) } diff --git a/crates/nu-command/src/generators/generate.rs b/crates/nu-command/src/generators/generate.rs index aeb375aab0..8d4f48d3fb 100644 --- a/crates/nu-command/src/generators/generate.rs +++ b/crates/nu-command/src/generators/generate.rs @@ -1,4 +1,3 @@ -use itertools::unfold; use nu_engine::{command_prelude::*, ClosureEval}; use nu_protocol::engine::Closure; @@ -81,7 +80,8 @@ used as the next argument to the closure, otherwise generation stops. // A type of Option is used to represent state. Invocation // will stop on None. Using Option allows functions to output // one final value before stopping. - let iter = unfold(Some(initial), move |state| { + let mut state = Some(initial); + let iter = std::iter::from_fn(move || { let arg = state.take()?; let (output, next_input) = match closure.run_with_value(arg) { @@ -160,13 +160,13 @@ used as the next argument to the closure, otherwise generation stops. // We use `state` to control when to stop, not `output`. By wrapping // it in a `Some`, we allow the generator to output `None` as a valid output // value. - *state = next_input; + state = next_input; Some(output) }); Ok(iter .flatten() - .into_pipeline_data(call.head, engine_state.ctrlc.clone())) + .into_pipeline_data(call.head, engine_state.signals().clone())) } } diff --git a/crates/nu-command/src/generators/seq.rs b/crates/nu-command/src/generators/seq.rs index 359d479ca0..3636d89d4c 100644 --- a/crates/nu-command/src/generators/seq.rs +++ b/crates/nu-command/src/generators/seq.rs @@ -129,7 +129,7 @@ pub fn run_seq( span, }, span, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), ) } else { ListStream::new( @@ -141,7 +141,7 @@ pub fn run_seq( span, }, span, - engine_state.ctrlc.clone(), + engine_state.signals().clone(), ) }; diff --git a/crates/nu-command/src/hash/generic_digest.rs b/crates/nu-command/src/hash/generic_digest.rs index ab15ccae7a..f098f8a0cb 100644 --- a/crates/nu-command/src/hash/generic_digest.rs +++ b/crates/nu-command/src/hash/generic_digest.rs @@ -96,7 +96,7 @@ where } } else { let args = Arguments { binary, cell_paths }; - operate(action::, args, input, head, engine_state.ctrlc.clone()) + operate(action::, args, input, head, engine_state.signals()) } } } diff --git a/crates/nu-command/src/help/help_aliases.rs b/crates/nu-command/src/help/help_aliases.rs index da03fa6398..41af9da0b3 100644 --- a/crates/nu-command/src/help/help_aliases.rs +++ b/crates/nu-command/src/help/help_aliases.rs @@ -143,7 +143,7 @@ pub fn help_aliases( long_desc.push_str("\n\n"); long_desc.push_str(&format!("{G}Expansion{RESET}:\n {alias_expansion}")); - let config = engine_state.get_config(); + let config = stack.get_config(engine_state); if !config.use_ansi_coloring { long_desc = nu_utils::strip_ansi_string_likely(long_desc); } diff --git a/crates/nu-command/src/help/help_modules.rs b/crates/nu-command/src/help/help_modules.rs index 5b39133a6d..c2c00cf400 100644 --- a/crates/nu-command/src/help/help_modules.rs +++ b/crates/nu-command/src/help/help_modules.rs @@ -230,7 +230,7 @@ pub fn help_modules( )); } - let config = engine_state.get_config(); + let config = stack.get_config(engine_state); if !config.use_ansi_coloring { long_desc = nu_utils::strip_ansi_string_likely(long_desc); } diff --git a/crates/nu-command/src/help/help_operators.rs b/crates/nu-command/src/help/help_operators.rs index a7beee3dd5..12803fad8e 100644 --- a/crates/nu-command/src/help/help_operators.rs +++ b/crates/nu-command/src/help/help_operators.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::ast::{Assignment, Bits, Boolean, Comparison, Math, Operator}; #[derive(Clone)] pub struct HelpOperators; @@ -27,282 +28,148 @@ impl Command for HelpOperators { _input: PipelineData, ) -> Result { let head = call.head; - let op_info = generate_operator_info(); - let mut recs = vec![]; - - for op in op_info { - recs.push(Value::record( + let mut operators = [ + Operator::Assignment(Assignment::Assign), + Operator::Assignment(Assignment::PlusAssign), + Operator::Assignment(Assignment::AppendAssign), + Operator::Assignment(Assignment::MinusAssign), + Operator::Assignment(Assignment::MultiplyAssign), + Operator::Assignment(Assignment::DivideAssign), + Operator::Comparison(Comparison::Equal), + Operator::Comparison(Comparison::NotEqual), + Operator::Comparison(Comparison::LessThan), + Operator::Comparison(Comparison::GreaterThan), + Operator::Comparison(Comparison::LessThanOrEqual), + Operator::Comparison(Comparison::GreaterThanOrEqual), + Operator::Comparison(Comparison::RegexMatch), + Operator::Comparison(Comparison::NotRegexMatch), + Operator::Comparison(Comparison::In), + Operator::Comparison(Comparison::NotIn), + Operator::Comparison(Comparison::StartsWith), + Operator::Comparison(Comparison::EndsWith), + Operator::Math(Math::Plus), + Operator::Math(Math::Append), + Operator::Math(Math::Minus), + Operator::Math(Math::Multiply), + Operator::Math(Math::Divide), + Operator::Math(Math::Modulo), + Operator::Math(Math::FloorDivision), + Operator::Math(Math::Pow), + Operator::Bits(Bits::BitOr), + Operator::Bits(Bits::BitXor), + Operator::Bits(Bits::BitAnd), + Operator::Bits(Bits::ShiftLeft), + Operator::Bits(Bits::ShiftRight), + Operator::Boolean(Boolean::And), + Operator::Boolean(Boolean::Or), + Operator::Boolean(Boolean::Xor), + ] + .into_iter() + .map(|op| { + Value::record( record! { - "type" => Value::string(op.op_type, head), - "operator" => Value::string(op.operator, head), - "name" => Value::string(op.name, head), - "description" => Value::string(op.description, head), - "precedence" => Value::int(op.precedence, head), + "type" => Value::string(op_type(&op), head), + "operator" => Value::string(op.to_string(), head), + "name" => Value::string(name(&op), head), + "description" => Value::string(description(&op), head), + "precedence" => Value::int(op.precedence().into(), head), }, head, - )); - } + ) + }) + .collect::>(); - Ok(Value::list(recs, head).into_pipeline_data()) + operators.push(Value::record( + record! { + "type" => Value::string("Boolean", head), + "operator" => Value::string("not", head), + "name" => Value::string("Not", head), + "description" => Value::string("Negates a value or expression.", head), + "precedence" => Value::int(0, head), + }, + head, + )); + + Ok(Value::list(operators, head).into_pipeline_data()) } } -struct OperatorInfo { - op_type: String, - operator: String, - name: String, - description: String, - precedence: i64, +fn op_type(operator: &Operator) -> &'static str { + match operator { + Operator::Comparison(_) => "Comparison", + Operator::Math(_) => "Math", + Operator::Boolean(_) => "Boolean", + Operator::Bits(_) => "Bitwise", + Operator::Assignment(_) => "Assignment", + } } -fn generate_operator_info() -> Vec { - vec![ - OperatorInfo { - op_type: "Assignment".into(), - operator: "=".into(), - name: "Assign".into(), - description: "Assigns a value to a variable.".into(), - precedence: 10, - }, - OperatorInfo { - op_type: "Assignment".into(), - operator: "+=".into(), - name: "PlusAssign".into(), - description: "Adds a value to a variable.".into(), - precedence: 10, - }, - OperatorInfo { - op_type: "Assignment".into(), - operator: "++=".into(), - name: "AppendAssign".into(), - description: "Appends a list or a value to a variable.".into(), - precedence: 10, - }, - OperatorInfo { - op_type: "Assignment".into(), - operator: "-=".into(), - name: "MinusAssign".into(), - description: "Subtracts a value from a variable.".into(), - precedence: 10, - }, - OperatorInfo { - op_type: "Assignment".into(), - operator: "*=".into(), - name: "MultiplyAssign".into(), - description: "Multiplies a variable by a value.".into(), - precedence: 10, - }, - OperatorInfo { - op_type: "Assignment".into(), - operator: "/=".into(), - name: "DivideAssign".into(), - description: "Divides a variable by a value.".into(), - precedence: 10, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "==".into(), - name: "Equal".into(), - description: "Checks if two values are equal.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "!=".into(), - name: "NotEqual".into(), - description: "Checks if two values are not equal.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "<".into(), - name: "LessThan".into(), - description: "Checks if a value is less than another.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "<=".into(), - name: "LessThanOrEqual".into(), - description: "Checks if a value is less than or equal to another.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: ">".into(), - name: "GreaterThan".into(), - description: "Checks if a value is greater than another.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: ">=".into(), - name: "GreaterThanOrEqual".into(), - description: "Checks if a value is greater than or equal to another.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "=~".into(), - name: "RegexMatch".into(), - description: "Checks if a value matches a regular expression.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "!~".into(), - name: "NotRegexMatch".into(), - description: "Checks if a value does not match a regular expression.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "in".into(), - name: "In".into(), - description: "Checks if a value is in a list or string.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "not-in".into(), - name: "NotIn".into(), - description: "Checks if a value is not in a list or string.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "starts-with".into(), - name: "StartsWith".into(), - description: "Checks if a string starts with another.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "ends-with".into(), - name: "EndsWith".into(), - description: "Checks if a string ends with another.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Comparison".into(), - operator: "not".into(), - name: "UnaryNot".into(), - description: "Negates a value or expression.".into(), - precedence: 0, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "+".into(), - name: "Plus".into(), - description: "Adds two values.".into(), - precedence: 90, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "++".into(), - name: "Append".into(), - description: "Appends two lists or a list and a value.".into(), - precedence: 80, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "-".into(), - name: "Minus".into(), - description: "Subtracts two values.".into(), - precedence: 90, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "*".into(), - name: "Multiply".into(), - description: "Multiplies two values.".into(), - precedence: 95, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "/".into(), - name: "Divide".into(), - description: "Divides two values.".into(), - precedence: 95, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "//".into(), - name: "FloorDivision".into(), - description: "Divides two values and floors the result.".into(), - precedence: 95, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "mod".into(), - name: "Modulo".into(), - description: "Divides two values and returns the remainder.".into(), - precedence: 95, - }, - OperatorInfo { - op_type: "Math".into(), - operator: "**".into(), - name: "Pow ".into(), - description: "Raises one value to the power of another.".into(), - precedence: 100, - }, - OperatorInfo { - op_type: "Bitwise".into(), - operator: "bit-or".into(), - name: "BitOr".into(), - description: "Performs a bitwise OR on two values.".into(), - precedence: 60, - }, - OperatorInfo { - op_type: "Bitwise".into(), - operator: "bit-xor".into(), - name: "BitXor".into(), - description: "Performs a bitwise XOR on two values.".into(), - precedence: 70, - }, - OperatorInfo { - op_type: "Bitwise".into(), - operator: "bit-and".into(), - name: "BitAnd".into(), - description: "Performs a bitwise AND on two values.".into(), - precedence: 75, - }, - OperatorInfo { - op_type: "Bitwise".into(), - operator: "bit-shl".into(), - name: "ShiftLeft".into(), - description: "Shifts a value left by another.".into(), - precedence: 85, - }, - OperatorInfo { - op_type: "Bitwise".into(), - operator: "bit-shr".into(), - name: "ShiftRight".into(), - description: "Shifts a value right by another.".into(), - precedence: 85, - }, - OperatorInfo { - op_type: "Boolean".into(), - operator: "and".into(), - name: "And".into(), - description: "Checks if two values are true.".into(), - precedence: 50, - }, - OperatorInfo { - op_type: "Boolean".into(), - operator: "or".into(), - name: "Or".into(), - description: "Checks if either value is true.".into(), - precedence: 40, - }, - OperatorInfo { - op_type: "Boolean".into(), - operator: "xor".into(), - name: "Xor".into(), - description: "Checks if one value is true and the other is false.".into(), - precedence: 45, - }, - ] +fn name(operator: &Operator) -> String { + match operator { + Operator::Comparison(op) => format!("{op:?}"), + Operator::Math(op) => format!("{op:?}"), + Operator::Boolean(op) => format!("{op:?}"), + Operator::Bits(op) => format!("{op:?}"), + Operator::Assignment(op) => format!("{op:?}"), + } +} + +fn description(operator: &Operator) -> &'static str { + // NOTE: if you have to add an operator here, also add it to the operator list above. + match operator { + Operator::Comparison(Comparison::Equal) => "Checks if two values are equal.", + Operator::Comparison(Comparison::NotEqual) => "Checks if two values are not equal.", + Operator::Comparison(Comparison::LessThan) => "Checks if a value is less than another.", + Operator::Comparison(Comparison::GreaterThan) => { + "Checks if a value is greater than another." + } + Operator::Comparison(Comparison::LessThanOrEqual) => { + "Checks if a value is less than or equal to another." + } + Operator::Comparison(Comparison::GreaterThanOrEqual) => { + "Checks if a value is greater than or equal to another." + } + Operator::Comparison(Comparison::RegexMatch) => { + "Checks if a value matches a regular expression." + } + Operator::Comparison(Comparison::NotRegexMatch) => { + "Checks if a value does not match a regular expression." + } + Operator::Comparison(Comparison::In) => { + "Checks if a value is in a list, is part of a string, or is a key in a record." + } + Operator::Comparison(Comparison::NotIn) => { + "Checks if a value is not in a list, is not part of a string, or is not a key in a record." + } + Operator::Comparison(Comparison::StartsWith) => "Checks if a string starts with another.", + Operator::Comparison(Comparison::EndsWith) => "Checks if a string ends with another.", + Operator::Math(Math::Plus) => "Adds two values.", + Operator::Math(Math::Append) => { + "Appends two lists, a list and a value, two strings, or two binary values." + } + Operator::Math(Math::Minus) => "Subtracts two values.", + Operator::Math(Math::Multiply) => "Multiplies two values.", + Operator::Math(Math::Divide) => "Divides two values.", + Operator::Math(Math::Modulo) => "Divides two values and returns the remainder.", + Operator::Math(Math::FloorDivision) => "Divides two values and floors the result.", + Operator::Math(Math::Pow) => "Raises one value to the power of another.", + Operator::Boolean(Boolean::And) => "Checks if both values are true.", + Operator::Boolean(Boolean::Or) => "Checks if either value is true.", + Operator::Boolean(Boolean::Xor) => "Checks if one value is true and the other is false.", + Operator::Bits(Bits::BitOr) => "Performs a bitwise OR on two values.", + Operator::Bits(Bits::BitXor) => "Performs a bitwise XOR on two values.", + Operator::Bits(Bits::BitAnd) => "Performs a bitwise AND on two values.", + Operator::Bits(Bits::ShiftLeft) => "Bitwise shifts a value left by another.", + Operator::Bits(Bits::ShiftRight) => "Bitwise shifts a value right by another.", + Operator::Assignment(Assignment::Assign) => "Assigns a value to a variable.", + Operator::Assignment(Assignment::PlusAssign) => "Adds a value to a variable.", + Operator::Assignment(Assignment::AppendAssign) => { + "Appends a list, a value, a string, or a binary value to a variable." + } + Operator::Assignment(Assignment::MinusAssign) => "Subtracts a value from a variable.", + Operator::Assignment(Assignment::MultiplyAssign) => "Multiplies a variable by a value.", + Operator::Assignment(Assignment::DivideAssign) => "Divides a variable by a value.", + } } #[cfg(test)] diff --git a/crates/nu-command/src/lib.rs b/crates/nu-command/src/lib.rs index a854fe3507..55c4974325 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod bytes; mod charting; mod conversions; diff --git a/crates/nu-command/src/math/abs.rs b/crates/nu-command/src/math/abs.rs index 8f653142fa..b16097ba8d 100644 --- a/crates/nu-command/src/math/abs.rs +++ b/crates/nu-command/src/math/abs.rs @@ -42,10 +42,7 @@ impl Command for SubCommand { input: PipelineData, ) -> Result { let head = call.head; - input.map( - move |value| abs_helper(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| abs_helper(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/math/ceil.rs b/crates/nu-command/src/math/ceil.rs index 0b0f6d1696..0886ff0bc5 100644 --- a/crates/nu-command/src/math/ceil.rs +++ b/crates/nu-command/src/math/ceil.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/math/floor.rs b/crates/nu-command/src/math/floor.rs index e85e3ca674..c2b34c2cf9 100644 --- a/crates/nu-command/src/math/floor.rs +++ b/crates/nu-command/src/math/floor.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/math/log.rs b/crates/nu-command/src/math/log.rs index 90fad17daf..7a7e4bd27b 100644 --- a/crates/nu-command/src/math/log.rs +++ b/crates/nu-command/src/math/log.rs @@ -59,7 +59,7 @@ impl Command for SubCommand { let base = base.item; input.map( move |value| operate(value, head, base), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/math/round.rs b/crates/nu-command/src/math/round.rs index 7693c9a316..eafe669cf1 100644 --- a/crates/nu-command/src/math/round.rs +++ b/crates/nu-command/src/math/round.rs @@ -50,7 +50,7 @@ impl Command for SubCommand { } input.map( move |value| operate(value, head, precision_param), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/math/sqrt.rs b/crates/nu-command/src/math/sqrt.rs index c9c9765912..a3ab5fbbd8 100644 --- a/crates/nu-command/src/math/sqrt.rs +++ b/crates/nu-command/src/math/sqrt.rs @@ -41,10 +41,7 @@ impl Command for SubCommand { if matches!(input, PipelineData::Empty) { return Err(ShellError::PipelineEmpty { dst_span: head }); } - input.map( - move |value| operate(value, head), - engine_state.ctrlc.clone(), - ) + input.map(move |value| operate(value, head), engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/math/utils.rs b/crates/nu-command/src/math/utils.rs index 9d2c15e15f..62f96ea073 100644 --- a/crates/nu-command/src/math/utils.rs +++ b/crates/nu-command/src/math/utils.rs @@ -1,6 +1,6 @@ use core::slice; use indexmap::IndexMap; -use nu_protocol::{ast::Call, IntoPipelineData, PipelineData, ShellError, Span, Value}; +use nu_protocol::{engine::Call, IntoPipelineData, PipelineData, ShellError, Signals, Span, Value}; pub fn run_with_function( call: &Call, @@ -92,7 +92,7 @@ pub fn calculate( } PipelineData::Value(Value::Range { val, .. }, ..) => { let new_vals: Result, ShellError> = val - .into_range_iter(span, None) + .into_range_iter(span, Signals::empty()) .map(|val| mf(&[val], span, name)) .collect(); diff --git a/crates/nu-command/src/network/http/client.rs b/crates/nu-command/src/network/http/client.rs index 2c02d7be33..deeb768eea 100644 --- a/crates/nu-command/src/network/http/client.rs +++ b/crates/nu-command/src/network/http/client.rs @@ -5,16 +5,12 @@ use base64::{ Engine, }; use nu_engine::command_prelude::*; -use nu_protocol::ByteStream; +use nu_protocol::{ByteStream, Signals}; use std::{ collections::HashMap, path::PathBuf, str::FromStr, - sync::{ - atomic::AtomicBool, - mpsc::{self, RecvTimeoutError}, - Arc, - }, + sync::mpsc::{self, RecvTimeoutError}, time::Duration, }; use ureq::{Error, ErrorKind, Request, Response}; @@ -129,7 +125,7 @@ pub fn response_to_buffer( let reader = response.into_reader(); PipelineData::ByteStream( - ByteStream::read(reader, span, engine_state.ctrlc.clone(), response_type) + ByteStream::read(reader, span, engine_state.signals().clone(), response_type) .with_known_size(buffer_size), None, ) @@ -192,13 +188,14 @@ pub fn send_request( request: Request, http_body: HttpBody, content_type: Option, - ctrl_c: Option>, + span: Span, + signals: &Signals, ) -> Result { let request_url = request.url().to_string(); match http_body { HttpBody::None => { - send_cancellable_request(&request_url, Box::new(|| request.call()), ctrl_c) + send_cancellable_request(&request_url, Box::new(|| request.call()), span, signals) } HttpBody::ByteStream(byte_stream) => { let req = if let Some(content_type) = content_type { @@ -207,7 +204,7 @@ pub fn send_request( request }; - send_cancellable_request_bytes(&request_url, req, byte_stream, ctrl_c) + send_cancellable_request_bytes(&request_url, req, byte_stream, span, signals) } HttpBody::Value(body) => { let (body_type, req) = match content_type { @@ -224,20 +221,32 @@ pub fn send_request( Value::Binary { val, .. } => send_cancellable_request( &request_url, Box::new(move || req.send_bytes(&val)), - ctrl_c, + span, + signals, ), Value::String { .. } if body_type == BodyType::Json => { let data = value_to_json_value(&body)?; - send_cancellable_request(&request_url, Box::new(|| req.send_json(data)), ctrl_c) + send_cancellable_request( + &request_url, + Box::new(|| req.send_json(data)), + span, + signals, + ) } Value::String { val, .. } => send_cancellable_request( &request_url, Box::new(move || req.send_string(&val)), - ctrl_c, + span, + signals, ), Value::Record { .. } if body_type == BodyType::Json => { let data = value_to_json_value(&body)?; - send_cancellable_request(&request_url, Box::new(|| req.send_json(data)), ctrl_c) + send_cancellable_request( + &request_url, + Box::new(|| req.send_json(data)), + span, + signals, + ) } Value::Record { val, .. } if body_type == BodyType::Form => { let mut data: Vec<(String, String)> = Vec::with_capacity(val.len()); @@ -254,7 +263,7 @@ pub fn send_request( .collect::>(); req.send_form(&data) }; - send_cancellable_request(&request_url, Box::new(request_fn), ctrl_c) + send_cancellable_request(&request_url, Box::new(request_fn), span, signals) } Value::List { vals, .. } if body_type == BodyType::Form => { if vals.len() % 2 != 0 { @@ -276,11 +285,16 @@ pub fn send_request( .collect::>(); req.send_form(&data) }; - send_cancellable_request(&request_url, Box::new(request_fn), ctrl_c) + send_cancellable_request(&request_url, Box::new(request_fn), span, signals) } Value::List { .. } if body_type == BodyType::Json => { let data = value_to_json_value(&body)?; - send_cancellable_request(&request_url, Box::new(|| req.send_json(data)), ctrl_c) + send_cancellable_request( + &request_url, + Box::new(|| req.send_json(data)), + span, + signals, + ) } _ => Err(ShellErrorOrRequestError::ShellError(ShellError::IOError { msg: "unsupported body input".into(), @@ -295,7 +309,8 @@ pub fn send_request( fn send_cancellable_request( request_url: &str, request_fn: Box Result + Sync + Send>, - ctrl_c: Option>, + span: Span, + signals: &Signals, ) -> Result { let (tx, rx) = mpsc::channel::>(); @@ -310,12 +325,7 @@ fn send_cancellable_request( // ...and poll the channel for responses loop { - if nu_utils::ctrl_c::was_pressed(&ctrl_c) { - // Return early and give up on the background thread. The connection will either time out or be disconnected - return Err(ShellErrorOrRequestError::ShellError( - ShellError::InterruptedByUser { span: None }, - )); - } + signals.check(span)?; // 100ms wait time chosen arbitrarily match rx.recv_timeout(Duration::from_millis(100)) { @@ -336,7 +346,8 @@ fn send_cancellable_request_bytes( request_url: &str, request: Request, byte_stream: ByteStream, - ctrl_c: Option>, + span: Span, + signals: &Signals, ) -> Result { let (tx, rx) = mpsc::channel::>(); let request_url_string = request_url.to_string(); @@ -369,12 +380,7 @@ fn send_cancellable_request_bytes( // ...and poll the channel for responses loop { - if nu_utils::ctrl_c::was_pressed(&ctrl_c) { - // Return early and give up on the background thread. The connection will either time out or be disconnected - return Err(ShellErrorOrRequestError::ShellError( - ShellError::InterruptedByUser { span: None }, - )); - } + signals.check(span)?; // 100ms wait time chosen arbitrarily match rx.recv_timeout(Duration::from_millis(100)) { diff --git a/crates/nu-command/src/network/http/delete.rs b/crates/nu-command/src/network/http/delete.rs index c2ef774a01..f32c9d15bb 100644 --- a/crates/nu-command/src/network/http/delete.rs +++ b/crates/nu-command/src/network/http/delete.rs @@ -203,7 +203,6 @@ fn helper( args: Arguments, ) -> Result { let span = args.url.span(); - let ctrl_c = engine_state.ctrlc.clone(); let (requested_url, _) = http_parse_url(call, span, args.url)?; let redirect_mode = http_parse_redirect_mode(args.redirect)?; @@ -214,7 +213,13 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request.clone(), args.data, args.content_type, ctrl_c); + let response = send_request( + request.clone(), + args.data, + args.content_type, + call.head, + engine_state.signals(), + ); let request_flags = RequestFlags { raw: args.raw, diff --git a/crates/nu-command/src/network/http/get.rs b/crates/nu-command/src/network/http/get.rs index 04582d5f00..3b702404f4 100644 --- a/crates/nu-command/src/network/http/get.rs +++ b/crates/nu-command/src/network/http/get.rs @@ -171,7 +171,6 @@ fn helper( args: Arguments, ) -> Result { let span = args.url.span(); - let ctrl_c = engine_state.ctrlc.clone(); let (requested_url, _) = http_parse_url(call, span, args.url)?; let redirect_mode = http_parse_redirect_mode(args.redirect)?; @@ -182,7 +181,13 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request.clone(), HttpBody::None, None, ctrl_c); + let response = send_request( + request.clone(), + HttpBody::None, + None, + call.head, + engine_state.signals(), + ); let request_flags = RequestFlags { raw: args.raw, diff --git a/crates/nu-command/src/network/http/head.rs b/crates/nu-command/src/network/http/head.rs index 57dfafcba2..797fa138e3 100644 --- a/crates/nu-command/src/network/http/head.rs +++ b/crates/nu-command/src/network/http/head.rs @@ -1,13 +1,11 @@ +use super::client::HttpBody; use crate::network::http::client::{ check_response_redirection, http_client, http_parse_redirect_mode, http_parse_url, request_add_authorization_header, request_add_custom_headers, request_handle_response_headers, request_set_timeout, send_request, }; use nu_engine::command_prelude::*; - -use std::sync::{atomic::AtomicBool, Arc}; - -use super::client::HttpBody; +use nu_protocol::Signals; #[derive(Clone)] pub struct SubCommand; @@ -133,9 +131,8 @@ fn run_head( timeout: call.get_flag(engine_state, stack, "max-time")?, redirect: call.get_flag(engine_state, stack, "redirect-mode")?, }; - let ctrl_c = engine_state.ctrlc.clone(); - helper(engine_state, stack, call, args, ctrl_c) + helper(engine_state, stack, call, args, engine_state.signals()) } // Helper function that actually goes to retrieve the resource from the url given @@ -145,7 +142,7 @@ fn helper( stack: &mut Stack, call: &Call, args: Arguments, - ctrlc: Option>, + signals: &Signals, ) -> Result { let span = args.url.span(); let (requested_url, _) = http_parse_url(call, span, args.url)?; @@ -158,7 +155,7 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request, HttpBody::None, None, ctrlc); + let response = send_request(request, HttpBody::None, None, call.head, signals); check_response_redirection(redirect_mode, span, &response)?; request_handle_response_headers(span, response) } diff --git a/crates/nu-command/src/network/http/options.rs b/crates/nu-command/src/network/http/options.rs index 91ecac02d5..cd9a1f79c1 100644 --- a/crates/nu-command/src/network/http/options.rs +++ b/crates/nu-command/src/network/http/options.rs @@ -151,7 +151,6 @@ fn helper( args: Arguments, ) -> Result { let span = args.url.span(); - let ctrl_c = engine_state.ctrlc.clone(); let (requested_url, _) = http_parse_url(call, span, args.url)?; let client = http_client(args.insecure, RedirectMode::Follow, engine_state, stack)?; @@ -161,7 +160,13 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request.clone(), HttpBody::None, None, ctrl_c); + let response = send_request( + request.clone(), + HttpBody::None, + None, + call.head, + engine_state.signals(), + ); // http options' response always showed in header, so we set full to true. // And `raw` is useless too because options method doesn't return body, here we set to true diff --git a/crates/nu-command/src/network/http/patch.rs b/crates/nu-command/src/network/http/patch.rs index 2cf66a2a82..7f49781284 100644 --- a/crates/nu-command/src/network/http/patch.rs +++ b/crates/nu-command/src/network/http/patch.rs @@ -205,7 +205,6 @@ fn helper( args: Arguments, ) -> Result { let span = args.url.span(); - let ctrl_c = engine_state.ctrlc.clone(); let (requested_url, _) = http_parse_url(call, span, args.url)?; let redirect_mode = http_parse_redirect_mode(args.redirect)?; @@ -216,7 +215,13 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request.clone(), args.data, args.content_type, ctrl_c); + let response = send_request( + request.clone(), + args.data, + args.content_type, + call.head, + engine_state.signals(), + ); let request_flags = RequestFlags { raw: args.raw, diff --git a/crates/nu-command/src/network/http/post.rs b/crates/nu-command/src/network/http/post.rs index d48bbcc9fe..ea4ec093f3 100644 --- a/crates/nu-command/src/network/http/post.rs +++ b/crates/nu-command/src/network/http/post.rs @@ -203,7 +203,6 @@ fn helper( args: Arguments, ) -> Result { let span = args.url.span(); - let ctrl_c = engine_state.ctrlc.clone(); let (requested_url, _) = http_parse_url(call, span, args.url)?; let redirect_mode = http_parse_redirect_mode(args.redirect)?; @@ -214,7 +213,13 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request.clone(), args.data, args.content_type, ctrl_c); + let response = send_request( + request.clone(), + args.data, + args.content_type, + call.head, + engine_state.signals(), + ); let request_flags = RequestFlags { raw: args.raw, diff --git a/crates/nu-command/src/network/http/put.rs b/crates/nu-command/src/network/http/put.rs index dbd3b245cb..e2118ea359 100644 --- a/crates/nu-command/src/network/http/put.rs +++ b/crates/nu-command/src/network/http/put.rs @@ -204,7 +204,6 @@ fn helper( args: Arguments, ) -> Result { let span = args.url.span(); - let ctrl_c = engine_state.ctrlc.clone(); let (requested_url, _) = http_parse_url(call, span, args.url)?; let redirect_mode = http_parse_redirect_mode(args.redirect)?; @@ -215,7 +214,13 @@ fn helper( request = request_add_authorization_header(args.user, args.password, request); request = request_add_custom_headers(args.headers, request)?; - let response = send_request(request.clone(), args.data, args.content_type, ctrl_c); + let response = send_request( + request.clone(), + args.data, + args.content_type, + call.head, + engine_state.signals(), + ); let request_flags = RequestFlags { raw: args.raw, diff --git a/crates/nu-command/src/network/url/decode.rs b/crates/nu-command/src/network/url/decode.rs index 8789eb13ca..b98e50a56e 100644 --- a/crates/nu-command/src/network/url/decode.rs +++ b/crates/nu-command/src/network/url/decode.rs @@ -48,7 +48,7 @@ impl Command for SubCommand { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/network/url/encode.rs b/crates/nu-command/src/network/url/encode.rs index 845487963b..96e0289903 100644 --- a/crates/nu-command/src/network/url/encode.rs +++ b/crates/nu-command/src/network/url/encode.rs @@ -50,15 +50,9 @@ impl Command for SubCommand { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); if call.has_flag(engine_state, stack, "all")? { - operate( - action_all, - args, - input, - call.head, - engine_state.ctrlc.clone(), - ) + operate(action_all, args, input, call.head, engine_state.signals()) } else { - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } } diff --git a/crates/nu-command/src/network/url/parse.rs b/crates/nu-command/src/network/url/parse.rs index e71c8d472a..287c64c7b9 100644 --- a/crates/nu-command/src/network/url/parse.rs +++ b/crates/nu-command/src/network/url/parse.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::Config; use url::Url; #[derive(Clone)] @@ -38,11 +39,15 @@ impl Command for SubCommand { fn run( &self, engine_state: &EngineState, - _: &mut Stack, + stack: &mut Stack, call: &Call, input: PipelineData, ) -> Result { - parse(input.into_value(call.head)?, call.head, engine_state) + parse( + input.into_value(call.head)?, + call.head, + &stack.get_config(engine_state), + ) } fn examples(&self) -> Vec { @@ -68,12 +73,12 @@ impl Command for SubCommand { } } -fn get_url_string(value: &Value, engine_state: &EngineState) -> String { - value.to_expanded_string("", engine_state.get_config()) +fn get_url_string(value: &Value, config: &Config) -> String { + value.to_expanded_string("", config) } -fn parse(value: Value, head: Span, engine_state: &EngineState) -> Result { - let url_string = get_url_string(&value, engine_state); +fn parse(value: Value, head: Span, config: &Config) -> Result { + let url_string = get_url_string(&value, config); let result_url = Url::parse(url_string.as_str()); diff --git a/crates/nu-command/src/path/basename.rs b/crates/nu-command/src/path/basename.rs index 04f7cf380e..8b7082048e 100644 --- a/crates/nu-command/src/path/basename.rs +++ b/crates/nu-command/src/path/basename.rs @@ -61,7 +61,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&get_basename, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -82,7 +82,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&get_basename, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/dirname.rs b/crates/nu-command/src/path/dirname.rs index 2eb864215e..218091ee34 100644 --- a/crates/nu-command/src/path/dirname.rs +++ b/crates/nu-command/src/path/dirname.rs @@ -69,7 +69,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&get_dirname, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -91,7 +91,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&get_dirname, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/exists.rs b/crates/nu-command/src/path/exists.rs index a99f04113e..86b00c6024 100644 --- a/crates/nu-command/src/path/exists.rs +++ b/crates/nu-command/src/path/exists.rs @@ -65,7 +65,7 @@ If you need to distinguish dirs and files, please use `path type`."# } input.map( move |value| super::operate(&exists, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -87,7 +87,7 @@ If you need to distinguish dirs and files, please use `path type`."# } input.map( move |value| super::operate(&exists, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/expand.rs b/crates/nu-command/src/path/expand.rs index 18497c6426..ac51978810 100644 --- a/crates/nu-command/src/path/expand.rs +++ b/crates/nu-command/src/path/expand.rs @@ -70,7 +70,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&expand, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -93,7 +93,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&expand, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/parse.rs b/crates/nu-command/src/path/parse.rs index 039f1012ed..cec2f5c6ac 100644 --- a/crates/nu-command/src/path/parse.rs +++ b/crates/nu-command/src/path/parse.rs @@ -63,7 +63,7 @@ On Windows, an extra 'prefix' column is added."# } input.map( move |value| super::operate(&parse, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -84,7 +84,7 @@ On Windows, an extra 'prefix' column is added."# } input.map( move |value| super::operate(&parse, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/relative_to.rs b/crates/nu-command/src/path/relative_to.rs index 6533f6fa79..35df385d74 100644 --- a/crates/nu-command/src/path/relative_to.rs +++ b/crates/nu-command/src/path/relative_to.rs @@ -67,7 +67,7 @@ path."# } input.map( move |value| super::operate(&relative_to, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -88,7 +88,7 @@ path."# } input.map( move |value| super::operate(&relative_to, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/split.rs b/crates/nu-command/src/path/split.rs index ea8e7e1b1f..80d86fb998 100644 --- a/crates/nu-command/src/path/split.rs +++ b/crates/nu-command/src/path/split.rs @@ -51,7 +51,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&split, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -70,7 +70,7 @@ impl Command for SubCommand { } input.map( move |value| super::operate(&split, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/path/type.rs b/crates/nu-command/src/path/type.rs index e58e697604..010fd295ad 100644 --- a/crates/nu-command/src/path/type.rs +++ b/crates/nu-command/src/path/type.rs @@ -1,13 +1,11 @@ use super::PathSubcommandArguments; use nu_engine::command_prelude::*; +use nu_path::AbsolutePathBuf; use nu_protocol::engine::StateWorkingSet; -use std::{ - io, - path::{Path, PathBuf}, -}; +use std::{io, path::Path}; struct Arguments { - pwd: PathBuf, + pwd: AbsolutePathBuf, } impl PathSubcommandArguments for Arguments {} @@ -64,7 +62,7 @@ If the path does not exist, null will be returned."# } input.map( move |value| super::operate(&path_type, &args, value, head), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -85,7 +83,7 @@ If the path does not exist, null will be returned."# } input.map( move |value| super::operate(&path_type, &args, value, head), - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/platform/ansi/ansi_.rs b/crates/nu-command/src/platform/ansi/ansi_.rs index 9e0b0bba66..3daed5e157 100644 --- a/crates/nu-command/src/platform/ansi/ansi_.rs +++ b/crates/nu-command/src/platform/ansi/ansi_.rs @@ -1,11 +1,8 @@ use nu_ansi_term::*; use nu_engine::command_prelude::*; -use nu_protocol::engine::StateWorkingSet; +use nu_protocol::{engine::StateWorkingSet, Signals}; use once_cell::sync::Lazy; -use std::{ - collections::HashMap, - sync::{atomic::AtomicBool, Arc}, -}; +use std::collections::HashMap; #[derive(Clone)] pub struct AnsiCommand; @@ -656,11 +653,14 @@ Operating system commands: let list: bool = call.has_flag(engine_state, stack, "list")?; let escape: bool = call.has_flag(engine_state, stack, "escape")?; let osc: bool = call.has_flag(engine_state, stack, "osc")?; - let use_ansi_coloring = engine_state.get_config().use_ansi_coloring; - let ctrlc = engine_state.ctrlc.clone(); + let use_ansi_coloring = stack.get_config(engine_state).use_ansi_coloring; if list { - return Ok(generate_ansi_code_list(ctrlc, call.head, use_ansi_coloring)); + return Ok(generate_ansi_code_list( + engine_state.signals().clone(), + call.head, + use_ansi_coloring, + )); } // The code can now be one of the ansi abbreviations like green_bold @@ -676,7 +676,7 @@ Operating system commands: } }; - let output = heavy_lifting(code, escape, osc, call)?; + let output = heavy_lifting(code, escape, osc, stack, call)?; Ok(Value::string(output, call.head).into_pipeline_data()) } @@ -691,10 +691,13 @@ Operating system commands: let escape: bool = call.has_flag_const(working_set, "escape")?; let osc: bool = call.has_flag_const(working_set, "osc")?; let use_ansi_coloring = working_set.get_config().use_ansi_coloring; - let ctrlc = working_set.permanent().ctrlc.clone(); if list { - return Ok(generate_ansi_code_list(ctrlc, call.head, use_ansi_coloring)); + return Ok(generate_ansi_code_list( + working_set.permanent().signals().clone(), + call.head, + use_ansi_coloring, + )); } // The code can now be one of the ansi abbreviations like green_bold @@ -710,26 +713,30 @@ Operating system commands: } }; - let output = heavy_lifting(code, escape, osc, call)?; + let output = heavy_lifting(code, escape, osc, &Stack::new(), call)?; Ok(Value::string(output, call.head).into_pipeline_data()) } } -fn heavy_lifting(code: Value, escape: bool, osc: bool, call: &Call) -> Result { +fn heavy_lifting( + code: Value, + escape: bool, + osc: bool, + stack: &Stack, + call: &Call, +) -> Result { let param_is_string = matches!(code, Value::String { .. }); if escape && osc { return Err(ShellError::IncompatibleParameters { left_message: "escape".into(), left_span: call - .get_named_arg("escape") - .expect("Unexpected missing argument") - .span, + .get_flag_span(stack, "escape") + .expect("Unexpected missing argument"), right_message: "osc".into(), right_span: call - .get_named_arg("osc") - .expect("Unexpected missing argument") - .span, + .get_flag_span(stack, "osc") + .expect("Unexpected missing argument"), }); } let code_string = if param_is_string { @@ -741,10 +748,7 @@ fn heavy_lifting(code: Value, escape: bool, osc: bool, call: &Call) -> Result = code_string.chars().collect(); if code_vec[0] == '\\' { - let span = match call.get_flag_expr("escape") { - Some(expr) => expr.span, - None => call.head, - }; + let span = call.get_flag_span(stack, "escape").unwrap_or(call.head); return Err(ShellError::TypeMismatch { err_message: "no need for escape characters".into(), @@ -827,7 +831,7 @@ pub fn str_to_ansi(s: &str) -> Option { } fn generate_ansi_code_list( - ctrlc: Option>, + signals: Signals, call_span: Span, use_ansi_coloring: bool, ) -> PipelineData { @@ -862,7 +866,7 @@ fn generate_ansi_code_list( Value::record(record, call_span) }) - .into_pipeline_data(call_span, ctrlc) + .into_pipeline_data(call_span, signals.clone()) } fn build_ansi_hashmap(v: &[AnsiCode]) -> HashMap<&str, &str> { diff --git a/crates/nu-command/src/platform/ansi/link.rs b/crates/nu-command/src/platform/ansi/link.rs index 68fc17977b..b45b365d3f 100644 --- a/crates/nu-command/src/platform/ansi/link.rs +++ b/crates/nu-command/src/platform/ansi/link.rs @@ -91,12 +91,12 @@ fn operate( if column_paths.is_empty() { input.map( move |v| process_value(&v, text.as_deref()), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } else { input.map( move |v| process_each_path(v, &column_paths, text.as_deref(), command_span), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } } diff --git a/crates/nu-command/src/platform/ansi/strip.rs b/crates/nu-command/src/platform/ansi/strip.rs index 35d410161c..4f7f8ea596 100644 --- a/crates/nu-command/src/platform/ansi/strip.rs +++ b/crates/nu-command/src/platform/ansi/strip.rs @@ -1,10 +1,12 @@ +use std::sync::Arc; + use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; use nu_protocol::Config; pub struct Arguments { cell_paths: Option>, - config: Config, + config: Arc, } impl CmdArgument for Arguments { @@ -51,12 +53,9 @@ impl Command for SubCommand { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 1)?; let cell_paths = (!cell_paths.is_empty()).then_some(cell_paths); - let config = engine_state.get_config(); - let args = Arguments { - cell_paths, - config: config.clone(), - }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + let config = stack.get_config(engine_state); + let args = Arguments { cell_paths, config }; + operate(action, args, input, call.head, engine_state.signals()) } fn examples(&self) -> Vec { diff --git a/crates/nu-command/src/platform/dir_info.rs b/crates/nu-command/src/platform/dir_info.rs index e4a3039672..10ce4f5420 100644 --- a/crates/nu-command/src/platform/dir_info.rs +++ b/crates/nu-command/src/platform/dir_info.rs @@ -1,10 +1,7 @@ use filesize::file_real_size_fast; use nu_glob::Pattern; -use nu_protocol::{record, ShellError, Span, Value}; -use std::{ - path::PathBuf, - sync::{atomic::AtomicBool, Arc}, -}; +use nu_protocol::{record, ShellError, Signals, Span, Value}; +use std::path::PathBuf; #[derive(Debug, Clone)] pub struct DirBuilder { @@ -82,8 +79,9 @@ impl DirInfo { path: impl Into, params: &DirBuilder, depth: Option, - ctrl_c: Option>, - ) -> Self { + span: Span, + signals: &Signals, + ) -> Result { let path = path.into(); let mut s = Self { @@ -107,14 +105,12 @@ impl DirInfo { match std::fs::read_dir(&s.path) { Ok(d) => { for f in d { - if nu_utils::ctrl_c::was_pressed(&ctrl_c) { - break; - } + signals.check(span)?; match f { Ok(i) => match i.file_type() { Ok(t) if t.is_dir() => { - s = s.add_dir(i.path(), depth, params, ctrl_c.clone()) + s = s.add_dir(i.path(), depth, params, span, signals)? } Ok(_t) => s = s.add_file(i.path(), params), Err(e) => s = s.add_error(e.into()), @@ -125,7 +121,7 @@ impl DirInfo { } Err(e) => s = s.add_error(e.into()), } - s + Ok(s) } fn add_dir( @@ -133,21 +129,22 @@ impl DirInfo { path: impl Into, mut depth: Option, params: &DirBuilder, - ctrl_c: Option>, - ) -> Self { + span: Span, + signals: &Signals, + ) -> Result { if let Some(current) = depth { if let Some(new) = current.checked_sub(1) { depth = Some(new); } else { - return self; + return Ok(self); } } - let d = DirInfo::new(path, params, depth, ctrl_c); + let d = DirInfo::new(path, params, depth, span, signals)?; self.size += d.size; self.blocks += d.blocks; self.dirs.push(d); - self + Ok(self) } fn add_file(mut self, f: impl Into, params: &DirBuilder) -> Self { diff --git a/crates/nu-command/src/platform/input/list.rs b/crates/nu-command/src/platform/input/list.rs index 44b21a8da4..37f16e75d7 100644 --- a/crates/nu-command/src/platform/input/list.rs +++ b/crates/nu-command/src/platform/input/list.rs @@ -79,6 +79,7 @@ impl Command for InputList { let fuzzy = call.has_flag(engine_state, stack, "fuzzy")?; let index = call.has_flag(engine_state, stack, "index")?; let display_path: Option = call.get_flag(engine_state, stack, "display")?; + let config = stack.get_config(engine_state); let options: Vec = match input { PipelineData::Value(Value::Range { .. }, ..) @@ -89,9 +90,9 @@ impl Command for InputList { let display_value = if let Some(ref cellpath) = display_path { val.clone() .follow_cell_path(&cellpath.members, false)? - .to_expanded_string(", ", engine_state.get_config()) + .to_expanded_string(", ", &config) } else { - val.to_expanded_string(", ", engine_state.get_config()) + val.to_expanded_string(", ", &config) }; Ok(Options { name: display_value, diff --git a/crates/nu-command/src/platform/is_terminal.rs b/crates/nu-command/src/platform/is_terminal.rs index c67329e839..2195f3ff8a 100644 --- a/crates/nu-command/src/platform/is_terminal.rs +++ b/crates/nu-command/src/platform/is_terminal.rs @@ -58,7 +58,7 @@ impl Command for IsTerminal { _ => { return Err(ShellError::IncompatibleParametersSingle { msg: "Only one stream may be checked".into(), - span: Span::merge_many(call.arguments.iter().map(|arg| arg.span())), + span: call.arguments_span(), }); } }; diff --git a/crates/nu-command/src/platform/kill.rs b/crates/nu-command/src/platform/kill.rs index 2e47ee8c78..1cf6f15f01 100644 --- a/crates/nu-command/src/platform/kill.rs +++ b/crates/nu-command/src/platform/kill.rs @@ -84,27 +84,26 @@ impl Command for Kill { { return Err(ShellError::IncompatibleParameters { left_message: "force".to_string(), - left_span: call - .get_named_arg("force") - .ok_or_else(|| ShellError::GenericError { + left_span: call.get_flag_span(stack, "force").ok_or_else(|| { + ShellError::GenericError { error: "Flag error".into(), msg: "flag force not found".into(), span: Some(call.head), help: None, inner: vec![], - })? - .span, + } + })?, right_message: "signal".to_string(), right_span: Span::merge( - call.get_named_arg("signal") - .ok_or_else(|| ShellError::GenericError { + call.get_flag_span(stack, "signal").ok_or_else(|| { + ShellError::GenericError { error: "Flag error".into(), msg: "flag signal not found".into(), span: Some(call.head), help: None, inner: vec![], - })? - .span, + } + })?, signal_span, ), }); diff --git a/crates/nu-command/src/platform/sleep.rs b/crates/nu-command/src/platform/sleep.rs index ddf429509c..4d5f6ec827 100644 --- a/crates/nu-command/src/platform/sleep.rs +++ b/crates/nu-command/src/platform/sleep.rs @@ -56,12 +56,7 @@ impl Command for Sleep { break; } thread::sleep(CTRL_C_CHECK_INTERVAL.min(time_until_deadline)); - // exit early if Ctrl+C was pressed - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - return Err(ShellError::InterruptedByUser { - span: Some(call.head), - }); - } + engine_state.signals().check(call.head)?; } Ok(Value::nothing(call.head).into_pipeline_data()) diff --git a/crates/nu-command/src/random/dice.rs b/crates/nu-command/src/random/dice.rs index 2fb659ad1e..5e3a1b98b6 100644 --- a/crates/nu-command/src/random/dice.rs +++ b/crates/nu-command/src/random/dice.rs @@ -78,7 +78,7 @@ fn dice( Value::int(thread_rng.gen_range(1..sides + 1) as i64, span) }); - Ok(ListStream::new(iter, span, engine_state.ctrlc.clone()).into()) + Ok(ListStream::new(iter, span, engine_state.signals().clone()).into()) } #[cfg(test)] diff --git a/crates/nu-command/src/stor/create.rs b/crates/nu-command/src/stor/create.rs index 630718489b..1e1219889d 100644 --- a/crates/nu-command/src/stor/create.rs +++ b/crates/nu-command/src/stor/create.rs @@ -54,7 +54,10 @@ impl Command for StorCreate { let span = call.head; let table_name: Option = call.get_flag(engine_state, stack, "table-name")?; let columns: Option = call.get_flag(engine_state, stack, "columns")?; - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + engine_state.signals().clone(), + )); process(table_name, span, &db, columns)?; // dbg!(db.clone()); @@ -141,6 +144,8 @@ fn process( #[cfg(test)] mod test { + use nu_protocol::Signals; + use super::*; #[test] @@ -154,7 +159,10 @@ mod test { fn test_process_with_valid_parameters() { let table_name = Some("test_table".to_string()); let span = Span::unknown(); - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let mut columns = Record::new(); columns.insert( "int_column".to_string(), @@ -170,7 +178,10 @@ mod test { fn test_process_with_missing_table_name() { let table_name = None; let span = Span::unknown(); - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let mut columns = Record::new(); columns.insert( "int_column".to_string(), @@ -190,7 +201,10 @@ mod test { fn test_process_with_missing_columns() { let table_name = Some("test_table".to_string()); let span = Span::unknown(); - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let result = process(table_name, span, &db, None); @@ -205,7 +219,10 @@ mod test { fn test_process_with_unsupported_column_data_type() { let table_name = Some("test_table".to_string()); let span = Span::unknown(); - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let mut columns = Record::new(); let column_datatype = "bogus_data_type".to_string(); columns.insert( diff --git a/crates/nu-command/src/stor/delete.rs b/crates/nu-command/src/stor/delete.rs index 4de4874140..676e0490b0 100644 --- a/crates/nu-command/src/stor/delete.rs +++ b/crates/nu-command/src/stor/delete.rs @@ -1,5 +1,6 @@ use crate::database::{SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; #[derive(Clone)] pub struct StorDelete; @@ -82,7 +83,10 @@ impl Command for StorDelete { } // Open the in-mem database - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); if let Some(new_table_name) = table_name_opt { if let Ok(conn) = db.open_connection() { diff --git a/crates/nu-command/src/stor/export.rs b/crates/nu-command/src/stor/export.rs index 95c5ee9f35..f8255eec49 100644 --- a/crates/nu-command/src/stor/export.rs +++ b/crates/nu-command/src/stor/export.rs @@ -1,5 +1,6 @@ use crate::database::{SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; #[derive(Clone)] pub struct StorExport; @@ -58,7 +59,10 @@ impl Command for StorExport { }; // Open the in-mem database - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); if let Ok(conn) = db.open_connection() { // This uses vacuum. I'm not really sure if this is the best way to do this. diff --git a/crates/nu-command/src/stor/import.rs b/crates/nu-command/src/stor/import.rs index 682694e8bb..20b5b64f2d 100644 --- a/crates/nu-command/src/stor/import.rs +++ b/crates/nu-command/src/stor/import.rs @@ -1,5 +1,6 @@ use crate::database::{SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; #[derive(Clone)] pub struct StorImport; @@ -58,7 +59,10 @@ impl Command for StorImport { }; // Open the in-mem database - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); if let Ok(mut conn) = db.open_connection() { db.restore_database_from_file(&mut conn, file_name) diff --git a/crates/nu-command/src/stor/insert.rs b/crates/nu-command/src/stor/insert.rs index 4b9677941b..0e5c9ec6af 100644 --- a/crates/nu-command/src/stor/insert.rs +++ b/crates/nu-command/src/stor/insert.rs @@ -1,5 +1,6 @@ use crate::database::{values_to_sql, SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; use rusqlite::params_from_iter; #[derive(Clone)] @@ -64,8 +65,11 @@ impl Command for StorInsert { let span = call.head; let table_name: Option = call.get_flag(engine_state, stack, "table-name")?; let data_record: Option = call.get_flag(engine_state, stack, "data-record")?; - // let config = engine_state.get_config(); - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + // let config = stack.get_config(engine_state); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); // Check if the record is being passed as input or using the data record parameter let columns = handle(span, data_record, input)?; @@ -198,7 +202,10 @@ mod test { #[test] fn test_process_with_simple_parameters() { - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let create_stmt = "CREATE TABLE test_process_with_simple_parameters ( int_column INTEGER, real_column REAL, @@ -237,7 +244,10 @@ mod test { #[test] fn test_process_string_with_space() { - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let create_stmt = "CREATE TABLE test_process_string_with_space ( str_column VARCHAR(255) )"; @@ -262,7 +272,10 @@ mod test { #[test] fn test_no_errors_when_string_too_long() { - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let create_stmt = "CREATE TABLE test_errors_when_string_too_long ( str_column VARCHAR(8) )"; @@ -287,7 +300,10 @@ mod test { #[test] fn test_no_errors_when_param_is_wrong_type() { - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let create_stmt = "CREATE TABLE test_errors_when_param_is_wrong_type ( int_column INT )"; @@ -312,7 +328,10 @@ mod test { #[test] fn test_errors_when_column_doesnt_exist() { - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let create_stmt = "CREATE TABLE test_errors_when_column_doesnt_exist ( int_column INT )"; @@ -337,7 +356,10 @@ mod test { #[test] fn test_errors_when_table_doesnt_exist() { - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); let table_name = Some("test_errors_when_table_doesnt_exist".to_string()); let span = Span::unknown(); diff --git a/crates/nu-command/src/stor/open.rs b/crates/nu-command/src/stor/open.rs index c7f6f9f746..ba0f17c2af 100644 --- a/crates/nu-command/src/stor/open.rs +++ b/crates/nu-command/src/stor/open.rs @@ -1,5 +1,6 @@ use crate::database::{SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; #[derive(Clone)] pub struct StorOpen; @@ -54,7 +55,10 @@ impl Command for StorOpen { // It returns the output of `select * from my_table_name` // Just create an empty database with MEMORY_DB and nothing else - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); // dbg!(db.clone()); Ok(db.into_value(call.head).into_pipeline_data()) diff --git a/crates/nu-command/src/stor/reset.rs b/crates/nu-command/src/stor/reset.rs index d4489fb702..ba9e2a9681 100644 --- a/crates/nu-command/src/stor/reset.rs +++ b/crates/nu-command/src/stor/reset.rs @@ -1,5 +1,6 @@ use crate::database::{SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; #[derive(Clone)] pub struct StorReset; @@ -42,7 +43,10 @@ impl Command for StorReset { let span = call.head; // Open the in-mem database - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); if let Ok(conn) = db.open_connection() { db.drop_all_tables(&conn) diff --git a/crates/nu-command/src/stor/update.rs b/crates/nu-command/src/stor/update.rs index d731207a3f..18cf6f9f0f 100644 --- a/crates/nu-command/src/stor/update.rs +++ b/crates/nu-command/src/stor/update.rs @@ -1,5 +1,6 @@ use crate::database::{SQLiteDatabase, MEMORY_DB}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; #[derive(Clone)] pub struct StorUpdate; @@ -79,7 +80,10 @@ impl Command for StorUpdate { call.get_flag(engine_state, stack, "where-clause")?; // Open the in-mem database - let db = Box::new(SQLiteDatabase::new(std::path::Path::new(MEMORY_DB), None)); + let db = Box::new(SQLiteDatabase::new( + std::path::Path::new(MEMORY_DB), + Signals::empty(), + )); // Check if the record is being passed as input or using the update record parameter let columns = handle(span, update_record, input)?; diff --git a/crates/nu-command/src/strings/char_.rs b/crates/nu-command/src/strings/char_.rs index 6834134b26..7737210254 100644 --- a/crates/nu-command/src/strings/char_.rs +++ b/crates/nu-command/src/strings/char_.rs @@ -1,8 +1,8 @@ use indexmap::{indexmap, IndexMap}; use nu_engine::command_prelude::*; +use nu_protocol::Signals; use once_cell::sync::Lazy; -use std::sync::{atomic::AtomicBool, Arc}; // Character used to separate directories in a Path Environment variable on windows is ";" #[cfg(target_family = "windows")] @@ -229,11 +229,13 @@ impl Command for Char { let list = call.has_flag_const(working_set, "list")?; let integer = call.has_flag_const(working_set, "integer")?; let unicode = call.has_flag_const(working_set, "unicode")?; - let ctrlc = working_set.permanent().ctrlc.clone(); // handle -l flag if list { - return Ok(generate_character_list(ctrlc, call.head)); + return Ok(generate_character_list( + working_set.permanent().signals().clone(), + call.head, + )); } // handle -i flag @@ -264,11 +266,13 @@ impl Command for Char { let list = call.has_flag(engine_state, stack, "list")?; let integer = call.has_flag(engine_state, stack, "integer")?; let unicode = call.has_flag(engine_state, stack, "unicode")?; - let ctrlc = engine_state.ctrlc.clone(); // handle -l flag if list { - return Ok(generate_character_list(ctrlc, call_span)); + return Ok(generate_character_list( + engine_state.signals().clone(), + call_span, + )); } // handle -i flag @@ -289,7 +293,7 @@ impl Command for Char { } } -fn generate_character_list(ctrlc: Option>, call_span: Span) -> PipelineData { +fn generate_character_list(signals: Signals, call_span: Span) -> PipelineData { CHAR_MAP .iter() .map(move |(name, s)| { @@ -308,7 +312,7 @@ fn generate_character_list(ctrlc: Option>, call_span: Span) -> P Value::record(record, call_span) }) - .into_pipeline_data(call_span, ctrlc) + .into_pipeline_data(call_span, signals) } fn handle_integer_flag( diff --git a/crates/nu-command/src/strings/detect_columns.rs b/crates/nu-command/src/strings/detect_columns.rs index 62a2d8bbcc..37a7cfe303 100644 --- a/crates/nu-command/src/strings/detect_columns.rs +++ b/crates/nu-command/src/strings/detect_columns.rs @@ -1,7 +1,7 @@ use itertools::Itertools; use nu_engine::command_prelude::*; -use nu_protocol::Range; -use std::{io::Cursor, iter::Peekable, str::CharIndices}; +use nu_protocol::{Config, Range}; +use std::{io::Cursor, iter::Peekable, str::CharIndices, sync::Arc}; type Input<'t> = Peekable>; @@ -110,11 +110,13 @@ none 8150224 4 8150220 1% /mnt/c' | detect columns --gue let num_rows_to_skip: Option = call.get_flag(engine_state, stack, "skip")?; let noheader = call.has_flag(engine_state, stack, "no-headers")?; let range: Option = call.get_flag(engine_state, stack, "combine-columns")?; + let config = stack.get_config(engine_state); let args = Arguments { noheader, num_rows_to_skip, range, + config, }; if call.has_flag(engine_state, stack, "guess")? { @@ -133,11 +135,13 @@ none 8150224 4 8150220 1% /mnt/c' | detect columns --gue let num_rows_to_skip: Option = call.get_flag_const(working_set, "skip")?; let noheader = call.has_flag_const(working_set, "no-headers")?; let range: Option = call.get_flag_const(working_set, "combine-columns")?; + let config = working_set.get_config().clone(); let args = Arguments { noheader, num_rows_to_skip, range, + config, }; if call.has_flag_const(working_set, "guess")? { @@ -152,6 +156,7 @@ struct Arguments { num_rows_to_skip: Option, noheader: bool, range: Option, + config: Arc, } fn guess_width( @@ -163,7 +168,7 @@ fn guess_width( use super::guess_width::GuessWidth; let input_span = input.span().unwrap_or(call.head); - let mut input = input.collect_string("", engine_state.get_config())?; + let mut input = input.collect_string("", &args.config)?; if let Some(rows) = args.num_rows_to_skip { input = input.lines().skip(rows).map(|x| x.to_string()).join("\n"); } @@ -199,7 +204,7 @@ fn guess_width( Err(e) => Value::error(e, input_span), } }) - .into_pipeline_data(input_span, engine_state.ctrlc.clone())) + .into_pipeline_data(input_span, engine_state.signals().clone())) } else { let length = result[0].len(); let columns: Vec = (0..length).map(|n| format!("column{n}")).collect(); @@ -224,7 +229,7 @@ fn guess_width( Err(e) => Value::error(e, input_span), } }) - .into_pipeline_data(input_span, engine_state.ctrlc.clone())) + .into_pipeline_data(input_span, engine_state.signals().clone())) } } @@ -235,9 +240,7 @@ fn detect_columns( args: Arguments, ) -> Result { let name_span = call.head; - let ctrlc = engine_state.ctrlc.clone(); - let config = engine_state.get_config(); - let input = input.collect_string("", config)?; + let input = input.collect_string("", &args.config)?; let input: Vec<_> = input .lines() @@ -316,7 +319,7 @@ fn detect_columns( None => Value::record(record, name_span), } }) - .into_pipeline_data(call.head, ctrlc)) + .into_pipeline_data(call.head, engine_state.signals().clone())) } else { Ok(PipelineData::empty()) } diff --git a/crates/nu-command/src/strings/encode_decode/base64.rs b/crates/nu-command/src/strings/encode_decode/base64.rs index 8050193212..dd9289a141 100644 --- a/crates/nu-command/src/strings/encode_decode/base64.rs +++ b/crates/nu-command/src/strings/encode_decode/base64.rs @@ -8,8 +8,8 @@ use base64::{ }; use nu_cmd_base::input_handler::{operate as general_operate, CmdArgument}; use nu_protocol::{ - ast::{Call, CellPath}, - engine::EngineState, + ast::CellPath, + engine::{Call, EngineState}, PipelineData, ShellError, Span, Spanned, Value, }; @@ -75,7 +75,7 @@ pub fn operate( cell_paths, }; - general_operate(action, args, input, call.head, engine_state.ctrlc.clone()) + general_operate(action, args, input, call.head, engine_state.signals()) } fn action( diff --git a/crates/nu-command/src/strings/format/date.rs b/crates/nu-command/src/strings/format/date.rs index 1aaf1fb851..dd005b9216 100644 --- a/crates/nu-command/src/strings/format/date.rs +++ b/crates/nu-command/src/strings/format/date.rs @@ -127,7 +127,7 @@ fn run( Some(format) => format_helper(value, format.item.as_str(), format.span, head), None => format_helper_rfc2822(value, head), }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/format/duration.rs b/crates/nu-command/src/strings/format/duration.rs index 281542f49a..fbbe192048 100644 --- a/crates/nu-command/src/strings/format/duration.rs +++ b/crates/nu-command/src/strings/format/duration.rs @@ -81,7 +81,7 @@ impl Command for FormatDuration { arg, input, call.head, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -108,7 +108,7 @@ impl Command for FormatDuration { arg, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/format/filesize.rs b/crates/nu-command/src/strings/format/filesize.rs index ebd43d90b1..63865ac2ac 100644 --- a/crates/nu-command/src/strings/format/filesize.rs +++ b/crates/nu-command/src/strings/format/filesize.rs @@ -76,7 +76,7 @@ impl Command for FormatFilesize { arg, input, call.head, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } @@ -101,7 +101,7 @@ impl Command for FormatFilesize { arg, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/mod.rs b/crates/nu-command/src/strings/mod.rs index d1ebf540e5..8b5af2dec4 100644 --- a/crates/nu-command/src/strings/mod.rs +++ b/crates/nu-command/src/strings/mod.rs @@ -17,8 +17,7 @@ pub use str_::*; use nu_engine::CallExt; use nu_protocol::{ - ast::Call, - engine::{EngineState, Stack, StateWorkingSet}, + engine::{Call, EngineState, Stack, StateWorkingSet}, ShellError, }; diff --git a/crates/nu-command/src/strings/parse.rs b/crates/nu-command/src/strings/parse.rs index 4318b3da8b..86b27aecbc 100644 --- a/crates/nu-command/src/strings/parse.rs +++ b/crates/nu-command/src/strings/parse.rs @@ -1,10 +1,7 @@ use fancy_regex::{Captures, Regex}; use nu_engine::command_prelude::*; -use nu_protocol::{engine::StateWorkingSet, ListStream}; -use std::{ - collections::VecDeque, - sync::{atomic::AtomicBool, Arc}, -}; +use nu_protocol::{engine::StateWorkingSet, ListStream, Signals}; +use std::collections::VecDeque; #[derive(Clone)] pub struct Parse; @@ -15,13 +12,17 @@ impl Command for Parse { } fn usage(&self) -> &str { - "Parse columns from string data using a simple pattern." + "Parse columns from string data using a simple pattern or a supplied regular expression." } fn search_terms(&self) -> Vec<&str> { vec!["pattern", "match", "regex", "str extract"] } + fn extra_usage(&self) -> &str { + "The parse command always uses regular expressions even when you use a simple pattern. If a simple pattern is supplied, parse will transform that pattern into a regular expression." + } + fn signature(&self) -> nu_protocol::Signature { Signature::build("parse") .required("pattern", SyntaxShape::String, "The pattern to match.") @@ -35,21 +36,24 @@ impl Command for Parse { } fn examples(&self) -> Vec { - let result = Value::test_list(vec![Value::test_record(record! { - "foo" => Value::test_string("hi"), - "bar" => Value::test_string("there"), - })]); - vec![ Example { description: "Parse a string into two named columns", example: "\"hi there\" | parse \"{foo} {bar}\"", - result: Some(result.clone()), + result: Some(Value::test_list( + vec![Value::test_record(record! { + "foo" => Value::test_string("hi"), + "bar" => Value::test_string("there"), + })])), }, Example { - description: "Parse a string using regex pattern", - example: "\"hi there\" | parse --regex '(?P\\w+) (?P\\w+)'", - result: Some(result), + description: "This is how the first example is interpreted in the source code", + example: "\"hi there\" | parse --regex '(?s)\\A(?P.*?) (?P.*?)\\z'", + result: Some(Value::test_list( + vec![Value::test_record(record! { + "foo" => Value::test_string("hi"), + "bar" => Value::test_string("there"), + })])), }, Example { description: "Parse a string using fancy-regex named capture group pattern", @@ -163,8 +167,6 @@ fn operate( }) .collect::>(); - let ctrlc = engine_state.ctrlc.clone(); - match input { PipelineData::Empty => Ok(PipelineData::Empty), PipelineData::Value(value, ..) => match value { @@ -192,10 +194,10 @@ fn operate( columns, iter, span: head, - ctrlc, + signals: engine_state.signals().clone(), }; - Ok(ListStream::new(iter, head, None).into()) + Ok(ListStream::new(iter, head, Signals::empty()).into()) } value => Err(ShellError::PipelineMismatch { exp_input_type: "string".into(), @@ -220,7 +222,7 @@ fn operate( columns, iter, span: head, - ctrlc, + signals: engine_state.signals().clone(), } }) .into()), @@ -232,10 +234,10 @@ fn operate( columns, iter: lines, span: head, - ctrlc, + signals: engine_state.signals().clone(), }; - Ok(ListStream::new(iter, head, None).into()) + Ok(ListStream::new(iter, head, Signals::empty()).into()) } else { Ok(PipelineData::Empty) } @@ -302,7 +304,7 @@ struct ParseIter>> { columns: Vec, iter: I, span: Span, - ctrlc: Option>, + signals: Signals, } impl>> ParseIter { @@ -320,7 +322,7 @@ impl>> Iterator for ParseIter { fn next(&mut self) -> Option { loop { - if nu_utils::ctrl_c::was_pressed(&self.ctrlc) { + if self.signals.interrupted() { return None; } diff --git a/crates/nu-command/src/strings/split/chars.rs b/crates/nu-command/src/strings/split/chars.rs index 08e73b9830..370df262ea 100644 --- a/crates/nu-command/src/strings/split/chars.rs +++ b/crates/nu-command/src/strings/split/chars.rs @@ -124,7 +124,7 @@ fn split_chars( let span = call.head; input.map( move |x| split_chars_helper(&x, span, graphemes), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/split/column.rs b/crates/nu-command/src/strings/split/column.rs index 02eff7845f..540cfabe54 100644 --- a/crates/nu-command/src/strings/split/column.rs +++ b/crates/nu-command/src/strings/split/column.rs @@ -170,7 +170,7 @@ fn split_column( input.flat_map( move |x| split_column_helper(&x, ®ex, &args.rest, args.collapse_empty, name_span), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/split/list.rs b/crates/nu-command/src/strings/split/list.rs index d52bb5401a..eb874841a2 100644 --- a/crates/nu-command/src/strings/split/list.rs +++ b/crates/nu-command/src/strings/split/list.rs @@ -215,9 +215,7 @@ fn split_list( let matcher = Matcher::new(has_regex, separator)?; for val in input { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - break; - } + engine_state.signals().check(call.head)?; if matcher.compare(&val)? { if !temp_list.is_empty() { diff --git a/crates/nu-command/src/strings/split/row.rs b/crates/nu-command/src/strings/split/row.rs index 8bc0003cb6..1f427a06e0 100644 --- a/crates/nu-command/src/strings/split/row.rs +++ b/crates/nu-command/src/strings/split/row.rs @@ -170,7 +170,7 @@ fn split_row( })?; input.flat_map( move |x| split_row_helper(&x, ®ex, args.max_split, name_span), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/split/words.rs b/crates/nu-command/src/strings/split/words.rs index 0dd5dc9383..6cb5562a70 100644 --- a/crates/nu-command/src/strings/split/words.rs +++ b/crates/nu-command/src/strings/split/words.rs @@ -177,7 +177,7 @@ fn split_words( input.map( move |x| split_words_helper(&x, args.word_length, span, args.graphemes), - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/str_/case/capitalize.rs b/crates/nu-command/src/strings/str_/case/capitalize.rs index 20f334976c..862ca127c2 100644 --- a/crates/nu-command/src/strings/str_/case/capitalize.rs +++ b/crates/nu-command/src/strings/str_/case/capitalize.rs @@ -108,7 +108,7 @@ fn operate( ret } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/str_/case/downcase.rs b/crates/nu-command/src/strings/str_/case/downcase.rs index 316050d501..0493a663aa 100644 --- a/crates/nu-command/src/strings/str_/case/downcase.rs +++ b/crates/nu-command/src/strings/str_/case/downcase.rs @@ -116,7 +116,7 @@ fn operate( ret } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/str_/case/mod.rs b/crates/nu-command/src/strings/str_/case/mod.rs index 32b50c4bc6..3390ff097e 100644 --- a/crates/nu-command/src/strings/str_/case/mod.rs +++ b/crates/nu-command/src/strings/str_/case/mod.rs @@ -38,7 +38,7 @@ where case_operation, cell_paths, }; - general_operate(action, args, input, call.head, engine_state.ctrlc.clone()) + general_operate(action, args, input, call.head, engine_state.signals()) } fn action(input: &Value, args: &Arguments, head: Span) -> Value diff --git a/crates/nu-command/src/strings/str_/case/upcase.rs b/crates/nu-command/src/strings/str_/case/upcase.rs index 9e55f25f64..557239ec91 100644 --- a/crates/nu-command/src/strings/str_/case/upcase.rs +++ b/crates/nu-command/src/strings/str_/case/upcase.rs @@ -93,7 +93,7 @@ fn operate( ret } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/str_/contains.rs b/crates/nu-command/src/strings/str_/contains.rs index ff9cb85fcb..27d8a8e313 100644 --- a/crates/nu-command/src/strings/str_/contains.rs +++ b/crates/nu-command/src/strings/str_/contains.rs @@ -68,7 +68,7 @@ impl Command for SubCommand { cell_paths, case_insensitive: call.has_flag(engine_state, stack, "ignore-case")?, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -103,7 +103,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/deunicode.rs b/crates/nu-command/src/strings/str_/deunicode.rs index 0b70cf2003..ef732571f9 100644 --- a/crates/nu-command/src/strings/str_/deunicode.rs +++ b/crates/nu-command/src/strings/str_/deunicode.rs @@ -39,7 +39,7 @@ impl Command for SubCommand { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -56,7 +56,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/distance.rs b/crates/nu-command/src/strings/str_/distance.rs index bd666e53f5..9b88f8e3e0 100644 --- a/crates/nu-command/src/strings/str_/distance.rs +++ b/crates/nu-command/src/strings/str_/distance.rs @@ -67,7 +67,7 @@ impl Command for SubCommand { compare_string, cell_paths, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -88,7 +88,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/ends_with.rs b/crates/nu-command/src/strings/str_/ends_with.rs index 40f643ea8e..d4841b74e0 100644 --- a/crates/nu-command/src/strings/str_/ends_with.rs +++ b/crates/nu-command/src/strings/str_/ends_with.rs @@ -68,7 +68,7 @@ impl Command for SubCommand { cell_paths, case_insensitive: call.has_flag(engine_state, stack, "ignore-case")?, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -89,7 +89,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/expand.rs b/crates/nu-command/src/strings/str_/expand.rs index 70fb51ec4c..b9759ef6a1 100644 --- a/crates/nu-command/src/strings/str_/expand.rs +++ b/crates/nu-command/src/strings/str_/expand.rs @@ -233,7 +233,7 @@ fn run( ), } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/str_/index_of.rs b/crates/nu-command/src/strings/str_/index_of.rs index 5b40f80d3d..a6b06ef9b7 100644 --- a/crates/nu-command/src/strings/str_/index_of.rs +++ b/crates/nu-command/src/strings/str_/index_of.rs @@ -93,7 +93,7 @@ impl Command for SubCommand { cell_paths, graphemes: grapheme_flags(engine_state, stack, call)?, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -117,7 +117,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/join.rs b/crates/nu-command/src/strings/str_/join.rs index 1d36f216d0..d297ea4cbc 100644 --- a/crates/nu-command/src/strings/str_/join.rs +++ b/crates/nu-command/src/strings/str_/join.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::Signals; use std::io::Write; @@ -88,30 +89,35 @@ fn run( let mut iter = input.into_iter(); let mut first = true; - let output = ByteStream::from_fn(span, None, ByteStreamType::String, move |buffer| { - // Write each input to the buffer - if let Some(value) = iter.next() { - // Write the separator if this is not the first - if first { - first = false; - } else if let Some(separator) = &separator { - write!(buffer, "{}", separator)?; - } - - match value { - Value::Error { error, .. } => { - return Err(*error); + let output = ByteStream::from_fn( + span, + Signals::empty(), + ByteStreamType::String, + move |buffer| { + // Write each input to the buffer + if let Some(value) = iter.next() { + // Write the separator if this is not the first + if first { + first = false; + } else if let Some(separator) = &separator { + write!(buffer, "{}", separator)?; } - // Hmm, not sure what we actually want. - // `to_expanded_string` formats dates as human readable which feels funny. - Value::Date { val, .. } => write!(buffer, "{val:?}")?, - value => write!(buffer, "{}", value.to_expanded_string("\n", &config))?, + + match value { + Value::Error { error, .. } => { + return Err(*error); + } + // Hmm, not sure what we actually want. + // `to_expanded_string` formats dates as human readable which feels funny. + Value::Date { val, .. } => write!(buffer, "{val:?}")?, + value => write!(buffer, "{}", value.to_expanded_string("\n", &config))?, + } + Ok(true) + } else { + Ok(false) } - Ok(true) - } else { - Ok(false) - } - }); + }, + ); Ok(PipelineData::ByteStream(output, metadata)) } diff --git a/crates/nu-command/src/strings/str_/length.rs b/crates/nu-command/src/strings/str_/length.rs index f456fe9467..ab0ba8db49 100644 --- a/crates/nu-command/src/strings/str_/length.rs +++ b/crates/nu-command/src/strings/str_/length.rs @@ -130,7 +130,7 @@ fn run( cell_paths: (!cell_paths.is_empty()).then_some(cell_paths), graphemes, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn action(input: &Value, arg: &Arguments, head: Span) -> Value { diff --git a/crates/nu-command/src/strings/str_/replace.rs b/crates/nu-command/src/strings/str_/replace.rs index 58e2574681..342dd0693e 100644 --- a/crates/nu-command/src/strings/str_/replace.rs +++ b/crates/nu-command/src/strings/str_/replace.rs @@ -102,7 +102,7 @@ impl Command for SubCommand { no_regex, multiline, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -134,7 +134,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/reverse.rs b/crates/nu-command/src/strings/str_/reverse.rs index cc3772db7c..9a339f7bcc 100644 --- a/crates/nu-command/src/strings/str_/reverse.rs +++ b/crates/nu-command/src/strings/str_/reverse.rs @@ -50,7 +50,7 @@ impl Command for SubCommand { ) -> Result { let cell_paths: Vec = call.rest(engine_state, stack, 0)?; let args = CellPathOnlyArgs::from(cell_paths); - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -66,7 +66,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/starts_with.rs b/crates/nu-command/src/strings/str_/starts_with.rs index aec12f6f77..bac451466c 100644 --- a/crates/nu-command/src/strings/str_/starts_with.rs +++ b/crates/nu-command/src/strings/str_/starts_with.rs @@ -70,7 +70,7 @@ impl Command for SubCommand { cell_paths, case_insensitive: call.has_flag(engine_state, stack, "ignore-case")?, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -92,7 +92,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/stats.rs b/crates/nu-command/src/strings/str_/stats.rs index 28c65afe2b..eb091ae4f6 100644 --- a/crates/nu-command/src/strings/str_/stats.rs +++ b/crates/nu-command/src/strings/str_/stats.rs @@ -122,7 +122,7 @@ fn stats( ), } }, - engine_state.ctrlc.clone(), + engine_state.signals(), ) } diff --git a/crates/nu-command/src/strings/str_/substring.rs b/crates/nu-command/src/strings/str_/substring.rs index 5ad7a967da..10464580c7 100644 --- a/crates/nu-command/src/strings/str_/substring.rs +++ b/crates/nu-command/src/strings/str_/substring.rs @@ -103,7 +103,7 @@ impl Command for SubCommand { cell_paths, graphemes: grapheme_flags(engine_state, stack, call)?, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } fn run_const( @@ -133,7 +133,7 @@ impl Command for SubCommand { args, input, call.head, - working_set.permanent().ctrlc.clone(), + working_set.permanent().signals(), ) } diff --git a/crates/nu-command/src/strings/str_/trim/trim_.rs b/crates/nu-command/src/strings/str_/trim/trim_.rs index 52c4017cda..76d886a010 100644 --- a/crates/nu-command/src/strings/str_/trim/trim_.rs +++ b/crates/nu-command/src/strings/str_/trim/trim_.rs @@ -190,7 +190,7 @@ fn run( cell_paths, mode, }; - operate(action, args, input, call.head, engine_state.ctrlc.clone()) + operate(action, args, input, call.head, engine_state.signals()) } #[derive(Debug, Copy, Clone)] diff --git a/crates/nu-command/src/system/exec.rs b/crates/nu-command/src/system/exec.rs index 35ab9428be..018017cfb2 100644 --- a/crates/nu-command/src/system/exec.rs +++ b/crates/nu-command/src/system/exec.rs @@ -39,7 +39,7 @@ On Windows based systems, Nushell will wait for the command to finish and then e let name: Spanned = call.req(engine_state, stack, 0)?; let executable = { let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; - let Some(executable) = crate::which(&name.item, &paths, &cwd) else { + let Some(executable) = crate::which(&name.item, &paths, cwd.as_ref()) else { return Err(crate::command_not_found( &name.item, call.head, diff --git a/crates/nu-command/src/system/nu_check.rs b/crates/nu-command/src/system/nu_check.rs index f9e0879c00..c0c29858d5 100644 --- a/crates/nu-command/src/system/nu_check.rs +++ b/crates/nu-command/src/system/nu_check.rs @@ -1,4 +1,4 @@ -use nu_engine::{command_prelude::*, env::get_config, find_in_dirs_env, get_dirs_var_from_call}; +use nu_engine::{command_prelude::*, find_in_dirs_env, get_dirs_var_from_call}; use nu_parser::{parse, parse_module_block, parse_module_file_or_dir, unescape_unquote_string}; use nu_protocol::engine::{FileStack, StateWorkingSet}; use std::path::Path; @@ -59,7 +59,7 @@ impl Command for NuCheck { } } PipelineData::ListStream(stream, ..) => { - let config = get_config(engine_state, stack); + let config = stack.get_config(engine_state); let list_stream = stream.into_string("\n", &config); let contents = Vec::from(list_stream); @@ -87,7 +87,7 @@ impl Command for NuCheck { &path_str.item, engine_state, stack, - get_dirs_var_from_call(call), + get_dirs_var_from_call(stack, call), ) { Ok(path) => { if let Some(path) = path { diff --git a/crates/nu-command/src/system/ps.rs b/crates/nu-command/src/system/ps.rs index c64549a44d..859575abbd 100644 --- a/crates/nu-command/src/system/ps.rs +++ b/crates/nu-command/src/system/ps.rs @@ -182,5 +182,5 @@ fn run_ps( Ok(output .into_iter() - .into_pipeline_data(span, engine_state.ctrlc.clone())) + .into_pipeline_data(span, engine_state.signals().clone())) } diff --git a/crates/nu-command/src/system/registry_query.rs b/crates/nu-command/src/system/registry_query.rs index 1e2a328356..9b68059ffa 100644 --- a/crates/nu-command/src/system/registry_query.rs +++ b/crates/nu-command/src/system/registry_query.rs @@ -106,7 +106,7 @@ fn registry_query( *registry_key_span, )) } - Ok(reg_values.into_pipeline_data(call_span, engine_state.ctrlc.clone())) + Ok(reg_values.into_pipeline_data(call_span, engine_state.signals().clone())) } else { match registry_value { Some(value) => { diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index f82c23a0e0..0b9f1950ea 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -1,9 +1,7 @@ use nu_cmd_base::hook::eval_hook; use nu_engine::{command_prelude::*, env_to_strings, get_eval_expression}; use nu_path::{dots::expand_ndots, expand_tilde}; -use nu_protocol::{ - ast::Expression, did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDest, -}; +use nu_protocol::{did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDest, Signals}; use nu_system::ForegroundChild; use nu_utils::IgnoreCaseExt; use pathdiff::diff_paths; @@ -13,7 +11,7 @@ use std::{ io::Write, path::{Path, PathBuf}, process::Stdio, - sync::{atomic::AtomicBool, Arc}, + sync::Arc, thread, }; @@ -79,7 +77,7 @@ impl Command for External { // Determine the PATH to be used and then use `which` to find it - though this has no // effect if it's an absolute path already let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; - let Some(executable) = which(expanded_name, &paths, &cwd) else { + let Some(executable) = which(expanded_name, &paths, cwd.as_ref()) else { return Err(command_not_found(&name_str, call.head, engine_state, stack)); }; executable @@ -221,22 +219,22 @@ pub fn eval_arguments_from_call( stack: &mut Stack, call: &Call, ) -> Result>, ShellError> { - let ctrlc = &engine_state.ctrlc; let cwd = engine_state.cwd(Some(stack))?; - let mut args: Vec> = vec![]; - for (expr, spread) in call.rest_iter(1) { - for arg in eval_argument(engine_state, stack, expr, spread)? { - match arg { - // Expand globs passed to run-external - Value::Glob { val, no_expand, .. } if !no_expand => args.extend( - expand_glob(&val, &cwd, expr.span, ctrlc)? - .into_iter() - .map(|s| s.into_spanned(expr.span)), - ), - other => { - args.push(OsString::from(coerce_into_string(other)?).into_spanned(expr.span)) - } - } + let eval_expression = get_eval_expression(engine_state); + let call_args = call.rest_iter_flattened(engine_state, stack, eval_expression, 1)?; + let mut args: Vec> = Vec::with_capacity(call_args.len()); + + for arg in call_args { + let span = arg.span(); + match arg { + // Expand globs passed to run-external + Value::Glob { val, no_expand, .. } if !no_expand => args.extend( + expand_glob(&val, cwd.as_std_path(), span, engine_state.signals())? + .into_iter() + .map(|s| s.into_spanned(span)), + ), + other => args + .push(OsString::from(coerce_into_string(engine_state, other)?).into_spanned(span)), } } Ok(args) @@ -244,42 +242,17 @@ pub fn eval_arguments_from_call( /// Custom `coerce_into_string()`, including globs, since those are often args to `run-external` /// as well -fn coerce_into_string(val: Value) -> Result { +fn coerce_into_string(engine_state: &EngineState, val: Value) -> Result { match val { + Value::List { .. } => Err(ShellError::CannotPassListToExternal { + arg: String::from_utf8_lossy(engine_state.get_span_contents(val.span())).into_owned(), + span: val.span(), + }), Value::Glob { val, .. } => Ok(val), _ => val.coerce_into_string(), } } -/// Evaluate an argument, returning more than one value if it was a list to be spread. -fn eval_argument( - engine_state: &EngineState, - stack: &mut Stack, - expr: &Expression, - spread: bool, -) -> Result, ShellError> { - let eval = get_eval_expression(engine_state); - match eval(engine_state, stack, expr)? { - Value::List { vals, .. } => { - if spread { - Ok(vals) - } else { - Err(ShellError::CannotPassListToExternal { - arg: String::from_utf8_lossy(engine_state.get_span_contents(expr.span)).into(), - span: expr.span, - }) - } - } - value => { - if spread { - Err(ShellError::CannotSpreadAsList { span: expr.span }) - } else { - Ok(vec![value]) - } - } - } -} - /// Performs glob expansion on `arg`. If the expansion found no matches or the pattern /// is not a valid glob, then this returns the original string as the expansion result. /// @@ -289,7 +262,7 @@ fn expand_glob( arg: &str, cwd: &Path, span: Span, - interrupt: &Option>, + signals: &Signals, ) -> Result, ShellError> { const GLOB_CHARS: &[char] = &['*', '?', '[']; @@ -307,9 +280,7 @@ fn expand_glob( let mut result: Vec = vec![]; for m in matches { - if nu_utils::ctrl_c::was_pressed(interrupt) { - return Err(ShellError::InterruptedByUser { span: Some(span) }); - } + signals.check(span)?; if let Ok(arg) = m { let arg = resolve_globbed_path_to_cwd_relative(arg, prefix.as_ref(), cwd); result.push(arg.into()); @@ -395,7 +366,7 @@ pub fn command_not_found( stack: &mut Stack, ) -> ShellError { // Run the `command_not_found` hook if there is one. - if let Some(hook) = &engine_state.config.hooks.command_not_found { + if let Some(hook) = &stack.get_config(engine_state).hooks.command_not_found { let mut stack = stack.start_capture(); // Set a special environment variable to avoid infinite loops when the // `command_not_found` hook triggers itself. @@ -609,33 +580,33 @@ mod test { Playground::setup("test_expand_glob", |dirs, play| { play.with_files(&[Stub::EmptyFile("a.txt"), Stub::EmptyFile("b.txt")]); - let cwd = dirs.test(); + let cwd = dirs.test().as_std_path(); - let actual = expand_glob("*.txt", cwd, Span::unknown(), &None).unwrap(); + let actual = expand_glob("*.txt", cwd, Span::unknown(), &Signals::empty()).unwrap(); let expected = &["a.txt", "b.txt"]; assert_eq!(actual, expected); - let actual = expand_glob("./*.txt", cwd, Span::unknown(), &None).unwrap(); + let actual = expand_glob("./*.txt", cwd, Span::unknown(), &Signals::empty()).unwrap(); assert_eq!(actual, expected); - let actual = expand_glob("'*.txt'", cwd, Span::unknown(), &None).unwrap(); + let actual = expand_glob("'*.txt'", cwd, Span::unknown(), &Signals::empty()).unwrap(); let expected = &["'*.txt'"]; assert_eq!(actual, expected); - let actual = expand_glob(".", cwd, Span::unknown(), &None).unwrap(); + let actual = expand_glob(".", cwd, Span::unknown(), &Signals::empty()).unwrap(); let expected = &["."]; assert_eq!(actual, expected); - let actual = expand_glob("./a.txt", cwd, Span::unknown(), &None).unwrap(); + let actual = expand_glob("./a.txt", cwd, Span::unknown(), &Signals::empty()).unwrap(); let expected = &["./a.txt"]; assert_eq!(actual, expected); - let actual = expand_glob("[*.txt", cwd, Span::unknown(), &None).unwrap(); + let actual = expand_glob("[*.txt", cwd, Span::unknown(), &Signals::empty()).unwrap(); let expected = &["[*.txt"]; assert_eq!(actual, expected); - let actual = expand_glob("~/foo.txt", cwd, Span::unknown(), &None).unwrap(); - let home = dirs_next::home_dir().expect("failed to get home dir"); + let actual = expand_glob("~/foo.txt", cwd, Span::unknown(), &Signals::empty()).unwrap(); + let home = dirs::home_dir().expect("failed to get home dir"); let expected: Vec = vec![home.join("foo.txt").into()]; assert_eq!(actual, expected); }) @@ -666,7 +637,7 @@ mod test { ByteStream::read( b"foo".as_slice(), Span::unknown(), - None, + Signals::empty(), ByteStreamType::Unknown, ), None, diff --git a/crates/nu-command/src/system/uname.rs b/crates/nu-command/src/system/uname.rs index e267fcaeb2..0bcb749f02 100644 --- a/crates/nu-command/src/system/uname.rs +++ b/crates/nu-command/src/system/uname.rs @@ -1,10 +1,5 @@ -use nu_protocol::record; -use nu_protocol::Value; -use nu_protocol::{ - ast::Call, - engine::{Command, EngineState, Stack}, - Category, Example, PipelineData, ShellError, Signature, Type, -}; +use nu_engine::command_prelude::*; +use nu_protocol::{record, Value}; #[derive(Clone)] pub struct UName; diff --git a/crates/nu-command/src/system/which_.rs b/crates/nu-command/src/system/which_.rs index f0cf4ece39..fd5b0beb18 100644 --- a/crates/nu-command/src/system/which_.rs +++ b/crates/nu-command/src/system/which_.rs @@ -188,7 +188,6 @@ fn which( applications: call.rest(engine_state, stack, 0)?, all: call.has_flag(engine_state, stack, "all")?, }; - let ctrlc = engine_state.ctrlc.clone(); if which_args.applications.is_empty() { return Err(ShellError::MissingParameter { @@ -214,7 +213,9 @@ fn which( output.extend(values); } - Ok(output.into_iter().into_pipeline_data(head, ctrlc)) + Ok(output + .into_iter() + .into_pipeline_data(head, engine_state.signals().clone())) } #[cfg(test)] diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index 8b39bf5649..c3e003be39 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -61,7 +61,7 @@ prints out the list properly."# let width_param: Option = call.get_flag(engine_state, stack, "width")?; let color_param: bool = call.has_flag(engine_state, stack, "color")?; let separator_param: Option = call.get_flag(engine_state, stack, "separator")?; - let config = engine_state.get_config(); + let config = &stack.get_config(engine_state); let env_str = match stack.get_env_var(engine_state, "LS_COLORS") { Some(v) => Some(env_to_string("LS_COLORS", &v, engine_state, stack)?), None => None, @@ -83,7 +83,7 @@ prints out the list properly."# separator_param, env_str, use_grid_icons, - &cwd, + cwd.as_ref(), )?) } else { Ok(PipelineData::empty()) @@ -101,7 +101,7 @@ prints out the list properly."# separator_param, env_str, use_grid_icons, - &cwd, + cwd.as_ref(), )?) } else { // dbg!(data); @@ -124,7 +124,7 @@ prints out the list properly."# separator_param, env_str, use_grid_icons, - &cwd, + cwd.as_ref(), )?) } x => { diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index f0cc90fa9f..e5fdf681f7 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -4,10 +4,10 @@ use lscolors::{LsColors, Style}; use nu_color_config::{color_from_hex, StyleComputer, TextStyle}; -use nu_engine::{command_prelude::*, env::get_config, env_to_string}; +use nu_engine::{command_prelude::*, env_to_string}; use nu_pretty_hex::HexConfig; use nu_protocol::{ - ByteStream, Config, DataSource, ListStream, PipelineMetadata, TableMode, ValueIterator, + ByteStream, Config, DataSource, ListStream, PipelineMetadata, Signals, TableMode, ValueIterator, }; use nu_table::{ common::create_nu_table_config, CollapsedTable, ExpandedTable, JustTable, NuTable, NuTableCell, @@ -19,7 +19,6 @@ use std::{ io::{IsTerminal, Read}, path::PathBuf, str::FromStr, - sync::{atomic::AtomicBool, Arc}, time::Instant, }; use terminal_size::{Height, Width}; @@ -259,7 +258,7 @@ fn parse_table_config( let flatten_separator: Option = call.get_flag(state, stack, "flatten-separator")?; let abbrivation: Option = call .get_flag(state, stack, "abbreviated")? - .or_else(|| get_config(state, stack).table_abbreviation_threshold); + .or_else(|| stack.get_config(state).table_abbreviation_threshold); let table_view = match (expand, collapse) { (false, false) => TableView::General, (_, true) => TableView::Collapsed, @@ -270,7 +269,7 @@ fn parse_table_config( }, }; let theme = - get_theme_flag(call, state, stack)?.unwrap_or_else(|| get_config(state, stack).table_mode); + get_theme_flag(call, state, stack)?.unwrap_or_else(|| stack.get_config(state).table_mode); let index = get_index_flag(call, state, stack)?; let term_width = get_width_param(width_param); @@ -345,7 +344,7 @@ fn get_theme_flag( struct CmdInput<'a> { engine_state: &'a EngineState, stack: &'a mut Stack, - call: &'a Call, + call: &'a Call<'a>, data: PipelineData, } @@ -353,7 +352,7 @@ impl<'a> CmdInput<'a> { fn new( engine_state: &'a EngineState, stack: &'a mut Stack, - call: &'a Call, + call: &'a Call<'a>, data: PipelineData, ) -> Self { Self { @@ -377,8 +376,8 @@ fn handle_table_command( ), PipelineData::ByteStream(..) => Ok(input.data), PipelineData::Value(Value::Binary { val, .. }, ..) => { - let ctrlc = input.engine_state.ctrlc.clone(); - let stream = ByteStream::read_binary(val, input.call.head, ctrlc); + let signals = input.engine_state.signals().clone(); + let stream = ByteStream::read_binary(val, input.call.head, signals); Ok(PipelineData::ByteStream( pretty_hex_stream(stream, input.call.head), None, @@ -386,8 +385,8 @@ fn handle_table_command( } // None of these two receive a StyleComputer because handle_row_stream() can produce it by itself using engine_state and stack. PipelineData::Value(Value::List { vals, .. }, metadata) => { - let ctrlc = input.engine_state.ctrlc.clone(); - let stream = ListStream::new(vals.into_iter(), span, ctrlc); + let signals = input.engine_state.signals().clone(); + let stream = ListStream::new(vals.into_iter(), span, signals); input.data = PipelineData::Empty; handle_row_stream(input, cfg, stream, metadata) @@ -410,8 +409,9 @@ fn handle_table_command( Table.run(input.engine_state, input.stack, input.call, base_pipeline) } PipelineData::Value(Value::Range { val, .. }, metadata) => { - let ctrlc = input.engine_state.ctrlc.clone(); - let stream = ListStream::new(val.into_range_iter(span, ctrlc), span, None); + let signals = input.engine_state.signals().clone(); + let stream = + ListStream::new(val.into_range_iter(span, Signals::empty()), span, signals); input.data = PipelineData::Empty; handle_row_stream(input, cfg, stream, metadata) } @@ -437,50 +437,55 @@ fn pretty_hex_stream(stream: ByteStream, span: Span) -> ByteStream { reader } else { // No stream to read from - return ByteStream::read_string("".into(), span, None); + return ByteStream::read_string("".into(), span, Signals::empty()); }; - ByteStream::from_fn(span, None, ByteStreamType::String, move |buffer| { - // Turn the buffer into a String we can write to - let mut write_buf = std::mem::take(buffer); - write_buf.clear(); - // SAFETY: we just truncated it empty - let mut write_buf = unsafe { String::from_utf8_unchecked(write_buf) }; + ByteStream::from_fn( + span, + Signals::empty(), + ByteStreamType::String, + move |buffer| { + // Turn the buffer into a String we can write to + let mut write_buf = std::mem::take(buffer); + write_buf.clear(); + // SAFETY: we just truncated it empty + let mut write_buf = unsafe { String::from_utf8_unchecked(write_buf) }; - // Write the title at the beginning - if cfg.title { - nu_pretty_hex::write_title(&mut write_buf, cfg, true).expect("format error"); - cfg.title = false; - - // Put the write_buf back into buffer - *buffer = write_buf.into_bytes(); - - Ok(true) - } else { - // Read up to `cfg.width` bytes - read_buf.clear(); - (&mut reader) - .take(cfg.width as u64) - .read_to_end(&mut read_buf) - .err_span(span)?; - - if !read_buf.is_empty() { - nu_pretty_hex::hex_write(&mut write_buf, &read_buf, cfg, Some(true)) - .expect("format error"); - write_buf.push('\n'); - - // Advance the address offset for next time - cfg.address_offset += read_buf.len(); + // Write the title at the beginning + if cfg.title { + nu_pretty_hex::write_title(&mut write_buf, cfg, true).expect("format error"); + cfg.title = false; // Put the write_buf back into buffer *buffer = write_buf.into_bytes(); Ok(true) } else { - Ok(false) + // Read up to `cfg.width` bytes + read_buf.clear(); + (&mut reader) + .take(cfg.width as u64) + .read_to_end(&mut read_buf) + .err_span(span)?; + + if !read_buf.is_empty() { + nu_pretty_hex::hex_write(&mut write_buf, &read_buf, cfg, Some(true)) + .expect("format error"); + write_buf.push('\n'); + + // Advance the address offset for next time + cfg.address_offset += read_buf.len(); + + // Put the write_buf back into buffer + *buffer = write_buf.into_bytes(); + + Ok(true) + } else { + Ok(false) + } } - } - }) + }, + ) } fn handle_record( @@ -488,11 +493,13 @@ fn handle_record( cfg: TableConfig, mut record: Record, ) -> Result { - let config = get_config(input.engine_state, input.stack); + let config = { + let state = input.engine_state; + let stack: &Stack = input.stack; + stack.get_config(state) + }; let span = input.data.span().unwrap_or(input.call.head); let styles = &StyleComputer::from_config(input.engine_state, input.stack); - let ctrlc = input.engine_state.ctrlc.clone(); - let ctrlc1 = ctrlc.clone(); if record.is_empty() { let value = @@ -517,7 +524,7 @@ fn handle_record( let opts = TableOpts::new( &config, styles, - ctrlc, + input.engine_state.signals(), span, cfg.term_width, indent, @@ -529,7 +536,7 @@ fn handle_record( let result = match result { Some(output) => maybe_strip_color(output, &config), - None => report_unsuccessful_output(ctrlc1, cfg.term_width), + None => report_unsuccessful_output(input.engine_state.signals(), cfg.term_width), }; let val = Value::string(result, span); @@ -537,8 +544,8 @@ fn handle_record( Ok(val.into_pipeline_data()) } -fn report_unsuccessful_output(ctrlc1: Option>, term_width: usize) -> String { - if nu_utils::ctrl_c::was_pressed(&ctrlc1) { +fn report_unsuccessful_output(signals: &Signals, term_width: usize) -> String { + if signals.interrupted() { "".into() } else { // assume this failed because the table was too wide @@ -599,15 +606,17 @@ fn handle_row_stream( stream: ListStream, metadata: Option, ) -> Result { - let ctrlc = input.engine_state.ctrlc.clone(); - let stream = match metadata.as_ref() { // First, `ls` sources: Some(PipelineMetadata { data_source: DataSource::Ls, .. }) => { - let config = get_config(input.engine_state, input.stack); + let config = { + let state = input.engine_state; + let stack: &Stack = input.stack; + stack.get_config(state) + }; let ls_colors_env_str = match input.stack.get_env_var(input.engine_state, "LS_COLORS") { Some(v) => Some(env_to_string( "LS_COLORS", @@ -680,11 +689,14 @@ fn handle_row_stream( // for the values it outputs. Because engine_state is passed in, config doesn't need to. input.engine_state.clone(), input.stack.clone(), - ctrlc.clone(), cfg, ); - let stream = - ByteStream::from_result_iter(paginator, input.call.head, None, ByteStreamType::String); + let stream = ByteStream::from_result_iter( + paginator, + input.call.head, + Signals::empty(), + ByteStreamType::String, + ); Ok(PipelineData::ByteStream(stream, None)) } @@ -717,7 +729,6 @@ struct PagingTableCreator { stream: ValueIterator, engine_state: EngineState, stack: Stack, - ctrlc: Option>, elements_displayed: usize, reached_end: bool, cfg: TableConfig, @@ -730,7 +741,6 @@ impl PagingTableCreator { stream: ListStream, engine_state: EngineState, stack: Stack, - ctrlc: Option>, cfg: TableConfig, ) -> Self { PagingTableCreator { @@ -738,7 +748,6 @@ impl PagingTableCreator { stream: stream.into_inner(), engine_state, stack, - ctrlc, cfg, elements_displayed: 0, reached_end: false, @@ -757,7 +766,11 @@ impl PagingTableCreator { return Ok(None); } - let cfg = get_config(&self.engine_state, &self.stack); + let cfg = { + let state = &self.engine_state; + let stack = &self.stack; + stack.get_config(state) + }; let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack); let opts = self.create_table_opts(&cfg, &style_comp); let view = TableView::Expanded { @@ -774,7 +787,11 @@ impl PagingTableCreator { return Ok(None); } - let cfg = get_config(&self.engine_state, &self.stack); + let cfg = { + let state = &self.engine_state; + let stack = &self.stack; + stack.get_config(state) + }; let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack); let opts = self.create_table_opts(&cfg, &style_comp); @@ -782,7 +799,11 @@ impl PagingTableCreator { } fn build_general(&mut self, batch: Vec) -> StringResult { - let cfg = get_config(&self.engine_state, &self.stack); + let cfg = { + let state = &self.engine_state; + let stack = &self.stack; + stack.get_config(state) + }; let style_comp = StyleComputer::from_config(&self.engine_state, &self.stack); let opts = self.create_table_opts(&cfg, &style_comp); @@ -790,14 +811,14 @@ impl PagingTableCreator { } fn create_table_opts<'a>( - &self, + &'a self, cfg: &'a Config, style_comp: &'a StyleComputer<'a>, ) -> TableOpts<'a> { TableOpts::new( cfg, style_comp, - self.ctrlc.clone(), + self.engine_state.signals(), self.head, self.cfg.term_width, (cfg.table_indent.left, cfg.table_indent.right), @@ -830,12 +851,15 @@ impl Iterator for PagingTableCreator { match self.cfg.abbreviation { Some(abbr) => { (batch, _, end) = - stream_collect_abbriviated(&mut self.stream, abbr, self.ctrlc.clone()); + stream_collect_abbriviated(&mut self.stream, abbr, self.engine_state.signals()); } None => { // Pull from stream until time runs out or we have enough items - (batch, end) = - stream_collect(&mut self.stream, STREAM_PAGE_SIZE, self.ctrlc.clone()); + (batch, end) = stream_collect( + &mut self.stream, + STREAM_PAGE_SIZE, + self.engine_state.signals(), + ); } } @@ -868,15 +892,24 @@ impl Iterator for PagingTableCreator { self.row_offset += batch_size; - let config = get_config(&self.engine_state, &self.stack); - convert_table_to_output(table, &config, &self.ctrlc, self.cfg.term_width) + let config = { + let state = &self.engine_state; + let stack = &self.stack; + stack.get_config(state) + }; + convert_table_to_output( + table, + &config, + self.engine_state.signals(), + self.cfg.term_width, + ) } } fn stream_collect( stream: impl Iterator, size: usize, - ctrlc: Option>, + signals: &Signals, ) -> (Vec, bool) { let start_time = Instant::now(); let mut end = true; @@ -896,7 +929,7 @@ fn stream_collect( break; } - if nu_utils::ctrl_c::was_pressed(&ctrlc) { + if signals.interrupted() { break; } } @@ -907,7 +940,7 @@ fn stream_collect( fn stream_collect_abbriviated( stream: impl Iterator, size: usize, - ctrlc: Option>, + signals: &Signals, ) -> (Vec, usize, bool) { let mut end = true; let mut read = 0; @@ -930,7 +963,7 @@ fn stream_collect_abbriviated( tail.push_back(item); } - if nu_utils::ctrl_c::was_pressed(&ctrlc) { + if signals.interrupted() { end = false; break; } @@ -1040,7 +1073,7 @@ fn create_empty_placeholder( engine_state: &EngineState, stack: &Stack, ) -> String { - let config = get_config(engine_state, stack); + let config = stack.get_config(engine_state); if !config.table_show_empty { return String::new(); } @@ -1062,7 +1095,7 @@ fn create_empty_placeholder( fn convert_table_to_output( table: Result, ShellError>, config: &Config, - ctrlc: &Option>, + signals: &Signals, term_width: usize, ) -> Option, ShellError>> { match table { @@ -1075,7 +1108,7 @@ fn convert_table_to_output( Some(Ok(bytes)) } Ok(None) => { - let msg = if nu_utils::ctrl_c::was_pressed(ctrlc) { + let msg = if signals.interrupted() { String::from("") } else { // assume this failed because the table was too wide diff --git a/crates/nu-command/tests/commands/cd.rs b/crates/nu-command/tests/commands/cd.rs index 87af52aa4d..f58638cc35 100644 --- a/crates/nu-command/tests/commands/cd.rs +++ b/crates/nu-command/tests/commands/cd.rs @@ -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()); }) } diff --git a/crates/nu-command/tests/commands/chunks.rs b/crates/nu-command/tests/commands/chunks.rs new file mode 100644 index 0000000000..eb0c580f15 --- /dev/null +++ b/crates/nu-command/tests/commands/chunks.rs @@ -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"); +} diff --git a/crates/nu-command/tests/commands/conversions/into/binary.rs b/crates/nu-command/tests/commands/conversions/into/binary.rs new file mode 100644 index 0000000000..c8fd24df77 --- /dev/null +++ b/crates/nu-command/tests/commands/conversions/into/binary.rs @@ -0,0 +1,13 @@ +use nu_test_support::nu; + +#[test] +fn sets_stream_from_internal_command_as_binary() { + let result = nu!("seq 1 10 | to text | into binary | describe"); + assert_eq!("binary (stream)", result.out); +} + +#[test] +fn sets_stream_from_external_command_as_binary() { + let result = nu!("^nu --testbin cococo | into binary | describe"); + assert_eq!("binary (stream)", result.out); +} diff --git a/crates/nu-command/tests/commands/conversions/into/mod.rs b/crates/nu-command/tests/commands/conversions/into/mod.rs index a7a829445a..ad10199b5c 100644 --- a/crates/nu-command/tests/commands/conversions/into/mod.rs +++ b/crates/nu-command/tests/commands/conversions/into/mod.rs @@ -1 +1,2 @@ +mod binary; mod int; diff --git a/crates/nu-command/tests/commands/database/into_sqlite.rs b/crates/nu-command/tests/commands/database/into_sqlite.rs index 595c88dc38..faa45040b3 100644 --- a/crates/nu-command/tests/commands/database/into_sqlite.rs +++ b/crates/nu-command/tests/commands/database/into_sqlite.rs @@ -465,7 +465,7 @@ fn make_sqlite_db(dirs: &Dirs, nu_table: &str) -> PathBuf { ); assert!(nucmd.status.success()); - testdb_path + testdb_path.into() } fn insert_test_rows(dirs: &Dirs, nu_table: &str, sql_query: Option<&str>, expected: Vec) { diff --git a/crates/nu-command/tests/commands/default.rs b/crates/nu-command/tests/commands/default.rs index 4fd41d7ec0..6ecd14960a 100644 --- a/crates/nu-command/tests/commands/default.rs +++ b/crates/nu-command/tests/commands/default.rs @@ -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"); +} diff --git a/crates/nu-command/tests/commands/mod.rs b/crates/nu-command/tests/commands/mod.rs index a1cb82a4b4..4c9f8e86f8 100644 --- a/crates/nu-command/tests/commands/mod.rs +++ b/crates/nu-command/tests/commands/mod.rs @@ -7,6 +7,7 @@ mod break_; mod bytes; mod cal; mod cd; +mod chunks; mod compact; mod complete; mod config_env_default; diff --git a/crates/nu-command/tests/commands/rm.rs b/crates/nu-command/tests/commands/rm.rs index b3a273ea86..e6cbf88cda 100644 --- a/crates/nu-command/tests/commands/rm.rs +++ b/crates/nu-command/tests/commands/rm.rs @@ -1,3 +1,4 @@ +use nu_path::AbsolutePath; use nu_test_support::fs::{files_exist_at, Stub::EmptyFile}; use nu_test_support::nu; use nu_test_support::playground::Playground; @@ -405,10 +406,10 @@ fn removes_file_after_cd() { } struct Cleanup<'a> { - dir_to_clean: &'a Path, + dir_to_clean: &'a AbsolutePath, } -fn set_dir_read_only(directory: &Path, read_only: bool) { +fn set_dir_read_only(directory: &AbsolutePath, read_only: bool) { let mut permissions = fs::metadata(directory).unwrap().permissions(); permissions.set_readonly(read_only); fs::set_permissions(directory, permissions).expect("failed to set directory permissions"); diff --git a/crates/nu-command/tests/commands/run_external.rs b/crates/nu-command/tests/commands/run_external.rs index 154c31b71c..17667c9bb3 100644 --- a/crates/nu-command/tests/commands/run_external.rs +++ b/crates/nu-command/tests/commands/run_external.rs @@ -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, diff --git a/crates/nu-command/tests/commands/select.rs b/crates/nu-command/tests/commands/select.rs index 741386998a..004aa16d6b 100644 --- a/crates/nu-command/tests/commands/select.rs +++ b/crates/nu-command/tests/commands/select.rs @@ -50,7 +50,7 @@ fn complex_nested_columns() { r#" {sample} | select nu."0xATYKARNU" nu.committers.name nu.releases.version - | get nu_releases_version + | get "nu.releases.version" | where $it > "0.8" | get 0 "# diff --git a/crates/nu-command/tests/commands/source_env.rs b/crates/nu-command/tests/commands/source_env.rs index 9dd0320a45..f9cc3fbc1c 100644 --- a/crates/nu-command/tests/commands/source_env.rs +++ b/crates/nu-command/tests/commands/source_env.rs @@ -1,4 +1,3 @@ -use nu_test_support::fs::AbsolutePath; use nu_test_support::fs::Stub::{FileWithContent, FileWithContentToBeTrimmed}; use nu_test_support::nu; use nu_test_support::pipeline; @@ -8,17 +7,18 @@ use nu_test_support::playground::Playground; #[test] fn sources_also_files_under_custom_lib_dirs_path() { Playground::setup("source_test_1", |dirs, nu| { - let file = AbsolutePath::new(dirs.test().join("config.toml")); - let library_path = AbsolutePath::new(dirs.test().join("lib")); + let file = dirs.test().join("config.toml"); + let library_path = dirs.test().join("lib"); - nu.with_config(&file); + nu.with_config(file); nu.with_files(&[FileWithContent( "config.toml", &format!( r#" - lib_dirs = ["{library_path}"] + lib_dirs = ["{}"] skip_welcome_message = true - "# + "#, + library_path.as_os_str().to_str().unwrap(), ), )]); diff --git a/crates/nu-command/tests/commands/table.rs b/crates/nu-command/tests/commands/table.rs index 9c4f34ebfd..272369a1c9 100644 --- a/crates/nu-command/tests/commands/table.rs +++ b/crates/nu-command/tests/commands/table.rs @@ -2889,3 +2889,21 @@ fn table_list() { let actual = nu!("table --list --theme basic"); assert_eq!(actual.out, "╭────┬────────────────╮│ 0 │ basic ││ 1 │ compact ││ 2 │ compact_double ││ 3 │ default ││ 4 │ heavy ││ 5 │ light ││ 6 │ none ││ 7 │ reinforced ││ 8 │ rounded ││ 9 │ thin ││ 10 │ with_love ││ 11 │ psql ││ 12 │ markdown ││ 13 │ dots ││ 14 │ restructured ││ 15 │ ascii_rounded ││ 16 │ basic_compact │╰────┴────────────────╯"); } + +#[test] +fn table_kv_header_on_separator_trim_algorithm() { + let actual = nu!("$env.config.table.header_on_separator = true; {key1: '111111111111111111111111111111111111111111111111111111111111'} | table --width=60 --theme basic"); + assert_eq!(actual.out, "+------+---------------------------------------------------+| key1 | 1111111111111111111111111111111111111111111111111 || | 11111111111 |+------+---------------------------------------------------+"); +} + +#[test] +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 | ... |+---+------------------+---------------+--------+-------+------------------+-----+"); +} diff --git a/crates/nu-command/tests/commands/ucp.rs b/crates/nu-command/tests/commands/ucp.rs index 728cf1dcb2..2c2b30f252 100644 --- a/crates/nu-command/tests/commands/ucp.rs +++ b/crates/nu-command/tests/commands/ucp.rs @@ -1,6 +1,6 @@ use nu_test_support::fs::file_contents; use nu_test_support::fs::{ - files_exist_at, AbsoluteFile, + files_exist_at, Stub::{EmptyFile, FileWithContent, FileWithPermission}, }; use nu_test_support::nu; @@ -55,7 +55,7 @@ fn copies_the_file_inside_directory_if_path_to_copy_is_directory() { fn copies_the_file_inside_directory_if_path_to_copy_is_directory_impl(progress: bool) { Playground::setup("ucp_test_2", |dirs, _| { - let expected_file = AbsoluteFile::new(dirs.test().join("sample.ini")); + let expected_file = dirs.test().join("sample.ini"); let progress_flag = if progress { "-p" } else { "" }; // Get the hash of the file content to check integrity after copy. @@ -64,13 +64,13 @@ fn copies_the_file_inside_directory_if_path_to_copy_is_directory_impl(progress: cwd: dirs.formats(), "cp {} ../formats/sample.ini {}", progress_flag, - expected_file.dir() + expected_file.parent().unwrap().as_os_str().to_str().unwrap(), ); assert!(dirs.test().join("sample.ini").exists()); // Check the integrity of the file. - let after_cp_hash = get_file_hash(expected_file); + let after_cp_hash = get_file_hash(expected_file.display()); assert_eq!(first_hash, after_cp_hash); }) } diff --git a/crates/nu-command/tests/commands/use_.rs b/crates/nu-command/tests/commands/use_.rs index 63b1f8391a..2895e1635f 100644 --- a/crates/nu-command/tests/commands/use_.rs +++ b/crates/nu-command/tests/commands/use_.rs @@ -1,4 +1,3 @@ -use nu_test_support::fs::AbsolutePath; use nu_test_support::fs::Stub::{FileWithContent, FileWithContentToBeTrimmed}; use nu_test_support::nu; use nu_test_support::pipeline; @@ -7,10 +6,10 @@ use nu_test_support::playground::Playground; #[test] fn use_module_file_within_block() { Playground::setup("use_test_1", |dirs, nu| { - let file = AbsolutePath::new(dirs.test().join("spam.nu")); + let file = dirs.test().join("spam.nu"); nu.with_files(&[FileWithContent( - &file.to_string(), + file.as_os_str().to_str().unwrap(), r#" export def foo [] { echo "hello world" @@ -37,10 +36,10 @@ fn use_module_file_within_block() { #[test] fn use_keeps_doc_comments() { Playground::setup("use_doc_comments", |dirs, nu| { - let file = AbsolutePath::new(dirs.test().join("spam.nu")); + let file = dirs.test().join("spam.nu"); nu.with_files(&[FileWithContent( - &file.to_string(), + file.as_os_str().to_str().unwrap(), r#" # this is my foo command export def foo [ diff --git a/crates/nu-engine/Cargo.toml b/crates/nu-engine/Cargo.toml index 3e6c3f787b..c416a2f590 100644 --- a/crates/nu-engine/Cargo.toml +++ b/crates/nu-engine/Cargo.toml @@ -15,6 +15,7 @@ nu-protocol = { path = "../nu-protocol", features = ["plugin"], version = "0.95. nu-path = { path = "../nu-path", version = "0.95.1" } nu-glob = { path = "../nu-glob", version = "0.95.1" } nu-utils = { path = "../nu-utils", version = "0.95.1" } +log = { workspace = true } [features] -plugin = [] \ No newline at end of file +plugin = [] diff --git a/crates/nu-engine/README.md b/crates/nu-engine/README.md new file mode 100644 index 0000000000..d5198cfc3f --- /dev/null +++ b/crates/nu-engine/README.md @@ -0,0 +1,9 @@ +This crate primarily drives the evaluation of expressions. + +(Some overlap with nu-protocol) + +- Provides `CallExt` + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-engine/src/call_ext.rs b/crates/nu-engine/src/call_ext.rs index daedb24a1f..d3f36215e6 100644 --- a/crates/nu-engine/src/call_ext.rs +++ b/crates/nu-engine/src/call_ext.rs @@ -1,10 +1,10 @@ use crate::eval_expression; use nu_protocol::{ - ast::Call, + ast, debugger::WithoutDebug, - engine::{EngineState, Stack, StateWorkingSet}, + engine::{self, EngineState, Stack, StateWorkingSet}, eval_const::eval_constant, - FromValue, ShellError, Value, + ir, FromValue, ShellError, Span, Value, }; pub trait CallExt { @@ -23,6 +23,9 @@ pub trait CallExt { name: &str, ) -> Result, ShellError>; + /// Efficiently get the span of a flag argument + fn get_flag_span(&self, stack: &Stack, name: &str) -> Option; + fn rest( &self, engine_state: &EngineState, @@ -56,9 +59,12 @@ pub trait CallExt { stack: &mut Stack, name: &str, ) -> Result; + + /// True if the command has any positional or rest arguments, excluding before the given index. + fn has_positional_args(&self, stack: &Stack, starting_pos: usize) -> bool; } -impl CallExt for Call { +impl CallExt for ast::Call { fn has_flag( &self, engine_state: &EngineState, @@ -104,6 +110,10 @@ impl CallExt for Call { } } + fn get_flag_span(&self, _stack: &Stack, name: &str) -> Option { + self.get_named_arg(name).map(|arg| arg.span) + } + fn rest( &self, engine_state: &EngineState, @@ -189,4 +199,205 @@ impl CallExt for Call { }) } } + + fn has_positional_args(&self, _stack: &Stack, starting_pos: usize) -> bool { + self.rest_iter(starting_pos).next().is_some() + } +} + +impl CallExt for ir::Call { + fn has_flag( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + flag_name: &str, + ) -> Result { + Ok(self + .named_iter(stack) + .find(|(name, _)| name.item == flag_name) + .is_some_and(|(_, value)| { + // Handle --flag=false + !matches!(value, Some(Value::Bool { val: false, .. })) + })) + } + + fn get_flag( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result, ShellError> { + if let Some(val) = self.get_named_arg(stack, name) { + T::from_value(val.clone()).map(Some) + } else { + Ok(None) + } + } + + fn get_flag_span(&self, stack: &Stack, name: &str) -> Option { + self.named_iter(stack) + .find_map(|(i_name, _)| (i_name.item == name).then_some(i_name.span)) + } + + fn rest( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + starting_pos: usize, + ) -> Result, ShellError> { + self.rest_iter_flattened(stack, starting_pos)? + .into_iter() + .map(T::from_value) + .collect() + } + + fn opt( + &self, + _engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result, ShellError> { + self.positional_iter(stack) + .nth(pos) + .cloned() + .map(T::from_value) + .transpose() + } + + fn opt_const( + &self, + _working_set: &StateWorkingSet, + _pos: usize, + ) -> Result, ShellError> { + Err(ShellError::IrEvalError { + msg: "const evaluation is not yet implemented on ir::Call".into(), + span: Some(self.head), + }) + } + + fn req( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result { + if let Some(val) = self.opt(engine_state, stack, pos)? { + Ok(val) + } else if self.positional_len(stack) == 0 { + Err(ShellError::AccessEmptyContent { span: self.head }) + } else { + Err(ShellError::AccessBeyondEnd { + max_idx: self.positional_len(stack) - 1, + span: self.head, + }) + } + } + + fn req_parser_info( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result { + // FIXME: this depends on the AST evaluator. We can fix this by making the parser info an + // enum rather than using expressions. It's not clear that evaluation of this is ever really + // needed. + if let Some(expr) = self.get_parser_info(stack, name) { + let expr = expr.clone(); + let stack = &mut stack.use_call_arg_out_dest(); + let result = eval_expression::(engine_state, stack, &expr)?; + FromValue::from_value(result) + } else { + Err(ShellError::CantFindColumn { + col_name: name.into(), + span: None, + src_span: self.head, + }) + } + } + + fn has_positional_args(&self, stack: &Stack, starting_pos: usize) -> bool { + self.rest_iter(stack, starting_pos).next().is_some() + } +} + +macro_rules! proxy { + ($self:ident . $method:ident ($($param:expr),*)) => (match &$self.inner { + engine::CallImpl::AstRef(call) => call.$method($($param),*), + engine::CallImpl::AstBox(call) => call.$method($($param),*), + engine::CallImpl::IrRef(call) => call.$method($($param),*), + engine::CallImpl::IrBox(call) => call.$method($($param),*), + }) +} + +impl CallExt for engine::Call<'_> { + fn has_flag( + &self, + engine_state: &EngineState, + stack: &mut Stack, + flag_name: &str, + ) -> Result { + proxy!(self.has_flag(engine_state, stack, flag_name)) + } + + fn get_flag( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result, ShellError> { + proxy!(self.get_flag(engine_state, stack, name)) + } + + fn get_flag_span(&self, stack: &Stack, name: &str) -> Option { + proxy!(self.get_flag_span(stack, name)) + } + + fn rest( + &self, + engine_state: &EngineState, + stack: &mut Stack, + starting_pos: usize, + ) -> Result, ShellError> { + proxy!(self.rest(engine_state, stack, starting_pos)) + } + + fn opt( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result, ShellError> { + proxy!(self.opt(engine_state, stack, pos)) + } + + fn opt_const( + &self, + working_set: &StateWorkingSet, + pos: usize, + ) -> Result, ShellError> { + proxy!(self.opt_const(working_set, pos)) + } + + fn req( + &self, + engine_state: &EngineState, + stack: &mut Stack, + pos: usize, + ) -> Result { + proxy!(self.req(engine_state, stack, pos)) + } + + fn req_parser_info( + &self, + engine_state: &EngineState, + stack: &mut Stack, + name: &str, + ) -> Result { + proxy!(self.req_parser_info(engine_state, stack, name)) + } + + fn has_positional_args(&self, stack: &Stack, starting_pos: usize) -> bool { + proxy!(self.has_positional_args(stack, starting_pos)) + } } diff --git a/crates/nu-engine/src/closure_eval.rs b/crates/nu-engine/src/closure_eval.rs index f4bc40658b..b271d90cbe 100644 --- a/crates/nu-engine/src/closure_eval.rs +++ b/crates/nu-engine/src/closure_eval.rs @@ -62,8 +62,8 @@ pub struct ClosureEval { stack: Stack, block: Arc, arg_index: usize, - env_vars: Vec, - env_hidden: HashMap>, + env_vars: Vec>, + env_hidden: Arc>>, eval: EvalBlockWithEarlyReturnFn, } diff --git a/crates/nu-engine/src/command_prelude.rs b/crates/nu-engine/src/command_prelude.rs index 5c21af27e0..e6ddb5fb91 100644 --- a/crates/nu-engine/src/command_prelude.rs +++ b/crates/nu-engine/src/command_prelude.rs @@ -1,7 +1,7 @@ pub use crate::CallExt; pub use nu_protocol::{ - ast::{Call, CellPath}, - engine::{Command, EngineState, Stack, StateWorkingSet}, + ast::CellPath, + engine::{Call, Command, EngineState, Stack, StateWorkingSet}, record, ByteStream, ByteStreamType, Category, ErrSpan, Example, IntoInterruptiblePipelineData, IntoPipelineData, IntoSpanned, PipelineData, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, diff --git a/crates/nu-engine/src/compile/builder.rs b/crates/nu-engine/src/compile/builder.rs new file mode 100644 index 0000000000..d77075cc3f --- /dev/null +++ b/crates/nu-engine/src/compile/builder.rs @@ -0,0 +1,575 @@ +use nu_protocol::{ + ir::{DataSlice, Instruction, IrAstRef, IrBlock, Literal}, + CompileError, IntoSpanned, RegId, Span, Spanned, +}; + +/// A label identifier. Only exists while building code. Replaced with the actual target. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct LabelId(pub usize); + +/// Builds [`IrBlock`]s progressively by consuming instructions and handles register allocation. +#[derive(Debug)] +pub(crate) struct BlockBuilder { + pub(crate) block_span: Option, + pub(crate) instructions: Vec, + pub(crate) spans: Vec, + /// The actual instruction index that a label refers to. While building IR, branch targets are + /// specified as indices into this array rather than the true instruction index. This makes it + /// easier to make modifications to code, as just this array needs to be changed, and it's also + /// less error prone as during `finish()` we check to make sure all of the used labels have had + /// an index actually set. + pub(crate) labels: Vec>, + pub(crate) data: Vec, + pub(crate) ast: Vec>, + pub(crate) comments: Vec, + pub(crate) register_allocation_state: Vec, + pub(crate) file_count: u32, + pub(crate) loop_stack: Vec, +} + +impl BlockBuilder { + /// Starts a new block, with the first register (`%0`) allocated as input. + pub(crate) fn new(block_span: Option) -> Self { + BlockBuilder { + block_span, + instructions: vec![], + spans: vec![], + labels: vec![], + data: vec![], + ast: vec![], + comments: vec![], + register_allocation_state: vec![true], + file_count: 0, + loop_stack: vec![], + } + } + + /// Get the next unused register for code generation. + pub(crate) fn next_register(&mut self) -> Result { + if let Some(index) = self + .register_allocation_state + .iter_mut() + .position(|is_allocated| { + if !*is_allocated { + *is_allocated = true; + true + } else { + false + } + }) + { + Ok(RegId(index as u32)) + } else if self.register_allocation_state.len() < (u32::MAX as usize - 2) { + let reg_id = RegId(self.register_allocation_state.len() as u32); + self.register_allocation_state.push(true); + Ok(reg_id) + } else { + Err(CompileError::RegisterOverflow { + block_span: self.block_span, + }) + } + } + + /// Check if a register is initialized with a value. + pub(crate) fn is_allocated(&self, reg_id: RegId) -> bool { + self.register_allocation_state + .get(reg_id.0 as usize) + .is_some_and(|state| *state) + } + + /// Mark a register as initialized. + pub(crate) fn mark_register(&mut self, reg_id: RegId) -> Result<(), CompileError> { + if let Some(is_allocated) = self.register_allocation_state.get_mut(reg_id.0 as usize) { + *is_allocated = true; + Ok(()) + } else { + Err(CompileError::RegisterOverflow { + block_span: self.block_span, + }) + } + } + + /// Mark a register as empty, so that it can be used again by something else. + #[track_caller] + pub(crate) fn free_register(&mut self, reg_id: RegId) -> Result<(), CompileError> { + let index = reg_id.0 as usize; + + if self + .register_allocation_state + .get(index) + .is_some_and(|is_allocated| *is_allocated) + { + self.register_allocation_state[index] = false; + Ok(()) + } else { + log::warn!("register {reg_id} uninitialized, builder = {self:#?}"); + Err(CompileError::RegisterUninitialized { + reg_id, + caller: std::panic::Location::caller().to_string(), + }) + } + } + + /// Define a label, which can be used by branch instructions. The target can optionally be + /// specified now. + pub(crate) fn label(&mut self, target_index: Option) -> LabelId { + let label_id = self.labels.len(); + self.labels.push(target_index); + LabelId(label_id) + } + + /// Change the target of a label. + pub(crate) fn set_label( + &mut self, + label_id: LabelId, + target_index: usize, + ) -> Result<(), CompileError> { + *self + .labels + .get_mut(label_id.0) + .ok_or(CompileError::UndefinedLabel { + label_id: label_id.0, + span: None, + })? = Some(target_index); + Ok(()) + } + + /// Insert an instruction into the block, automatically marking any registers populated by + /// the instruction, and freeing any registers consumed by the instruction. + #[track_caller] + pub(crate) fn push(&mut self, instruction: Spanned) -> Result<(), CompileError> { + // Free read registers, and mark write registers. + // + // If a register is both read and written, it should be on both sides, so that we can verify + // that the register was in the right state beforehand. + let mut allocate = |read: &[RegId], write: &[RegId]| -> Result<(), CompileError> { + for reg in read { + self.free_register(*reg)?; + } + for reg in write { + self.mark_register(*reg)?; + } + Ok(()) + }; + + let allocate_result = match &instruction.item { + Instruction::Unreachable => Ok(()), + Instruction::LoadLiteral { dst, lit } => { + allocate(&[], &[*dst]).and( + // Free any registers on the literal + match lit { + Literal::Range { + start, + step, + end, + inclusion: _, + } => allocate(&[*start, *step, *end], &[]), + Literal::Bool(_) + | Literal::Int(_) + | Literal::Float(_) + | Literal::Filesize(_) + | Literal::Duration(_) + | Literal::Binary(_) + | Literal::Block(_) + | Literal::Closure(_) + | Literal::RowCondition(_) + | Literal::List { capacity: _ } + | Literal::Record { capacity: _ } + | Literal::Filepath { + val: _, + no_expand: _, + } + | Literal::Directory { + val: _, + no_expand: _, + } + | Literal::GlobPattern { + val: _, + no_expand: _, + } + | Literal::String(_) + | Literal::RawString(_) + | Literal::CellPath(_) + | Literal::Date(_) + | Literal::Nothing => Ok(()), + }, + ) + } + Instruction::LoadValue { dst, val: _ } => allocate(&[], &[*dst]), + Instruction::Move { dst, src } => allocate(&[*src], &[*dst]), + Instruction::Clone { dst, src } => allocate(&[*src], &[*dst, *src]), + Instruction::Collect { src_dst } => allocate(&[*src_dst], &[*src_dst]), + Instruction::Span { src_dst } => allocate(&[*src_dst], &[*src_dst]), + Instruction::Drop { src } => allocate(&[*src], &[]), + Instruction::Drain { src } => allocate(&[*src], &[]), + Instruction::LoadVariable { dst, var_id: _ } => allocate(&[], &[*dst]), + Instruction::StoreVariable { var_id: _, src } => allocate(&[*src], &[]), + Instruction::LoadEnv { dst, key: _ } => allocate(&[], &[*dst]), + Instruction::LoadEnvOpt { dst, key: _ } => allocate(&[], &[*dst]), + Instruction::StoreEnv { key: _, src } => allocate(&[*src], &[]), + Instruction::PushPositional { src } => allocate(&[*src], &[]), + Instruction::AppendRest { src } => allocate(&[*src], &[]), + Instruction::PushFlag { name: _ } => Ok(()), + Instruction::PushShortFlag { short: _ } => Ok(()), + Instruction::PushNamed { name: _, src } => allocate(&[*src], &[]), + Instruction::PushShortNamed { short: _, src } => allocate(&[*src], &[]), + Instruction::PushParserInfo { name: _, info: _ } => Ok(()), + Instruction::RedirectOut { mode: _ } => Ok(()), + Instruction::RedirectErr { mode: _ } => Ok(()), + Instruction::CheckErrRedirected { src } => allocate(&[*src], &[*src]), + Instruction::OpenFile { + file_num: _, + path, + append: _, + } => allocate(&[*path], &[]), + Instruction::WriteFile { file_num: _, src } => allocate(&[*src], &[]), + Instruction::CloseFile { file_num: _ } => Ok(()), + Instruction::Call { + decl_id: _, + src_dst, + } => allocate(&[*src_dst], &[*src_dst]), + Instruction::StringAppend { src_dst, val } => allocate(&[*src_dst, *val], &[*src_dst]), + Instruction::GlobFrom { + src_dst, + no_expand: _, + } => allocate(&[*src_dst], &[*src_dst]), + Instruction::ListPush { src_dst, item } => allocate(&[*src_dst, *item], &[*src_dst]), + Instruction::ListSpread { src_dst, items } => { + allocate(&[*src_dst, *items], &[*src_dst]) + } + Instruction::RecordInsert { src_dst, key, val } => { + allocate(&[*src_dst, *key, *val], &[*src_dst]) + } + Instruction::RecordSpread { src_dst, items } => { + allocate(&[*src_dst, *items], &[*src_dst]) + } + Instruction::Not { src_dst } => allocate(&[*src_dst], &[*src_dst]), + Instruction::BinaryOp { + lhs_dst, + op: _, + rhs, + } => allocate(&[*lhs_dst, *rhs], &[*lhs_dst]), + Instruction::FollowCellPath { src_dst, path } => { + allocate(&[*src_dst, *path], &[*src_dst]) + } + Instruction::CloneCellPath { dst, src, path } => { + allocate(&[*src, *path], &[*src, *dst]) + } + Instruction::UpsertCellPath { + src_dst, + path, + new_value, + } => allocate(&[*src_dst, *path, *new_value], &[*src_dst]), + Instruction::Jump { index: _ } => Ok(()), + Instruction::BranchIf { cond, index: _ } => allocate(&[*cond], &[]), + Instruction::BranchIfEmpty { src, index: _ } => allocate(&[*src], &[*src]), + Instruction::Match { + pattern: _, + src, + index: _, + } => allocate(&[*src], &[*src]), + Instruction::CheckMatchGuard { src } => allocate(&[*src], &[*src]), + Instruction::Iterate { + dst, + stream, + end_index: _, + } => allocate(&[*stream], &[*dst, *stream]), + Instruction::OnError { index: _ } => Ok(()), + Instruction::OnErrorInto { index: _, dst } => allocate(&[], &[*dst]), + Instruction::PopErrorHandler => Ok(()), + Instruction::CheckExternalFailed { dst, src } => allocate(&[*src], &[*dst, *src]), + Instruction::ReturnEarly { src } => allocate(&[*src], &[]), + Instruction::Return { src } => allocate(&[*src], &[]), + }; + + // Add more context to the error + match allocate_result { + Ok(()) => (), + Err(CompileError::RegisterUninitialized { reg_id, caller }) => { + return Err(CompileError::RegisterUninitializedWhilePushingInstruction { + reg_id, + caller, + instruction: format!("{:?}", instruction.item), + span: instruction.span, + }); + } + Err(err) => return Err(err), + } + + self.instructions.push(instruction.item); + self.spans.push(instruction.span); + self.ast.push(None); + self.comments.push(String::new()); + Ok(()) + } + + /// Set the AST of the last instruction. Separate method because it's rarely used. + pub(crate) fn set_last_ast(&mut self, ast_ref: Option) { + *self.ast.last_mut().expect("no last instruction") = ast_ref; + } + + /// Add a comment to the last instruction. + pub(crate) fn add_comment(&mut self, comment: impl std::fmt::Display) { + add_comment( + self.comments.last_mut().expect("no last instruction"), + comment, + ) + } + + /// Load a register with a literal. + pub(crate) fn load_literal( + &mut self, + reg_id: RegId, + literal: Spanned, + ) -> Result<(), CompileError> { + self.push( + Instruction::LoadLiteral { + dst: reg_id, + lit: literal.item, + } + .into_spanned(literal.span), + )?; + Ok(()) + } + + /// Allocate a new register and load a literal into it. + pub(crate) fn literal(&mut self, literal: Spanned) -> Result { + let reg_id = self.next_register()?; + self.load_literal(reg_id, literal)?; + Ok(reg_id) + } + + /// Deallocate a register and set it to `Empty`, if it is allocated + pub(crate) fn drop_reg(&mut self, reg_id: RegId) -> Result<(), CompileError> { + if self.is_allocated(reg_id) { + self.push(Instruction::Drop { src: reg_id }.into_spanned(Span::unknown()))?; + } + Ok(()) + } + + /// Set a register to `Empty`, but mark it as in-use, e.g. for input + pub(crate) fn load_empty(&mut self, reg_id: RegId) -> Result<(), CompileError> { + self.drop_reg(reg_id)?; + self.mark_register(reg_id) + } + + /// Drain the stream in a register (fully consuming it) + pub(crate) fn drain(&mut self, src: RegId, span: Span) -> Result<(), CompileError> { + self.push(Instruction::Drain { src }.into_spanned(span)) + } + + /// Add data to the `data` array and return a [`DataSlice`] referencing it. + pub(crate) fn data(&mut self, data: impl AsRef<[u8]>) -> Result { + let data = data.as_ref(); + let start = self.data.len(); + if data.is_empty() { + Ok(DataSlice::empty()) + } else if start + data.len() < u32::MAX as usize { + let slice = DataSlice { + start: start as u32, + len: data.len() as u32, + }; + self.data.extend_from_slice(data); + Ok(slice) + } else { + Err(CompileError::DataOverflow { + block_span: self.block_span, + }) + } + } + + /// Clone a register with a `clone` instruction. + pub(crate) fn clone_reg(&mut self, src: RegId, span: Span) -> Result { + let dst = self.next_register()?; + self.push(Instruction::Clone { dst, src }.into_spanned(span))?; + Ok(dst) + } + + /// Add a `branch-if` instruction + pub(crate) fn branch_if( + &mut self, + cond: RegId, + label_id: LabelId, + span: Span, + ) -> Result<(), CompileError> { + self.push( + Instruction::BranchIf { + cond, + index: label_id.0, + } + .into_spanned(span), + ) + } + + /// Add a `branch-if-empty` instruction + pub(crate) fn branch_if_empty( + &mut self, + src: RegId, + label_id: LabelId, + span: Span, + ) -> Result<(), CompileError> { + self.push( + Instruction::BranchIfEmpty { + src, + index: label_id.0, + } + .into_spanned(span), + ) + } + + /// Add a `jump` instruction + pub(crate) fn jump(&mut self, label_id: LabelId, span: Span) -> Result<(), CompileError> { + self.push(Instruction::Jump { index: label_id.0 }.into_spanned(span)) + } + + /// The index that the next instruction [`.push()`]ed will have. + pub(crate) fn here(&self) -> usize { + self.instructions.len() + } + + /// Allocate a new file number, for redirection. + pub(crate) fn next_file_num(&mut self) -> Result { + let next = self.file_count; + self.file_count = self + .file_count + .checked_add(1) + .ok_or(CompileError::FileOverflow { + block_span: self.block_span, + })?; + Ok(next) + } + + /// Push a new loop state onto the builder. Creates new labels that must be set. + pub(crate) fn begin_loop(&mut self) -> Loop { + let loop_ = Loop { + break_label: self.label(None), + continue_label: self.label(None), + }; + self.loop_stack.push(loop_); + loop_ + } + + /// True if we are currently in a loop. + pub(crate) fn is_in_loop(&self) -> bool { + !self.loop_stack.is_empty() + } + + /// Add a loop breaking jump instruction. + pub(crate) fn push_break(&mut self, span: Span) -> Result<(), CompileError> { + let loop_ = self + .loop_stack + .last() + .ok_or_else(|| CompileError::NotInALoop { + msg: "`break` called from outside of a loop".into(), + span: Some(span), + })?; + self.jump(loop_.break_label, span) + } + + /// Add a loop continuing jump instruction. + pub(crate) fn push_continue(&mut self, span: Span) -> Result<(), CompileError> { + let loop_ = self + .loop_stack + .last() + .ok_or_else(|| CompileError::NotInALoop { + msg: "`continue` called from outside of a loop".into(), + span: Some(span), + })?; + self.jump(loop_.continue_label, span) + } + + /// Pop the loop state. Checks that the loop being ended is the same one that was expected. + pub(crate) fn end_loop(&mut self, loop_: Loop) -> Result<(), CompileError> { + let ended_loop = self + .loop_stack + .pop() + .ok_or_else(|| CompileError::NotInALoop { + msg: "end_loop() called outside of a loop".into(), + span: None, + })?; + + if ended_loop == loop_ { + Ok(()) + } else { + Err(CompileError::IncoherentLoopState { + block_span: self.block_span, + }) + } + } + + /// Mark an unreachable code path. Produces an error at runtime if executed. + pub(crate) fn unreachable(&mut self, span: Span) -> Result<(), CompileError> { + self.push(Instruction::Unreachable.into_spanned(span)) + } + + /// Consume the builder and produce the final [`IrBlock`]. + pub(crate) fn finish(mut self) -> Result { + // Add comments to label targets + for (index, label_target) in self.labels.iter().enumerate() { + if let Some(label_target) = label_target { + add_comment( + &mut self.comments[*label_target], + format_args!("label({index})"), + ); + } + } + + // Populate the actual target indices of labels into the instructions + for ((index, instruction), span) in + self.instructions.iter_mut().enumerate().zip(&self.spans) + { + if let Some(label_id) = instruction.branch_target() { + let target_index = self.labels.get(label_id).cloned().flatten().ok_or( + CompileError::UndefinedLabel { + label_id, + span: Some(*span), + }, + )?; + // Add a comment to the target index that we come from here + add_comment( + &mut self.comments[target_index], + format_args!("from({index}:)"), + ); + instruction.set_branch_target(target_index).map_err(|_| { + CompileError::SetBranchTargetOfNonBranchInstruction { + instruction: format!("{:?}", instruction), + span: *span, + } + })?; + } + } + + Ok(IrBlock { + instructions: self.instructions, + spans: self.spans, + data: self.data.into(), + ast: self.ast, + comments: self.comments.into_iter().map(|s| s.into()).collect(), + register_count: self + .register_allocation_state + .len() + .try_into() + .expect("register count overflowed in finish() despite previous checks"), + file_count: self.file_count, + }) + } +} + +/// Keeps track of the `break` and `continue` target labels for a loop. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct Loop { + pub(crate) break_label: LabelId, + pub(crate) continue_label: LabelId, +} + +/// Add a new comment to an existing one +fn add_comment(comment: &mut String, new_comment: impl std::fmt::Display) { + use std::fmt::Write; + write!( + comment, + "{}{}", + if comment.is_empty() { "" } else { ", " }, + new_comment + ) + .expect("formatting failed"); +} diff --git a/crates/nu-engine/src/compile/call.rs b/crates/nu-engine/src/compile/call.rs new file mode 100644 index 0000000000..d9f1b8e581 --- /dev/null +++ b/crates/nu-engine/src/compile/call.rs @@ -0,0 +1,270 @@ +use std::sync::Arc; + +use nu_protocol::{ + ast::{Argument, Call, Expression, ExternalArgument}, + engine::StateWorkingSet, + ir::{Instruction, IrAstRef, Literal}, + IntoSpanned, RegId, Span, Spanned, +}; + +use super::{compile_expression, keyword::*, BlockBuilder, CompileError, RedirectModes}; + +pub(crate) fn compile_call( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + let decl = working_set.get_decl(call.decl_id); + + // Check if this call has --help - if so, just redirect to `help` + if call.named_iter().any(|(name, _, _)| name.item == "help") { + return compile_help( + working_set, + builder, + decl.name().into_spanned(call.head), + io_reg, + ); + } + + // Try to figure out if this is a keyword call like `if`, and handle those specially + if decl.is_keyword() { + match decl.name() { + "if" => { + return compile_if(working_set, builder, call, redirect_modes, io_reg); + } + "match" => { + return compile_match(working_set, builder, call, redirect_modes, io_reg); + } + "const" => { + // This differs from the behavior of the const command, which adds the const value + // to the stack. Since `load-variable` also checks `engine_state` for the variable + // and will get a const value though, is it really necessary to do that? + return builder.load_empty(io_reg); + } + "alias" => { + // Alias does nothing + return builder.load_empty(io_reg); + } + "let" | "mut" => { + return compile_let(working_set, builder, call, redirect_modes, io_reg); + } + "try" => { + return compile_try(working_set, builder, call, redirect_modes, io_reg); + } + "loop" => { + return compile_loop(working_set, builder, call, redirect_modes, io_reg); + } + "while" => { + return compile_while(working_set, builder, call, redirect_modes, io_reg); + } + "for" => { + return compile_for(working_set, builder, call, redirect_modes, io_reg); + } + "break" => { + return compile_break(working_set, builder, call, redirect_modes, io_reg); + } + "continue" => { + return compile_continue(working_set, builder, call, redirect_modes, io_reg); + } + "return" => { + return compile_return(working_set, builder, call, redirect_modes, io_reg); + } + _ => (), + } + } + + // Keep AST if the decl needs it. + let requires_ast = decl.requires_ast_for_arguments(); + + // It's important that we evaluate the args first before trying to set up the argument + // state for the call. + // + // We could technically compile anything that isn't another call safely without worrying about + // the argument state, but we'd have to check all of that first and it just isn't really worth + // it. + enum CompiledArg<'a> { + Positional(RegId, Span, Option), + Named( + &'a str, + Option<&'a str>, + Option, + Span, + Option, + ), + Spread(RegId, Span, Option), + } + + let mut compiled_args = vec![]; + + for arg in &call.arguments { + let arg_reg = arg + .expr() + .map(|expr| { + let arg_reg = builder.next_register()?; + + compile_expression( + working_set, + builder, + expr, + RedirectModes::capture_out(arg.span()), + None, + arg_reg, + )?; + + Ok(arg_reg) + }) + .transpose()?; + + let ast_ref = arg + .expr() + .filter(|_| requires_ast) + .map(|expr| IrAstRef(Arc::new(expr.clone()))); + + match arg { + Argument::Positional(_) | Argument::Unknown(_) => { + compiled_args.push(CompiledArg::Positional( + arg_reg.expect("expr() None in non-Named"), + arg.span(), + ast_ref, + )) + } + Argument::Named((name, short, _)) => compiled_args.push(CompiledArg::Named( + &name.item, + short.as_ref().map(|spanned| spanned.item.as_str()), + arg_reg, + arg.span(), + ast_ref, + )), + Argument::Spread(_) => compiled_args.push(CompiledArg::Spread( + arg_reg.expect("expr() None in non-Named"), + arg.span(), + ast_ref, + )), + } + } + + // Now that the args are all compiled, set up the call state (argument stack and redirections) + for arg in compiled_args { + match arg { + CompiledArg::Positional(reg, span, ast_ref) => { + builder.push(Instruction::PushPositional { src: reg }.into_spanned(span))?; + builder.set_last_ast(ast_ref); + } + CompiledArg::Named(name, short, Some(reg), span, ast_ref) => { + if !name.is_empty() { + let name = builder.data(name)?; + builder.push(Instruction::PushNamed { name, src: reg }.into_spanned(span))?; + } else { + let short = builder.data(short.unwrap_or(""))?; + builder + .push(Instruction::PushShortNamed { short, src: reg }.into_spanned(span))?; + } + builder.set_last_ast(ast_ref); + } + CompiledArg::Named(name, short, None, span, ast_ref) => { + if !name.is_empty() { + let name = builder.data(name)?; + builder.push(Instruction::PushFlag { name }.into_spanned(span))?; + } else { + let short = builder.data(short.unwrap_or(""))?; + builder.push(Instruction::PushShortFlag { short }.into_spanned(span))?; + } + builder.set_last_ast(ast_ref); + } + CompiledArg::Spread(reg, span, ast_ref) => { + builder.push(Instruction::AppendRest { src: reg }.into_spanned(span))?; + builder.set_last_ast(ast_ref); + } + } + } + + // Add any parser info from the call + for (name, info) in &call.parser_info { + let name = builder.data(name)?; + let info = Box::new(info.clone()); + builder.push(Instruction::PushParserInfo { name, info }.into_spanned(call.head))?; + } + + if let Some(mode) = redirect_modes.out { + builder.push(mode.map(|mode| Instruction::RedirectOut { mode }))?; + } + + if let Some(mode) = redirect_modes.err { + builder.push(mode.map(|mode| Instruction::RedirectErr { mode }))?; + } + + // The state is set up, so we can do the call into io_reg + builder.push( + Instruction::Call { + decl_id: call.decl_id, + src_dst: io_reg, + } + .into_spanned(call.head), + )?; + + Ok(()) +} + +pub(crate) fn compile_help( + working_set: &StateWorkingSet<'_>, + builder: &mut BlockBuilder, + decl_name: Spanned<&str>, + io_reg: RegId, +) -> Result<(), CompileError> { + let help_command_id = + working_set + .find_decl(b"help") + .ok_or_else(|| CompileError::MissingRequiredDeclaration { + decl_name: "help".into(), + span: decl_name.span, + })?; + + let name_data = builder.data(decl_name.item)?; + let name_literal = builder.literal(decl_name.map(|_| Literal::String(name_data)))?; + + builder.push(Instruction::PushPositional { src: name_literal }.into_spanned(decl_name.span))?; + + builder.push( + Instruction::Call { + decl_id: help_command_id, + src_dst: io_reg, + } + .into_spanned(decl_name.span), + )?; + + Ok(()) +} + +pub(crate) fn compile_external_call( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + head: &Expression, + args: &[ExternalArgument], + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pass everything to run-external + let run_external_id = working_set + .find_decl(b"run-external") + .ok_or(CompileError::RunExternalNotFound { span: head.span })?; + + let mut call = Call::new(head.span); + call.decl_id = run_external_id; + + call.arguments.push(Argument::Positional(head.clone())); + + for arg in args { + match arg { + ExternalArgument::Regular(expr) => { + call.arguments.push(Argument::Positional(expr.clone())); + } + ExternalArgument::Spread(expr) => { + call.arguments.push(Argument::Spread(expr.clone())); + } + } + } + + compile_call(working_set, builder, &call, redirect_modes, io_reg) +} diff --git a/crates/nu-engine/src/compile/expression.rs b/crates/nu-engine/src/compile/expression.rs new file mode 100644 index 0000000000..38ee58ea26 --- /dev/null +++ b/crates/nu-engine/src/compile/expression.rs @@ -0,0 +1,535 @@ +use super::{ + compile_binary_op, compile_block, compile_call, compile_external_call, compile_load_env, + BlockBuilder, CompileError, RedirectModes, +}; + +use nu_protocol::{ + ast::{CellPath, Expr, Expression, ListItem, RecordItem, ValueWithUnit}, + engine::StateWorkingSet, + ir::{DataSlice, Instruction, Literal}, + IntoSpanned, RegId, Span, Value, ENV_VARIABLE_ID, +}; + +pub(crate) fn compile_expression( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + expr: &Expression, + redirect_modes: RedirectModes, + in_reg: Option, + out_reg: RegId, +) -> Result<(), CompileError> { + let drop_input = |builder: &mut BlockBuilder| { + if let Some(in_reg) = in_reg { + if in_reg != out_reg { + builder.drop_reg(in_reg)?; + } + } + Ok(()) + }; + + let lit = |builder: &mut BlockBuilder, literal: Literal| { + drop_input(builder)?; + + builder + .push( + Instruction::LoadLiteral { + dst: out_reg, + lit: literal, + } + .into_spanned(expr.span), + ) + .map(|_| ()) + }; + + let ignore = |builder: &mut BlockBuilder| { + drop_input(builder)?; + builder.load_empty(out_reg) + }; + + let unexpected = |expr_name: &str| CompileError::UnexpectedExpression { + expr_name: expr_name.into(), + span: expr.span, + }; + + let move_in_reg_to_out_reg = |builder: &mut BlockBuilder| { + // Ensure that out_reg contains the input value, because a call only uses one register + if let Some(in_reg) = in_reg { + if in_reg != out_reg { + // Have to move in_reg to out_reg so it can be used + builder.push( + Instruction::Move { + dst: out_reg, + src: in_reg, + } + .into_spanned(expr.span), + )?; + } + } else { + // Will have to initialize out_reg with Empty first + builder.load_empty(out_reg)?; + } + Ok(()) + }; + + match &expr.expr { + Expr::Bool(b) => lit(builder, Literal::Bool(*b)), + Expr::Int(i) => lit(builder, Literal::Int(*i)), + Expr::Float(f) => lit(builder, Literal::Float(*f)), + Expr::Binary(bin) => { + let data_slice = builder.data(bin)?; + lit(builder, Literal::Binary(data_slice)) + } + Expr::Range(range) => { + // Compile the subexpressions of the range + let compile_part = |builder: &mut BlockBuilder, + part_expr: Option<&Expression>| + -> Result { + let reg = builder.next_register()?; + if let Some(part_expr) = part_expr { + compile_expression( + working_set, + builder, + part_expr, + RedirectModes::capture_out(part_expr.span), + None, + reg, + )?; + } else { + builder.load_literal(reg, Literal::Nothing.into_spanned(expr.span))?; + } + Ok(reg) + }; + + drop_input(builder)?; + + let start = compile_part(builder, range.from.as_ref())?; + let step = compile_part(builder, range.next.as_ref())?; + let end = compile_part(builder, range.to.as_ref())?; + + // Assemble the range + builder.load_literal( + out_reg, + Literal::Range { + start, + step, + end, + inclusion: range.operator.inclusion, + } + .into_spanned(expr.span), + ) + } + Expr::Var(var_id) => { + drop_input(builder)?; + builder.push( + Instruction::LoadVariable { + dst: out_reg, + var_id: *var_id, + } + .into_spanned(expr.span), + )?; + Ok(()) + } + Expr::VarDecl(_) => Err(unexpected("VarDecl")), + Expr::Call(call) => { + move_in_reg_to_out_reg(builder)?; + + compile_call(working_set, builder, call, redirect_modes, out_reg) + } + Expr::ExternalCall(head, args) => { + move_in_reg_to_out_reg(builder)?; + + compile_external_call(working_set, builder, head, args, redirect_modes, out_reg) + } + Expr::Operator(_) => Err(unexpected("Operator")), + Expr::RowCondition(block_id) => lit(builder, Literal::RowCondition(*block_id)), + Expr::UnaryNot(subexpr) => { + drop_input(builder)?; + compile_expression( + working_set, + builder, + subexpr, + RedirectModes::capture_out(subexpr.span), + None, + out_reg, + )?; + builder.push(Instruction::Not { src_dst: out_reg }.into_spanned(expr.span))?; + Ok(()) + } + Expr::BinaryOp(lhs, op, rhs) => { + if let Expr::Operator(ref operator) = op.expr { + drop_input(builder)?; + compile_binary_op( + working_set, + builder, + lhs, + operator.clone().into_spanned(op.span), + rhs, + expr.span, + out_reg, + ) + } else { + Err(CompileError::UnsupportedOperatorExpression { span: op.span }) + } + } + Expr::Subexpression(block_id) => { + let block = working_set.get_block(*block_id); + compile_block(working_set, builder, block, redirect_modes, in_reg, out_reg) + } + Expr::Block(block_id) => lit(builder, Literal::Block(*block_id)), + Expr::Closure(block_id) => lit(builder, Literal::Closure(*block_id)), + Expr::MatchBlock(_) => Err(unexpected("MatchBlock")), // only for `match` keyword + Expr::List(items) => { + // Guess capacity based on items (does not consider spread as more than 1) + lit( + builder, + Literal::List { + capacity: items.len(), + }, + )?; + for item in items { + // Compile the expression of the item / spread + let reg = builder.next_register()?; + let expr = match item { + ListItem::Item(expr) | ListItem::Spread(_, expr) => expr, + }; + compile_expression( + working_set, + builder, + expr, + RedirectModes::capture_out(expr.span), + None, + reg, + )?; + + match item { + ListItem::Item(_) => { + // Add each item using list-push + builder.push( + Instruction::ListPush { + src_dst: out_reg, + item: reg, + } + .into_spanned(expr.span), + )?; + } + ListItem::Spread(spread_span, _) => { + // Spread the list using list-spread + builder.push( + Instruction::ListSpread { + src_dst: out_reg, + items: reg, + } + .into_spanned(*spread_span), + )?; + } + } + } + Ok(()) + } + Expr::Table(table) => { + lit( + builder, + Literal::List { + capacity: table.rows.len(), + }, + )?; + + // Evaluate the columns + let column_registers = table + .columns + .iter() + .map(|column| { + let reg = builder.next_register()?; + compile_expression( + working_set, + builder, + column, + RedirectModes::capture_out(column.span), + None, + reg, + )?; + Ok(reg) + }) + .collect::, CompileError>>()?; + + // Build records for each row + for row in table.rows.iter() { + let row_reg = builder.next_register()?; + builder.load_literal( + row_reg, + Literal::Record { + capacity: table.columns.len(), + } + .into_spanned(expr.span), + )?; + for (column_reg, item) in column_registers.iter().zip(row.iter()) { + let column_reg = builder.clone_reg(*column_reg, item.span)?; + let item_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + item, + RedirectModes::capture_out(item.span), + None, + item_reg, + )?; + builder.push( + Instruction::RecordInsert { + src_dst: row_reg, + key: column_reg, + val: item_reg, + } + .into_spanned(item.span), + )?; + } + builder.push( + Instruction::ListPush { + src_dst: out_reg, + item: row_reg, + } + .into_spanned(expr.span), + )?; + } + + // Free the column registers, since they aren't needed anymore + for reg in column_registers { + builder.drop_reg(reg)?; + } + + Ok(()) + } + Expr::Record(items) => { + lit( + builder, + Literal::Record { + capacity: items.len(), + }, + )?; + + for item in items { + match item { + RecordItem::Pair(key, val) => { + // Add each item using record-insert + let key_reg = builder.next_register()?; + let val_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + key, + RedirectModes::capture_out(key.span), + None, + key_reg, + )?; + compile_expression( + working_set, + builder, + val, + RedirectModes::capture_out(val.span), + None, + val_reg, + )?; + builder.push( + Instruction::RecordInsert { + src_dst: out_reg, + key: key_reg, + val: val_reg, + } + .into_spanned(expr.span), + )?; + } + RecordItem::Spread(spread_span, expr) => { + // Spread the expression using record-spread + let reg = builder.next_register()?; + compile_expression( + working_set, + builder, + expr, + RedirectModes::capture_out(expr.span), + None, + reg, + )?; + builder.push( + Instruction::RecordSpread { + src_dst: out_reg, + items: reg, + } + .into_spanned(*spread_span), + )?; + } + } + } + Ok(()) + } + Expr::Keyword(kw) => { + // keyword: just pass through expr, since commands that use it and are not being + // specially handled already are often just positional anyway + compile_expression( + working_set, + builder, + &kw.expr, + redirect_modes, + in_reg, + out_reg, + ) + } + Expr::ValueWithUnit(value_with_unit) => { + lit(builder, literal_from_value_with_unit(value_with_unit)?) + } + Expr::DateTime(dt) => lit(builder, Literal::Date(Box::new(*dt))), + Expr::Filepath(path, no_expand) => { + let val = builder.data(path)?; + lit( + builder, + Literal::Filepath { + val, + no_expand: *no_expand, + }, + ) + } + Expr::Directory(path, no_expand) => { + let val = builder.data(path)?; + lit( + builder, + Literal::Directory { + val, + no_expand: *no_expand, + }, + ) + } + Expr::GlobPattern(path, no_expand) => { + let val = builder.data(path)?; + lit( + builder, + Literal::GlobPattern { + val, + no_expand: *no_expand, + }, + ) + } + Expr::String(s) => { + let data_slice = builder.data(s)?; + lit(builder, Literal::String(data_slice)) + } + Expr::RawString(rs) => { + let data_slice = builder.data(rs)?; + lit(builder, Literal::RawString(data_slice)) + } + Expr::CellPath(path) => lit(builder, Literal::CellPath(Box::new(path.clone()))), + Expr::FullCellPath(full_cell_path) => { + if matches!(full_cell_path.head.expr, Expr::Var(ENV_VARIABLE_ID)) { + compile_load_env(builder, expr.span, &full_cell_path.tail, out_reg) + } else { + compile_expression( + working_set, + builder, + &full_cell_path.head, + RedirectModes::capture_out(expr.span), + in_reg, + out_reg, + )?; + // Only do the follow if this is actually needed + if !full_cell_path.tail.is_empty() { + let cell_path_reg = builder.literal( + Literal::CellPath(Box::new(CellPath { + members: full_cell_path.tail.clone(), + })) + .into_spanned(expr.span), + )?; + builder.push( + Instruction::FollowCellPath { + src_dst: out_reg, + path: cell_path_reg, + } + .into_spanned(expr.span), + )?; + } + Ok(()) + } + } + Expr::ImportPattern(_) => Err(unexpected("ImportPattern")), + Expr::Overlay(_) => Err(unexpected("Overlay")), + Expr::Signature(_) => ignore(builder), // no effect + Expr::StringInterpolation(exprs) | Expr::GlobInterpolation(exprs, _) => { + let mut exprs_iter = exprs.iter().peekable(); + + if exprs_iter + .peek() + .is_some_and(|e| matches!(e.expr, Expr::String(..) | Expr::RawString(..))) + { + // If the first expression is a string or raw string literal, just take it and build + // from that + compile_expression( + working_set, + builder, + exprs_iter.next().expect("peek() was Some"), + RedirectModes::capture_out(expr.span), + None, + out_reg, + )?; + } else { + // Start with an empty string + lit(builder, Literal::String(DataSlice::empty()))?; + } + + // Compile each expression and append to out_reg + for expr in exprs_iter { + let scratch_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + expr, + RedirectModes::capture_out(expr.span), + None, + scratch_reg, + )?; + builder.push( + Instruction::StringAppend { + src_dst: out_reg, + val: scratch_reg, + } + .into_spanned(expr.span), + )?; + } + + // If it's a glob interpolation, change it to a glob + if let Expr::GlobInterpolation(_, no_expand) = expr.expr { + builder.push( + Instruction::GlobFrom { + src_dst: out_reg, + no_expand, + } + .into_spanned(expr.span), + )?; + } + + Ok(()) + } + Expr::Nothing => lit(builder, Literal::Nothing), + Expr::Garbage => Err(CompileError::Garbage { span: expr.span }), + } +} + +fn literal_from_value_with_unit(value_with_unit: &ValueWithUnit) -> Result { + let Expr::Int(int_value) = value_with_unit.expr.expr else { + return Err(CompileError::UnexpectedExpression { + expr_name: format!("{:?}", value_with_unit.expr), + span: value_with_unit.expr.span, + }); + }; + + match value_with_unit + .unit + .item + .build_value(int_value, Span::unknown()) + .map_err(|err| CompileError::InvalidLiteral { + msg: err.to_string(), + span: value_with_unit.expr.span, + })? { + Value::Filesize { val, .. } => Ok(Literal::Filesize(val)), + Value::Duration { val, .. } => Ok(Literal::Duration(val)), + other => Err(CompileError::InvalidLiteral { + msg: format!("bad value returned by Unit::build_value(): {other:?}"), + span: value_with_unit.unit.span, + }), + } +} diff --git a/crates/nu-engine/src/compile/keyword.rs b/crates/nu-engine/src/compile/keyword.rs new file mode 100644 index 0000000000..12f4a54c10 --- /dev/null +++ b/crates/nu-engine/src/compile/keyword.rs @@ -0,0 +1,902 @@ +use nu_protocol::{ + ast::{Block, Call, Expr, Expression}, + engine::StateWorkingSet, + ir::Instruction, + IntoSpanned, RegId, Type, VarId, +}; + +use super::{compile_block, compile_expression, BlockBuilder, CompileError, RedirectModes}; + +/// Compile a call to `if` as a branch-if +pub(crate) fn compile_if( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %io_reg <- + // not %io_reg + // branch-if %io_reg, FALSE + // TRUE: ...... + // jump END + // FALSE: ...... OR drop %io_reg + // END: + let invalid = || CompileError::InvalidKeywordCall { + keyword: "if".into(), + span: call.head, + }; + + let condition = call.positional_nth(0).ok_or_else(invalid)?; + let true_block_arg = call.positional_nth(1).ok_or_else(invalid)?; + let else_arg = call.positional_nth(2); + + let true_block_id = true_block_arg.as_block().ok_or_else(invalid)?; + let true_block = working_set.get_block(true_block_id); + + let true_label = builder.label(None); + let false_label = builder.label(None); + let end_label = builder.label(None); + + let not_condition_reg = { + // Compile the condition first + let condition_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + condition, + RedirectModes::capture_out(condition.span), + None, + condition_reg, + )?; + + // Negate the condition - we basically only want to jump if the condition is false + builder.push( + Instruction::Not { + src_dst: condition_reg, + } + .into_spanned(call.head), + )?; + + condition_reg + }; + + // Set up a branch if the condition is false. + builder.branch_if(not_condition_reg, false_label, call.head)?; + builder.add_comment("if false"); + + // Compile the true case + builder.set_label(true_label, builder.here())?; + compile_block( + working_set, + builder, + true_block, + redirect_modes.clone(), + Some(io_reg), + io_reg, + )?; + + // Add a jump over the false case + builder.jump(end_label, else_arg.map(|e| e.span).unwrap_or(call.head))?; + builder.add_comment("end if"); + + // On the else side now, assert that io_reg is still valid + builder.set_label(false_label, builder.here())?; + builder.mark_register(io_reg)?; + + if let Some(else_arg) = else_arg { + let Expression { + expr: Expr::Keyword(else_keyword), + .. + } = else_arg + else { + return Err(invalid()); + }; + + if else_keyword.keyword.as_ref() != b"else" { + return Err(invalid()); + } + + let else_expr = &else_keyword.expr; + + match &else_expr.expr { + Expr::Block(block_id) => { + let false_block = working_set.get_block(*block_id); + compile_block( + working_set, + builder, + false_block, + redirect_modes, + Some(io_reg), + io_reg, + )?; + } + _ => { + // The else case supports bare expressions too, not only blocks + compile_expression( + working_set, + builder, + else_expr, + redirect_modes, + Some(io_reg), + io_reg, + )?; + } + } + } else { + // We don't have an else expression/block, so just set io_reg = Empty + builder.load_empty(io_reg)?; + } + + // Set the end label + builder.set_label(end_label, builder.here())?; + + Ok(()) +} + +/// Compile a call to `match` +pub(crate) fn compile_match( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %match_reg <- + // collect %match_reg + // match (pat1), %match_reg, PAT1 + // MATCH2: match (pat2), %match_reg, PAT2 + // FAIL: drop %io_reg + // drop %match_reg + // jump END + // PAT1: %guard_reg <- + // check-match-guard %guard_reg + // not %guard_reg + // branch-if %guard_reg, MATCH2 + // drop %match_reg + // <...expr...> + // jump END + // PAT2: drop %match_reg + // <...expr...> + // jump END + // END: + let invalid = || CompileError::InvalidKeywordCall { + keyword: "match".into(), + span: call.head, + }; + + let match_expr = call.positional_nth(0).ok_or_else(invalid)?; + + let match_block_arg = call.positional_nth(1).ok_or_else(invalid)?; + let match_block = match_block_arg.as_match_block().ok_or_else(invalid)?; + + let match_reg = builder.next_register()?; + + // Evaluate the match expression (patterns will be checked against this). + compile_expression( + working_set, + builder, + match_expr, + RedirectModes::capture_out(match_expr.span), + None, + match_reg, + )?; + + // Important to collect it first + builder.push(Instruction::Collect { src_dst: match_reg }.into_spanned(match_expr.span))?; + + // Generate the `match` instructions. Guards are not used at this stage. + let mut match_labels = Vec::with_capacity(match_block.len()); + let mut next_labels = Vec::with_capacity(match_block.len()); + let end_label = builder.label(None); + + for (pattern, _) in match_block { + let match_label = builder.label(None); + match_labels.push(match_label); + builder.push( + Instruction::Match { + pattern: Box::new(pattern.pattern.clone()), + src: match_reg, + index: match_label.0, + } + .into_spanned(pattern.span), + )?; + // Also add a label for the next match instruction or failure case + next_labels.push(builder.label(Some(builder.here()))); + } + + // Match fall-through to jump to the end, if no match + builder.load_empty(io_reg)?; + builder.drop_reg(match_reg)?; + builder.jump(end_label, call.head)?; + + // Generate each of the match expressions. Handle guards here, if present. + for (index, (pattern, expr)) in match_block.iter().enumerate() { + let match_label = match_labels[index]; + let next_label = next_labels[index]; + + // `io_reg` and `match_reg` are still valid at each of these branch targets + builder.mark_register(io_reg)?; + builder.mark_register(match_reg)?; + + // Set the original match instruction target here + builder.set_label(match_label, builder.here())?; + + // Handle guard, if present + if let Some(guard) = &pattern.guard { + let guard_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + guard, + RedirectModes::capture_out(guard.span), + None, + guard_reg, + )?; + builder + .push(Instruction::CheckMatchGuard { src: guard_reg }.into_spanned(guard.span))?; + builder.push(Instruction::Not { src_dst: guard_reg }.into_spanned(guard.span))?; + // Branch to the next match instruction if the branch fails to match + builder.branch_if( + guard_reg, + next_label, + // Span the branch with the next pattern, or the head if this is the end + match_block + .get(index + 1) + .map(|b| b.0.span) + .unwrap_or(call.head), + )?; + builder.add_comment("if match guard false"); + } + + // match_reg no longer needed, successful match + builder.drop_reg(match_reg)?; + + // Execute match right hand side expression + if let Some(block_id) = expr.as_block() { + let block = working_set.get_block(block_id); + compile_block( + working_set, + builder, + block, + redirect_modes.clone(), + Some(io_reg), + io_reg, + )?; + } else { + compile_expression( + working_set, + builder, + expr, + redirect_modes.clone(), + Some(io_reg), + io_reg, + )?; + } + + // Jump to the end after the match logic is done + builder.jump(end_label, call.head)?; + builder.add_comment("end match"); + } + + // Set the end destination + builder.set_label(end_label, builder.here())?; + + Ok(()) +} + +/// Compile a call to `let` or `mut` (just do store-variable) +pub(crate) fn compile_let( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %io_reg <- ...... <- %io_reg + // store-variable $var, %io_reg + let invalid = || CompileError::InvalidKeywordCall { + keyword: "let".into(), + span: call.head, + }; + + let var_decl_arg = call.positional_nth(0).ok_or_else(invalid)?; + let block_arg = call.positional_nth(1).ok_or_else(invalid)?; + + let var_id = var_decl_arg.as_var().ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + let variable = working_set.get_variable(var_id); + + compile_block( + working_set, + builder, + block, + RedirectModes::capture_out(call.head), + Some(io_reg), + io_reg, + )?; + + // If the variable is a glob type variable, we should cast it with GlobFrom + if variable.ty == Type::Glob { + builder.push( + Instruction::GlobFrom { + src_dst: io_reg, + no_expand: true, + } + .into_spanned(call.head), + )?; + } + + builder.push( + Instruction::StoreVariable { + var_id, + src: io_reg, + } + .into_spanned(call.head), + )?; + builder.add_comment("let"); + + // Don't forget to set io_reg to Empty afterward, as that's the result of an assignment + builder.load_empty(io_reg)?; + + Ok(()) +} + +/// Compile a call to `try`, setting an error handler over the evaluated block +pub(crate) fn compile_try( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode (literal block): + // + // on-error-into ERR, %io_reg // or without + // %io_reg <- <...block...> <- %io_reg + // check-external-failed %failed_reg, %io_reg + // branch-if %failed_reg, FAIL + // pop-error-handler + // jump END + // FAIL: drain %io_reg + // unreachable + // ERR: clone %err_reg, %io_reg + // store-variable $err_var, %err_reg // or without + // %io_reg <- <...catch block...> <- %io_reg // set to empty if no catch block + // END: + // + // with expression that can't be inlined: + // + // %closure_reg <- + // on-error-into ERR, %io_reg + // %io_reg <- <...block...> <- %io_reg + // check-external-failed %failed_reg, %io_reg + // branch-if %failed_reg, FAIL + // pop-error-handler + // jump END + // FAIL: drain %io_reg + // unreachable + // ERR: clone %err_reg, %io_reg + // push-positional %closure_reg + // push-positional %err_reg + // call "do", %io_reg + // END: + let invalid = || CompileError::InvalidKeywordCall { + keyword: "try".into(), + span: call.head, + }; + + let block_arg = call.positional_nth(0).ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + let catch_expr = match call.positional_nth(1) { + Some(kw_expr) => Some(kw_expr.as_keyword().ok_or_else(invalid)?), + None => None, + }; + let catch_span = catch_expr.map(|e| e.span).unwrap_or(call.head); + + let err_label = builder.label(None); + let failed_label = builder.label(None); + let end_label = builder.label(None); + + // We have two ways of executing `catch`: if it was provided as a literal, we can inline it. + // Otherwise, we have to evaluate the expression and keep it as a register, and then call `do`. + enum CatchType<'a> { + Block { + block: &'a Block, + var_id: Option, + }, + Closure { + closure_reg: RegId, + }, + } + + let catch_type = catch_expr + .map(|catch_expr| match catch_expr.as_block() { + Some(block_id) => { + let block = working_set.get_block(block_id); + let var_id = block.signature.get_positional(0).and_then(|v| v.var_id); + Ok(CatchType::Block { block, var_id }) + } + None => { + // We have to compile the catch_expr and use it as a closure + let closure_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + catch_expr, + RedirectModes::capture_out(catch_expr.span), + None, + closure_reg, + )?; + Ok(CatchType::Closure { closure_reg }) + } + }) + .transpose()?; + + // Put the error handler instruction. If we have a catch expression then we should capture the + // error. + if catch_type.is_some() { + builder.push( + Instruction::OnErrorInto { + index: err_label.0, + dst: io_reg, + } + .into_spanned(call.head), + )? + } else { + // Otherwise, we don't need the error value. + builder.push(Instruction::OnError { index: err_label.0 }.into_spanned(call.head))? + }; + + builder.add_comment("try"); + + // Compile the block + compile_block( + working_set, + builder, + block, + redirect_modes.clone(), + Some(io_reg), + io_reg, + )?; + + // Check for external command exit code failure, and also redirect that to the catch handler + let failed_reg = builder.next_register()?; + builder.push( + Instruction::CheckExternalFailed { + dst: failed_reg, + src: io_reg, + } + .into_spanned(catch_span), + )?; + builder.branch_if(failed_reg, failed_label, catch_span)?; + + // Successful case: pop the error handler + builder.push(Instruction::PopErrorHandler.into_spanned(call.head))?; + + // Jump over the failure case + builder.jump(end_label, catch_span)?; + + // Set up an error handler preamble for failed external. + // Draining the %io_reg results in the error handler being called with Empty, and sets + // $env.LAST_EXIT_CODE + builder.set_label(failed_label, builder.here())?; + builder.drain(io_reg, catch_span)?; + builder.add_comment("branches to err"); + builder.unreachable(catch_span)?; + + // This is the real error handler + builder.set_label(err_label, builder.here())?; + + // Mark out register as likely not clean - state in error handler is not well defined + builder.mark_register(io_reg)?; + + // Now compile whatever is necessary for the error handler + match catch_type { + Some(CatchType::Block { block, var_id }) => { + // Error will be in io_reg + builder.mark_register(io_reg)?; + if let Some(var_id) = var_id { + // Take a copy of the error as $err, since it will also be input + let err_reg = builder.next_register()?; + builder.push( + Instruction::Clone { + dst: err_reg, + src: io_reg, + } + .into_spanned(catch_span), + )?; + builder.push( + Instruction::StoreVariable { + var_id, + src: err_reg, + } + .into_spanned(catch_span), + )?; + } + // Compile the block, now that the variable is set + compile_block( + working_set, + builder, + block, + redirect_modes, + Some(io_reg), + io_reg, + )?; + } + Some(CatchType::Closure { closure_reg }) => { + // We should call `do`. Error will be in io_reg + let do_decl_id = working_set.find_decl(b"do").ok_or_else(|| { + CompileError::MissingRequiredDeclaration { + decl_name: "do".into(), + span: call.head, + } + })?; + + // Take a copy of io_reg, because we pass it both as an argument and input + builder.mark_register(io_reg)?; + let err_reg = builder.next_register()?; + builder.push( + Instruction::Clone { + dst: err_reg, + src: io_reg, + } + .into_spanned(catch_span), + )?; + + // Push the closure and the error + builder + .push(Instruction::PushPositional { src: closure_reg }.into_spanned(catch_span))?; + builder.push(Instruction::PushPositional { src: err_reg }.into_spanned(catch_span))?; + + // Call `$err | do $closure $err` + builder.push( + Instruction::Call { + decl_id: do_decl_id, + src_dst: io_reg, + } + .into_spanned(catch_span), + )?; + } + None => { + // Just set out to empty. + builder.load_empty(io_reg)?; + } + } + + // This is the end - if we succeeded, should jump here + builder.set_label(end_label, builder.here())?; + + Ok(()) +} + +/// Compile a call to `loop` (via `jump`) +pub(crate) fn compile_loop( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // drop %io_reg + // LOOP: %io_reg <- ...... + // drain %io_reg + // jump %LOOP + // END: drop %io_reg + let invalid = || CompileError::InvalidKeywordCall { + keyword: "loop".into(), + span: call.head, + }; + + let block_arg = call.positional_nth(0).ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + let loop_ = builder.begin_loop(); + builder.load_empty(io_reg)?; + + builder.set_label(loop_.continue_label, builder.here())?; + + compile_block( + working_set, + builder, + block, + RedirectModes::default(), + None, + io_reg, + )?; + + // Drain the output, just like for a semicolon + builder.drain(io_reg, call.head)?; + + builder.jump(loop_.continue_label, call.head)?; + builder.add_comment("loop"); + + builder.set_label(loop_.break_label, builder.here())?; + builder.end_loop(loop_)?; + + // State of %io_reg is not necessarily well defined here due to control flow, so make sure it's + // empty. + builder.mark_register(io_reg)?; + builder.load_empty(io_reg)?; + + Ok(()) +} + +/// Compile a call to `while`, via branch instructions +pub(crate) fn compile_while( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // LOOP: %io_reg <- + // branch-if %io_reg, TRUE + // jump FALSE + // TRUE: %io_reg <- ...... + // drain %io_reg + // jump LOOP + // FALSE: drop %io_reg + let invalid = || CompileError::InvalidKeywordCall { + keyword: "while".into(), + span: call.head, + }; + + let cond_arg = call.positional_nth(0).ok_or_else(invalid)?; + let block_arg = call.positional_nth(1).ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + let loop_ = builder.begin_loop(); + builder.set_label(loop_.continue_label, builder.here())?; + + let true_label = builder.label(None); + + compile_expression( + working_set, + builder, + cond_arg, + RedirectModes::capture_out(call.head), + None, + io_reg, + )?; + + builder.branch_if(io_reg, true_label, call.head)?; + builder.add_comment("while"); + builder.jump(loop_.break_label, call.head)?; + builder.add_comment("end while"); + + builder.set_label(true_label, builder.here())?; + + compile_block( + working_set, + builder, + block, + RedirectModes::default(), + None, + io_reg, + )?; + + // Drain the result, just like for a semicolon + builder.drain(io_reg, call.head)?; + + builder.jump(loop_.continue_label, call.head)?; + builder.add_comment("while"); + + builder.set_label(loop_.break_label, builder.here())?; + builder.end_loop(loop_)?; + + // State of %io_reg is not necessarily well defined here due to control flow, so make sure it's + // empty. + builder.mark_register(io_reg)?; + builder.load_empty(io_reg)?; + + Ok(()) +} + +/// Compile a call to `for` (via `iterate`) +pub(crate) fn compile_for( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %stream_reg <- + // LOOP: iterate %io_reg, %stream_reg, END + // store-variable $var, %io_reg + // %io_reg <- <...block...> + // drain %io_reg + // jump LOOP + // END: drop %io_reg + let invalid = || CompileError::InvalidKeywordCall { + keyword: "for".into(), + span: call.head, + }; + + if call.get_named_arg("numbered").is_some() { + // This is deprecated and we don't support it. + return Err(invalid()); + } + + let var_decl_arg = call.positional_nth(0).ok_or_else(invalid)?; + let var_id = var_decl_arg.as_var().ok_or_else(invalid)?; + + let in_arg = call.positional_nth(1).ok_or_else(invalid)?; + let in_expr = in_arg.as_keyword().ok_or_else(invalid)?; + + let block_arg = call.positional_nth(2).ok_or_else(invalid)?; + let block_id = block_arg.as_block().ok_or_else(invalid)?; + let block = working_set.get_block(block_id); + + // Ensure io_reg is marked so we don't use it + builder.mark_register(io_reg)?; + + let stream_reg = builder.next_register()?; + + compile_expression( + working_set, + builder, + in_expr, + RedirectModes::capture_out(in_expr.span), + None, + stream_reg, + )?; + + // Set up loop state + let loop_ = builder.begin_loop(); + builder.set_label(loop_.continue_label, builder.here())?; + + // This gets a value from the stream each time it's executed + // io_reg basically will act as our scratch register here + builder.push( + Instruction::Iterate { + dst: io_reg, + stream: stream_reg, + end_index: loop_.break_label.0, + } + .into_spanned(call.head), + )?; + builder.add_comment("for"); + + // Put the received value in the variable + builder.push( + Instruction::StoreVariable { + var_id, + src: io_reg, + } + .into_spanned(var_decl_arg.span), + )?; + + // Do the body of the block + compile_block( + working_set, + builder, + block, + RedirectModes::default(), + None, + io_reg, + )?; + + // Drain the output, just like for a semicolon + builder.drain(io_reg, call.head)?; + + // Loop back to iterate to get the next value + builder.jump(loop_.continue_label, call.head)?; + + // Set the end of the loop + builder.set_label(loop_.break_label, builder.here())?; + builder.end_loop(loop_)?; + + // We don't need stream_reg anymore, after the loop + // io_reg may or may not be empty, so be sure it is + builder.free_register(stream_reg)?; + builder.mark_register(io_reg)?; + builder.load_empty(io_reg)?; + + Ok(()) +} + +/// Compile a call to `break`. +pub(crate) fn compile_break( + _working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + if builder.is_in_loop() { + builder.load_empty(io_reg)?; + builder.push_break(call.head)?; + builder.add_comment("break"); + } else { + // Fall back to calling the command if we can't find the loop target statically + builder.push( + Instruction::Call { + decl_id: call.decl_id, + src_dst: io_reg, + } + .into_spanned(call.head), + )?; + } + Ok(()) +} + +/// Compile a call to `continue`. +pub(crate) fn compile_continue( + _working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + if builder.is_in_loop() { + builder.load_empty(io_reg)?; + builder.push_continue(call.head)?; + builder.add_comment("continue"); + } else { + // Fall back to calling the command if we can't find the loop target statically + builder.push( + Instruction::Call { + decl_id: call.decl_id, + src_dst: io_reg, + } + .into_spanned(call.head), + )?; + } + Ok(()) +} + +/// Compile a call to `return` as a `return-early` instruction. +/// +/// This is not strictly necessary, but it is more efficient. +pub(crate) fn compile_return( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + call: &Call, + _redirect_modes: RedirectModes, + io_reg: RegId, +) -> Result<(), CompileError> { + // Pseudocode: + // + // %io_reg <- + // return-early %io_reg + if let Some(arg_expr) = call.positional_nth(0) { + compile_expression( + working_set, + builder, + arg_expr, + RedirectModes::capture_out(arg_expr.span), + None, + io_reg, + )?; + } else { + builder.load_empty(io_reg)?; + } + + // TODO: It would be nice if this could be `return` instead, but there is a little bit of + // behaviour remaining that still depends on `ShellError::Return` + builder.push(Instruction::ReturnEarly { src: io_reg }.into_spanned(call.head))?; + + // io_reg is supposed to remain allocated + builder.load_empty(io_reg)?; + + Ok(()) +} diff --git a/crates/nu-engine/src/compile/mod.rs b/crates/nu-engine/src/compile/mod.rs new file mode 100644 index 0000000000..8f6ae22682 --- /dev/null +++ b/crates/nu-engine/src/compile/mod.rs @@ -0,0 +1,204 @@ +use nu_protocol::{ + ast::{Block, Pipeline, PipelineRedirection, RedirectionSource, RedirectionTarget}, + engine::StateWorkingSet, + ir::{Instruction, IrBlock, RedirectMode}, + CompileError, IntoSpanned, RegId, Span, +}; + +mod builder; +mod call; +mod expression; +mod keyword; +mod operator; +mod redirect; + +use builder::BlockBuilder; +use call::*; +use expression::compile_expression; +use operator::*; +use redirect::*; + +const BLOCK_INPUT: RegId = RegId(0); + +/// Compile Nushell pipeline abstract syntax tree (AST) to internal representation (IR) instructions +/// for evaluation. +pub fn compile(working_set: &StateWorkingSet, block: &Block) -> Result { + let mut builder = BlockBuilder::new(block.span); + + let span = block.span.unwrap_or(Span::unknown()); + + compile_block( + working_set, + &mut builder, + block, + RedirectModes::caller(span), + Some(BLOCK_INPUT), + BLOCK_INPUT, + )?; + + // A complete block has to end with a `return` + builder.push(Instruction::Return { src: BLOCK_INPUT }.into_spanned(span))?; + + builder.finish() +} + +/// Compiles a [`Block`] in-place into an IR block. This can be used in a nested manner, for example +/// by [`compile_if()`], where the instructions for the blocks for the if/else are inlined into the +/// top-level IR block. +fn compile_block( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + block: &Block, + redirect_modes: RedirectModes, + in_reg: Option, + out_reg: RegId, +) -> Result<(), CompileError> { + let span = block.span.unwrap_or(Span::unknown()); + let mut redirect_modes = Some(redirect_modes); + if !block.pipelines.is_empty() { + let last_index = block.pipelines.len() - 1; + for (index, pipeline) in block.pipelines.iter().enumerate() { + compile_pipeline( + working_set, + builder, + pipeline, + span, + // the redirect mode only applies to the last pipeline. + if index == last_index { + redirect_modes + .take() + .expect("should only take redirect_modes once") + } else { + RedirectModes::default() + }, + // input is only passed to the first pipeline. + if index == 0 { in_reg } else { None }, + out_reg, + )?; + + if index != last_index { + // Explicitly drain the out reg after each non-final pipeline, because that's how + // the semicolon functions. + if builder.is_allocated(out_reg) { + builder.push(Instruction::Drain { src: out_reg }.into_spanned(span))?; + } + builder.load_empty(out_reg)?; + } + } + Ok(()) + } else if in_reg.is_none() { + builder.load_empty(out_reg) + } else { + Ok(()) + } +} + +fn compile_pipeline( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + pipeline: &Pipeline, + fallback_span: Span, + redirect_modes: RedirectModes, + in_reg: Option, + out_reg: RegId, +) -> Result<(), CompileError> { + let mut iter = pipeline.elements.iter().peekable(); + let mut in_reg = in_reg; + let mut redirect_modes = Some(redirect_modes); + while let Some(element) = iter.next() { + let span = element.pipe.unwrap_or(fallback_span); + + // We have to get the redirection mode from either the explicit redirection in the pipeline + // element, or from the next expression if it's specified there. If this is the last + // element, then it's from whatever is passed in as the mode to use. + + let next_redirect_modes = if let Some(next_element) = iter.peek() { + let mut modes = redirect_modes_of_expression(working_set, &next_element.expr, span)?; + + // If there's a next element with no inherent redirection we always pipe out *unless* + // this is a single redirection of stderr to pipe (e>|) + if modes.out.is_none() + && !matches!( + element.redirection, + Some(PipelineRedirection::Single { + source: RedirectionSource::Stderr, + target: RedirectionTarget::Pipe { .. } + }) + ) + { + let pipe_span = next_element.pipe.unwrap_or(next_element.expr.span); + modes.out = Some(RedirectMode::Pipe.into_spanned(pipe_span)); + } + + modes + } else { + redirect_modes + .take() + .expect("should only take redirect_modes once") + }; + + let spec_redirect_modes = match &element.redirection { + Some(PipelineRedirection::Single { source, target }) => { + let mode = redirection_target_to_mode(working_set, builder, target)?; + match source { + RedirectionSource::Stdout => RedirectModes { + out: Some(mode), + err: None, + }, + RedirectionSource::Stderr => RedirectModes { + out: None, + err: Some(mode), + }, + RedirectionSource::StdoutAndStderr => RedirectModes { + out: Some(mode), + err: Some(mode), + }, + } + } + Some(PipelineRedirection::Separate { out, err }) => { + // In this case, out and err must not both be Pipe + assert!( + !matches!( + (out, err), + ( + RedirectionTarget::Pipe { .. }, + RedirectionTarget::Pipe { .. } + ) + ), + "for Separate redirection, out and err targets must not both be Pipe" + ); + let out = redirection_target_to_mode(working_set, builder, out)?; + let err = redirection_target_to_mode(working_set, builder, err)?; + RedirectModes { + out: Some(out), + err: Some(err), + } + } + None => RedirectModes { + out: None, + err: None, + }, + }; + + let redirect_modes = RedirectModes { + out: spec_redirect_modes.out.or(next_redirect_modes.out), + err: spec_redirect_modes.err.or(next_redirect_modes.err), + }; + + compile_expression( + working_set, + builder, + &element.expr, + redirect_modes.clone(), + in_reg, + out_reg, + )?; + + // Clean up the redirection + finish_redirection(builder, redirect_modes, out_reg)?; + + // The next pipeline element takes input from this output + in_reg = Some(out_reg); + } + Ok(()) +} diff --git a/crates/nu-engine/src/compile/operator.rs b/crates/nu-engine/src/compile/operator.rs new file mode 100644 index 0000000000..a1ed3f66df --- /dev/null +++ b/crates/nu-engine/src/compile/operator.rs @@ -0,0 +1,378 @@ +use nu_protocol::{ + ast::{Assignment, Boolean, CellPath, Expr, Expression, Math, Operator, PathMember}, + engine::StateWorkingSet, + ir::{Instruction, Literal}, + IntoSpanned, RegId, Span, Spanned, ENV_VARIABLE_ID, +}; +use nu_utils::IgnoreCaseExt; + +use super::{compile_expression, BlockBuilder, CompileError, RedirectModes}; + +pub(crate) fn compile_binary_op( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + lhs: &Expression, + op: Spanned, + rhs: &Expression, + span: Span, + out_reg: RegId, +) -> Result<(), CompileError> { + if let Operator::Assignment(assign_op) = op.item { + if let Some(decomposed_op) = decompose_assignment(assign_op) { + // Compiling an assignment that uses a binary op with the existing value + compile_binary_op( + working_set, + builder, + lhs, + decomposed_op.into_spanned(op.span), + rhs, + span, + out_reg, + )?; + } else { + // Compiling a plain assignment, where the current left-hand side value doesn't matter + compile_expression( + working_set, + builder, + rhs, + RedirectModes::capture_out(rhs.span), + None, + out_reg, + )?; + } + + compile_assignment(working_set, builder, lhs, op.span, out_reg)?; + + // Load out_reg with Nothing, as that's the result of an assignment + builder.load_literal(out_reg, Literal::Nothing.into_spanned(op.span)) + } else { + // Not an assignment: just do the binary op + let lhs_reg = out_reg; + + compile_expression( + working_set, + builder, + lhs, + RedirectModes::capture_out(lhs.span), + None, + lhs_reg, + )?; + + match op.item { + // `and` / `or` are short-circuiting, and we can get by with one register and a branch + Operator::Boolean(Boolean::And) => { + let true_label = builder.label(None); + builder.branch_if(lhs_reg, true_label, op.span)?; + + // If the branch was not taken it's false, so short circuit to load false + let false_label = builder.label(None); + builder.jump(false_label, op.span)?; + + builder.set_label(true_label, builder.here())?; + compile_expression( + working_set, + builder, + rhs, + RedirectModes::capture_out(rhs.span), + None, + lhs_reg, + )?; + + let end_label = builder.label(None); + builder.jump(end_label, op.span)?; + + // Consumed by `branch-if`, so we have to set it false again + builder.set_label(false_label, builder.here())?; + builder.load_literal(lhs_reg, Literal::Bool(false).into_spanned(lhs.span))?; + + builder.set_label(end_label, builder.here())?; + } + Operator::Boolean(Boolean::Or) => { + let true_label = builder.label(None); + builder.branch_if(lhs_reg, true_label, op.span)?; + + // If the branch was not taken it's false, so do the right-side expression + compile_expression( + working_set, + builder, + rhs, + RedirectModes::capture_out(rhs.span), + None, + lhs_reg, + )?; + + let end_label = builder.label(None); + builder.jump(end_label, op.span)?; + + // Consumed by `branch-if`, so we have to set it true again + builder.set_label(true_label, builder.here())?; + builder.load_literal(lhs_reg, Literal::Bool(true).into_spanned(lhs.span))?; + + builder.set_label(end_label, builder.here())?; + } + _ => { + // Any other operator, via `binary-op` + let rhs_reg = builder.next_register()?; + + compile_expression( + working_set, + builder, + rhs, + RedirectModes::capture_out(rhs.span), + None, + rhs_reg, + )?; + + builder.push( + Instruction::BinaryOp { + lhs_dst: lhs_reg, + op: op.item, + rhs: rhs_reg, + } + .into_spanned(op.span), + )?; + } + } + + if lhs_reg != out_reg { + builder.push( + Instruction::Move { + dst: out_reg, + src: lhs_reg, + } + .into_spanned(op.span), + )?; + } + + builder.push(Instruction::Span { src_dst: out_reg }.into_spanned(span))?; + + Ok(()) + } +} + +/// The equivalent plain operator to use for an assignment, if any +pub(crate) fn decompose_assignment(assignment: Assignment) -> Option { + match assignment { + Assignment::Assign => None, + Assignment::PlusAssign => Some(Operator::Math(Math::Plus)), + Assignment::AppendAssign => Some(Operator::Math(Math::Append)), + Assignment::MinusAssign => Some(Operator::Math(Math::Minus)), + Assignment::MultiplyAssign => Some(Operator::Math(Math::Multiply)), + Assignment::DivideAssign => Some(Operator::Math(Math::Divide)), + } +} + +/// Compile assignment of the value in a register to a left-hand expression +pub(crate) fn compile_assignment( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + lhs: &Expression, + assignment_span: Span, + rhs_reg: RegId, +) -> Result<(), CompileError> { + match lhs.expr { + Expr::Var(var_id) => { + // Double check that the variable is supposed to be mutable + if !working_set.get_variable(var_id).mutable { + return Err(CompileError::AssignmentRequiresMutableVar { span: lhs.span }); + } + + builder.push( + Instruction::StoreVariable { + var_id, + src: rhs_reg, + } + .into_spanned(assignment_span), + )?; + Ok(()) + } + Expr::FullCellPath(ref path) => match (&path.head, &path.tail) { + ( + Expression { + expr: Expr::Var(var_id), + .. + }, + _, + ) if *var_id == ENV_VARIABLE_ID => { + // This will be an assignment to an environment variable. + let Some(PathMember::String { val: key, .. }) = path.tail.first() else { + return Err(CompileError::CannotReplaceEnv { span: lhs.span }); + }; + + // Some env vars can't be set by Nushell code. + const AUTOMATIC_NAMES: &[&str] = &["PWD", "FILE_PWD", "CURRENT_FILE"]; + if AUTOMATIC_NAMES.iter().any(|name| key.eq_ignore_case(name)) { + return Err(CompileError::AutomaticEnvVarSetManually { + envvar_name: "PWD".into(), + span: lhs.span, + }); + } + + let key_data = builder.data(key)?; + + let val_reg = if path.tail.len() > 1 { + // Get the current value of the head and first tail of the path, from env + let head_reg = builder.next_register()?; + + // We could use compile_load_env, but this shares the key data... + // Always use optional, because it doesn't matter if it's already there + builder.push( + Instruction::LoadEnvOpt { + dst: head_reg, + key: key_data, + } + .into_spanned(lhs.span), + )?; + + // Default to empty record so we can do further upserts + let default_label = builder.label(None); + let upsert_label = builder.label(None); + builder.branch_if_empty(head_reg, default_label, assignment_span)?; + builder.jump(upsert_label, assignment_span)?; + + builder.set_label(default_label, builder.here())?; + builder.load_literal( + head_reg, + Literal::Record { capacity: 0 }.into_spanned(lhs.span), + )?; + + // Do the upsert on the current value to incorporate rhs + builder.set_label(upsert_label, builder.here())?; + compile_upsert_cell_path( + builder, + (&path.tail[1..]).into_spanned(lhs.span), + head_reg, + rhs_reg, + assignment_span, + )?; + + head_reg + } else { + // Path has only one tail, so we don't need the current value to do an upsert, + // just set it directly to rhs + rhs_reg + }; + + // Finally, store the modified env variable + builder.push( + Instruction::StoreEnv { + key: key_data, + src: val_reg, + } + .into_spanned(assignment_span), + )?; + Ok(()) + } + (_, tail) if tail.is_empty() => { + // If the path tail is empty, we can really just treat this as if it were an + // assignment to the head + compile_assignment(working_set, builder, &path.head, assignment_span, rhs_reg) + } + _ => { + // Just a normal assignment to some path + let head_reg = builder.next_register()?; + + // Compile getting current value of the head expression + compile_expression( + working_set, + builder, + &path.head, + RedirectModes::capture_out(path.head.span), + None, + head_reg, + )?; + + // Upsert the tail of the path into the old value of the head expression + compile_upsert_cell_path( + builder, + path.tail.as_slice().into_spanned(lhs.span), + head_reg, + rhs_reg, + assignment_span, + )?; + + // Now compile the assignment of the updated value to the head + compile_assignment(working_set, builder, &path.head, assignment_span, head_reg) + } + }, + Expr::Garbage => Err(CompileError::Garbage { span: lhs.span }), + _ => Err(CompileError::AssignmentRequiresVar { span: lhs.span }), + } +} + +/// Compile an upsert-cell-path instruction, with known literal members +pub(crate) fn compile_upsert_cell_path( + builder: &mut BlockBuilder, + members: Spanned<&[PathMember]>, + src_dst: RegId, + new_value: RegId, + span: Span, +) -> Result<(), CompileError> { + let path_reg = builder.literal( + Literal::CellPath( + CellPath { + members: members.item.to_vec(), + } + .into(), + ) + .into_spanned(members.span), + )?; + builder.push( + Instruction::UpsertCellPath { + src_dst, + path: path_reg, + new_value, + } + .into_spanned(span), + )?; + Ok(()) +} + +/// Compile the correct sequence to get an environment variable + follow a path on it +pub(crate) fn compile_load_env( + builder: &mut BlockBuilder, + span: Span, + path: &[PathMember], + out_reg: RegId, +) -> Result<(), CompileError> { + if path.is_empty() { + builder.push( + Instruction::LoadVariable { + dst: out_reg, + var_id: ENV_VARIABLE_ID, + } + .into_spanned(span), + )?; + } else { + let (key, optional) = match &path[0] { + PathMember::String { val, optional, .. } => (builder.data(val)?, *optional), + PathMember::Int { span, .. } => { + return Err(CompileError::AccessEnvByInt { span: *span }) + } + }; + let tail = &path[1..]; + + if optional { + builder.push(Instruction::LoadEnvOpt { dst: out_reg, key }.into_spanned(span))?; + } else { + builder.push(Instruction::LoadEnv { dst: out_reg, key }.into_spanned(span))?; + } + + if !tail.is_empty() { + let path = builder.literal( + Literal::CellPath(Box::new(CellPath { + members: tail.to_vec(), + })) + .into_spanned(span), + )?; + builder.push( + Instruction::FollowCellPath { + src_dst: out_reg, + path, + } + .into_spanned(span), + )?; + } + } + Ok(()) +} diff --git a/crates/nu-engine/src/compile/redirect.rs b/crates/nu-engine/src/compile/redirect.rs new file mode 100644 index 0000000000..15af1a9f8c --- /dev/null +++ b/crates/nu-engine/src/compile/redirect.rs @@ -0,0 +1,157 @@ +use nu_protocol::{ + ast::{Expression, RedirectionTarget}, + engine::StateWorkingSet, + ir::{Instruction, RedirectMode}, + IntoSpanned, OutDest, RegId, Span, Spanned, +}; + +use super::{compile_expression, BlockBuilder, CompileError}; + +#[derive(Default, Clone)] +pub(crate) struct RedirectModes { + pub(crate) out: Option>, + pub(crate) err: Option>, +} + +impl RedirectModes { + pub(crate) fn capture_out(span: Span) -> Self { + RedirectModes { + out: Some(RedirectMode::Capture.into_spanned(span)), + err: None, + } + } + + pub(crate) fn caller(span: Span) -> RedirectModes { + RedirectModes { + out: Some(RedirectMode::Caller.into_spanned(span)), + err: Some(RedirectMode::Caller.into_spanned(span)), + } + } +} + +pub(crate) fn redirection_target_to_mode( + working_set: &StateWorkingSet, + builder: &mut BlockBuilder, + target: &RedirectionTarget, +) -> Result, CompileError> { + Ok(match target { + RedirectionTarget::File { + expr, + append, + span: redir_span, + } => { + let file_num = builder.next_file_num()?; + let path_reg = builder.next_register()?; + compile_expression( + working_set, + builder, + expr, + RedirectModes::capture_out(*redir_span), + None, + path_reg, + )?; + builder.push( + Instruction::OpenFile { + file_num, + path: path_reg, + append: *append, + } + .into_spanned(*redir_span), + )?; + RedirectMode::File { file_num }.into_spanned(*redir_span) + } + RedirectionTarget::Pipe { span } => RedirectMode::Pipe.into_spanned(*span), + }) +} + +pub(crate) fn redirect_modes_of_expression( + working_set: &StateWorkingSet, + expression: &Expression, + redir_span: Span, +) -> Result { + let (out, err) = expression.expr.pipe_redirection(working_set); + Ok(RedirectModes { + out: out + .map(|r| r.into_spanned(redir_span)) + .map(out_dest_to_redirect_mode) + .transpose()?, + err: err + .map(|r| r.into_spanned(redir_span)) + .map(out_dest_to_redirect_mode) + .transpose()?, + }) +} + +/// Finish the redirection for an expression, writing to and closing files as necessary +pub(crate) fn finish_redirection( + builder: &mut BlockBuilder, + modes: RedirectModes, + out_reg: RegId, +) -> Result<(), CompileError> { + if let Some(Spanned { + item: RedirectMode::File { file_num }, + span, + }) = modes.out + { + // If out is a file and err is a pipe, we must not consume the expression result - + // that is actually the err, in that case. + if !matches!( + modes.err, + Some(Spanned { + item: RedirectMode::Pipe { .. }, + .. + }) + ) { + builder.push( + Instruction::WriteFile { + file_num, + src: out_reg, + } + .into_spanned(span), + )?; + builder.load_empty(out_reg)?; + } + builder.push(Instruction::CloseFile { file_num }.into_spanned(span))?; + } + + match modes.err { + Some(Spanned { + item: RedirectMode::File { file_num }, + span, + }) => { + // Close the file, unless it's the same as out (in which case it was already closed) + if !modes.out.is_some_and(|out_mode| match out_mode.item { + RedirectMode::File { + file_num: out_file_num, + } => file_num == out_file_num, + _ => false, + }) { + builder.push(Instruction::CloseFile { file_num }.into_spanned(span))?; + } + } + Some(Spanned { + item: RedirectMode::Pipe, + span, + }) => { + builder.push(Instruction::CheckErrRedirected { src: out_reg }.into_spanned(span))?; + } + _ => (), + } + + Ok(()) +} + +pub(crate) fn out_dest_to_redirect_mode( + out_dest: Spanned, +) -> Result, CompileError> { + let span = out_dest.span; + out_dest + .map(|out_dest| match out_dest { + OutDest::Pipe => Ok(RedirectMode::Pipe), + OutDest::Capture => Ok(RedirectMode::Capture), + OutDest::Null => Ok(RedirectMode::Null), + OutDest::Inherit => Ok(RedirectMode::Inherit), + OutDest::File(_) => Err(CompileError::InvalidRedirectMode { span }), + }) + .transpose() +} diff --git a/crates/nu-engine/src/documentation.rs b/crates/nu-engine/src/documentation.rs index a7d4950036..a0dee3d343 100644 --- a/crates/nu-engine/src/documentation.rs +++ b/crates/nu-engine/src/documentation.rs @@ -3,8 +3,8 @@ use nu_protocol::{ ast::{Argument, Call, Expr, Expression, RecordItem}, debugger::WithoutDebug, engine::{Command, EngineState, Stack, UNKNOWN_SPAN_ID}, - record, Category, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId, Spanned, - SyntaxShape, Type, Value, + record, Category, Config, Example, IntoPipelineData, PipelineData, Signature, Span, SpanId, + Spanned, SyntaxShape, Type, Value, }; use std::{collections::HashMap, fmt::Write}; @@ -13,7 +13,7 @@ pub fn get_full_help( engine_state: &EngineState, stack: &mut Stack, ) -> String { - let config = engine_state.get_config(); + let config = stack.get_config(engine_state); let doc_config = DocumentationConfig { no_subcommands: false, no_color: !config.use_ansi_coloring, @@ -45,10 +45,12 @@ fn nu_highlight_string(code_string: &str, engine_state: &EngineState, stack: &mu if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) { let decl = engine_state.get_decl(highlighter); + let call = Call::new(Span::unknown()); + if let Ok(output) = decl.run( engine_state, stack, - &Call::new(Span::unknown()), + &(&call).into(), Value::string(code_string, Span::unknown()).into_pipeline_data(), ) { let result = output.into_value(Span::unknown()); @@ -68,16 +70,30 @@ fn get_documentation( config: &DocumentationConfig, is_parser_keyword: bool, ) -> String { + let nu_config = stack.get_config(engine_state); + // Create ansi colors //todo make these configurable -- pull from enginestate.config - let help_section_name: String = - get_ansi_color_for_component_or_default(engine_state, "shape_string", "\x1b[32m"); // default: green + let help_section_name: String = get_ansi_color_for_component_or_default( + engine_state, + &nu_config, + "shape_string", + "\x1b[32m", + ); // default: green - let help_subcolor_one: String = - get_ansi_color_for_component_or_default(engine_state, "shape_external", "\x1b[36m"); // default: cyan - // was const bb: &str = "\x1b[1;34m"; // bold blue - let help_subcolor_two: String = - get_ansi_color_for_component_or_default(engine_state, "shape_block", "\x1b[94m"); // default: light blue (nobold, should be bolding the *names*) + let help_subcolor_one: String = get_ansi_color_for_component_or_default( + engine_state, + &nu_config, + "shape_external", + "\x1b[36m", + ); // default: cyan + // was const bb: &str = "\x1b[1;34m"; // bold blue + let help_subcolor_two: String = get_ansi_color_for_component_or_default( + engine_state, + &nu_config, + "shape_block", + "\x1b[94m", + ); // default: light blue (nobold, should be bolding the *names*) const RESET: &str = "\x1b[0m"; // reset @@ -137,13 +153,12 @@ fn get_documentation( } if !sig.named.is_empty() { - long_desc.push_str(&get_flags_section(Some(engine_state), sig, |v| { - nu_highlight_string( - &v.to_parsable_string(", ", &engine_state.config), - engine_state, - stack, - ) - })) + long_desc.push_str(&get_flags_section( + Some(engine_state), + Some(&nu_config), + sig, + |v| nu_highlight_string(&v.to_parsable_string(", ", &nu_config), engine_state, stack), + )) } if !sig.required_positional.is_empty() @@ -187,7 +202,7 @@ fn get_documentation( format!( " (optional, default: {})", nu_highlight_string( - &value.to_parsable_string(", ", &engine_state.config), + &value.to_parsable_string(", ", &nu_config), engine_state, stack ) @@ -269,11 +284,12 @@ fn get_documentation( let _ = write!(long_desc, "\n > {}\n", example.example); } else if let Some(highlighter) = engine_state.find_decl(b"nu-highlight", &[]) { let decl = engine_state.get_decl(highlighter); + let call = Call::new(Span::unknown()); match decl.run( engine_state, stack, - &Call::new(Span::unknown()), + &(&call).into(), Value::string(example.example, Span::unknown()).into_pipeline_data(), ) { Ok(output) => { @@ -326,7 +342,7 @@ fn get_documentation( .run( engine_state, stack, - &table_call, + &(&table_call).into(), PipelineData::Value(result.clone(), None), ) .ok() @@ -336,7 +352,7 @@ fn get_documentation( let _ = writeln!( long_desc, " {}", - item.to_expanded_string("", engine_state.get_config()) + item.to_expanded_string("", &nu_config) .replace('\n', "\n ") .trim() ); @@ -355,15 +371,16 @@ fn get_documentation( fn get_ansi_color_for_component_or_default( engine_state: &EngineState, + nu_config: &Config, theme_component: &str, default: &str, ) -> String { - if let Some(color) = &engine_state.get_config().color_config.get(theme_component) { + if let Some(color) = &nu_config.color_config.get(theme_component) { let caller_stack = &mut Stack::new().capture(); let span = Span::unknown(); let span_id = UNKNOWN_SPAN_ID; - let argument_opt = get_argument_for_color_value(engine_state, color, span, span_id); + let argument_opt = get_argument_for_color_value(nu_config, color, span, span_id); // Call ansi command using argument if let Some(argument) = argument_opt { @@ -391,8 +408,8 @@ fn get_ansi_color_for_component_or_default( } fn get_argument_for_color_value( - engine_state: &EngineState, - color: &&Value, + nu_config: &Config, + color: &Value, span: Span, span_id: SpanId, ) -> Option { @@ -409,9 +426,7 @@ fn get_argument_for_color_value( Type::String, ), Expression::new_existing( - Expr::String( - v.clone().to_expanded_string("", engine_state.get_config()), - ), + Expr::String(v.clone().to_expanded_string("", nu_config)), span, span_id, Type::String, @@ -453,6 +468,7 @@ pub fn document_shape(shape: SyntaxShape) -> SyntaxShape { pub fn get_flags_section( engine_state_opt: Option<&EngineState>, + nu_config_opt: Option<&Config>, signature: &Signature, mut value_formatter: F, // format default Value (because some calls cant access config or nu-highlight) ) -> String @@ -467,13 +483,26 @@ where // Sometimes we want to get the flags without engine_state // For example, in nu-plugin. In that case, we fall back on default values if let Some(engine_state) = engine_state_opt { - help_section_name = - get_ansi_color_for_component_or_default(engine_state, "shape_string", "\x1b[32m"); // default: green - help_subcolor_one = - get_ansi_color_for_component_or_default(engine_state, "shape_external", "\x1b[36m"); // default: cyan - // was const bb: &str = "\x1b[1;34m"; // bold blue - help_subcolor_two = - get_ansi_color_for_component_or_default(engine_state, "shape_block", "\x1b[94m"); + let nu_config = nu_config_opt.unwrap_or_else(|| engine_state.get_config()); + help_section_name = get_ansi_color_for_component_or_default( + engine_state, + nu_config, + "shape_string", + "\x1b[32m", + ); // default: green + help_subcolor_one = get_ansi_color_for_component_or_default( + engine_state, + nu_config, + "shape_external", + "\x1b[36m", + ); // default: cyan + // was const bb: &str = "\x1b[1;34m"; // bold blue + help_subcolor_two = get_ansi_color_for_component_or_default( + engine_state, + nu_config, + "shape_block", + "\x1b[94m", + ); // default: light blue (nobold, should be bolding the *names*) } else { help_section_name = "\x1b[32m".to_string(); diff --git a/crates/nu-engine/src/env.rs b/crates/nu-engine/src/env.rs index 048d9bfb99..f93e78af88 100644 --- a/crates/nu-engine/src/env.rs +++ b/crates/nu-engine/src/env.rs @@ -1,9 +1,9 @@ use crate::ClosureEvalOnce; use nu_path::canonicalize_with; use nu_protocol::{ - ast::{Call, Expr}, - engine::{EngineState, Stack, StateWorkingSet}, - Config, ShellError, Span, Value, VarId, + ast::Expr, + engine::{Call, EngineState, Stack, StateWorkingSet}, + ShellError, Span, Value, VarId, }; use std::{ collections::HashMap, @@ -244,14 +244,15 @@ pub fn path_str( } pub const DIR_VAR_PARSER_INFO: &str = "dirs_var"; -pub fn get_dirs_var_from_call(call: &Call) -> Option { - call.get_parser_info(DIR_VAR_PARSER_INFO).and_then(|x| { - if let Expr::Var(id) = x.expr { - Some(id) - } else { - None - } - }) +pub fn get_dirs_var_from_call(stack: &Stack, call: &Call) -> Option { + call.get_parser_info(stack, DIR_VAR_PARSER_INFO) + .and_then(|x| { + if let Expr::Var(id) = x.expr { + Some(id) + } else { + None + } + }) } /// This helper function is used to find files during eval @@ -322,18 +323,6 @@ pub fn find_in_dirs_env( Ok(check_dir(lib_dirs).or_else(|| check_dir(lib_dirs_fallback))) } -/// Get config -/// -/// This combines config stored in permanent state and any runtime updates to the environment. This -/// is the canonical way to fetch config at runtime when you have Stack available. -pub fn get_config(engine_state: &EngineState, stack: &Stack) -> Config { - if let Some(mut config_record) = stack.get_env_var(engine_state, "config") { - config_record.parse_as_config(engine_state.get_config()).0 - } else { - engine_state.get_config().clone() - } -} - fn get_converted_value( engine_state: &EngineState, stack: &Stack, diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 044bf86980..42d0aed288 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -1,19 +1,20 @@ +use crate::eval_ir_block; #[allow(deprecated)] -use crate::{current_dir, get_config, get_full_help}; -use nu_path::expand_path_with; +use crate::{current_dir, get_full_help}; +use nu_path::{expand_path_with, AbsolutePathBuf}; use nu_protocol::{ ast::{ Assignment, Block, Call, Expr, Expression, ExternalArgument, PathMember, PipelineElement, PipelineRedirection, RedirectionSource, RedirectionTarget, }, debugger::DebugContext, - engine::{Closure, EngineState, Redirection, Stack}, + engine::{Closure, EngineState, Redirection, Stack, StateWorkingSet}, eval_base::Eval, ByteStreamSource, Config, FromValue, IntoPipelineData, OutDest, PipelineData, ShellError, Span, Spanned, Type, Value, VarId, ENV_VARIABLE_ID, }; use nu_utils::IgnoreCaseExt; -use std::{borrow::Cow, fs::OpenOptions, path::PathBuf}; +use std::{fs::OpenOptions, path::PathBuf, sync::Arc}; pub fn eval_call( engine_state: &EngineState, @@ -21,9 +22,7 @@ pub fn eval_call( call: &Call, input: PipelineData, ) -> Result { - if nu_utils::ctrl_c::was_pressed(&engine_state.ctrlc) { - return Ok(Value::nothing(call.head).into_pipeline_data()); - } + engine_state.signals().check(call.head)?; let decl = engine_state.get_decl(call.decl_id); if !decl.is_known_external() && call.named_iter().any(|(flag, _, _)| flag.item == "help") { @@ -176,7 +175,7 @@ pub fn eval_call( // We pass caller_stack here with the knowledge that internal commands // are going to be specifically looking for global state in the stack // rather than any local state. - decl.run(engine_state, caller_stack, call, input) + decl.run(engine_state, caller_stack, &call.into(), input) } } @@ -197,6 +196,9 @@ pub fn redirect_env(engine_state: &EngineState, caller_stack: &mut Stack, callee for (var, value) in callee_stack.get_stack_env_vars() { caller_stack.add_env_var(var, value); } + + // set config to callee config, to capture any updates to that + caller_stack.config = callee_stack.config.clone(); } fn eval_external( @@ -225,7 +227,7 @@ fn eval_external( } } - command.run(engine_state, stack, &call, input) + command.run(engine_state, stack, &(&call).into(), input) } pub fn eval_expression( @@ -509,6 +511,11 @@ pub fn eval_block( block: &Block, mut input: PipelineData, ) -> Result { + // Remove once IR is the default. + if stack.use_ir { + return eval_ir_block::(engine_state, stack, block, input); + } + D::enter_block(engine_state, block); let num_pipelines = block.len(); @@ -523,7 +530,7 @@ pub fn eval_block( for (i, element) in elements.iter().enumerate() { let next = elements.get(i + 1).unwrap_or(last); - let (next_out, next_err) = next.pipe_redirection(engine_state); + let (next_out, next_err) = next.pipe_redirection(&StateWorkingSet::new(engine_state)); let (stdout, stderr) = eval_element_redirection::( engine_state, stack, @@ -648,8 +655,8 @@ impl Eval for EvalRuntime { type MutState = Stack; - fn get_config<'a>(engine_state: Self::State<'a>, stack: &mut Stack) -> Cow<'a, Config> { - Cow::Owned(get_config(engine_state, stack)) + fn get_config(engine_state: Self::State<'_>, stack: &mut Stack) -> Arc { + stack.get_config(engine_state) } fn eval_filepath( @@ -681,7 +688,10 @@ impl Eval for EvalRuntime { } else if quoted { Ok(Value::string(path, span)) } else { - let cwd = engine_state.cwd(Some(stack)).unwrap_or_default(); + let cwd = engine_state + .cwd(Some(stack)) + .map(AbsolutePathBuf::into_std_path_buf) + .unwrap_or_default(); let path = expand_path_with(path, cwd, true); Ok(Value::string(path.to_string_lossy(), span)) @@ -836,7 +846,14 @@ impl Eval for EvalRuntime { }); } + let is_config = original_key == "config"; + stack.add_env_var(original_key, value); + + // Trigger the update to config, if we modified that. + if is_config { + stack.update_config(engine_state)?; + } } else { lhs.upsert_data_at_cell_path(&cell_path.tail, rhs)?; stack.add_var(*var_id, lhs); @@ -902,7 +919,7 @@ impl Eval for EvalRuntime { /// /// An automatic environment variable cannot be assigned to by user code. /// Current there are three of them: $env.PWD, $env.FILE_PWD, $env.CURRENT_FILE -fn is_automatic_env_var(var: &str) -> bool { +pub(crate) fn is_automatic_env_var(var: &str) -> bool { let names = ["PWD", "FILE_PWD", "CURRENT_FILE"]; names.iter().any(|&name| { if cfg!(windows) { diff --git a/crates/nu-engine/src/eval_helpers.rs b/crates/nu-engine/src/eval_helpers.rs index 66bda3e0eb..65ebc6b61d 100644 --- a/crates/nu-engine/src/eval_helpers.rs +++ b/crates/nu-engine/src/eval_helpers.rs @@ -1,6 +1,6 @@ use crate::{ eval_block, eval_block_with_early_return, eval_expression, eval_expression_with_input, - eval_subexpression, + eval_ir_block, eval_subexpression, }; use nu_protocol::{ ast::{Block, Expression}, @@ -13,6 +13,10 @@ use nu_protocol::{ pub type EvalBlockFn = fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result; +/// Type of eval_ir_block() function +pub type EvalIrBlockFn = + fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result; + /// Type of eval_block_with_early_return() function pub type EvalBlockWithEarlyReturnFn = fn(&EngineState, &mut Stack, &Block, PipelineData) -> Result; @@ -42,6 +46,16 @@ pub fn get_eval_block(engine_state: &EngineState) -> EvalBlockFn { } } +/// Helper function to fetch `eval_ir_block()` with the correct type parameter based on whether +/// engine_state is configured with or without a debugger. +pub fn get_eval_ir_block(engine_state: &EngineState) -> EvalIrBlockFn { + if engine_state.is_debugging() { + eval_ir_block:: + } else { + eval_ir_block:: + } +} + /// Helper function to fetch `eval_block_with_early_return()` with the correct type parameter based /// on whether engine_state is configured with or without a debugger. pub fn get_eval_block_with_early_return(engine_state: &EngineState) -> EvalBlockWithEarlyReturnFn { diff --git a/crates/nu-engine/src/eval_ir.rs b/crates/nu-engine/src/eval_ir.rs new file mode 100644 index 0000000000..29a677bd69 --- /dev/null +++ b/crates/nu-engine/src/eval_ir.rs @@ -0,0 +1,1475 @@ +use std::{borrow::Cow, fs::File, sync::Arc}; + +use nu_path::{expand_path_with, AbsolutePathBuf}; +use nu_protocol::{ + ast::{Bits, Block, Boolean, CellPath, Comparison, Math, Operator}, + 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, +}; +use nu_utils::IgnoreCaseExt; + +use crate::{eval::is_automatic_env_var, eval_block_with_early_return}; + +/// Evaluate the compiled representation of a [`Block`]. +pub fn eval_ir_block( + engine_state: &EngineState, + stack: &mut Stack, + block: &Block, + input: PipelineData, +) -> Result { + // Rust does not check recursion limits outside of const evaluation. + // But nu programs run in the same process as the shell. + // To prevent a stack overflow in user code from crashing the shell, + // we limit the recursion depth of function calls. + let maximum_call_stack_depth: u64 = engine_state.config.recursion_limit as u64; + if stack.recursion_count > maximum_call_stack_depth { + return Err(ShellError::RecursionLimitReached { + recursion_limit: maximum_call_stack_depth, + span: block.span, + }); + } + + if let Some(ir_block) = &block.ir_block { + D::enter_block(engine_state, block); + + let args_base = stack.arguments.get_base(); + let error_handler_base = stack.error_handlers.get_base(); + + // Allocate and initialize registers. I've found that it's not really worth trying to avoid + // the heap allocation here by reusing buffers - our allocator is fast enough + let mut registers = Vec::with_capacity(ir_block.register_count as usize); + for _ in 0..ir_block.register_count { + registers.push(PipelineData::Empty); + } + + // Initialize file storage. + let mut files = vec![None; ir_block.file_count as usize]; + + let result = eval_ir_block_impl::( + &mut EvalContext { + engine_state, + stack, + data: &ir_block.data, + block_span: &block.span, + args_base, + error_handler_base, + redirect_out: None, + redirect_err: None, + matches: vec![], + registers: &mut registers[..], + files: &mut files[..], + }, + ir_block, + input, + ); + + stack.error_handlers.leave_frame(error_handler_base); + stack.arguments.leave_frame(args_base); + + D::leave_block(engine_state, block); + + result + } else { + // FIXME blocks having IR should not be optional + Err(ShellError::GenericError { + error: "Can't evaluate block in IR mode".into(), + msg: "block is missing compiled representation".into(), + span: block.span, + help: Some("the IrBlock is probably missing due to a compilation error".into()), + inner: vec![], + }) + } +} + +/// All of the pointers necessary for evaluation +struct EvalContext<'a> { + engine_state: &'a EngineState, + stack: &'a mut Stack, + data: &'a Arc<[u8]>, + /// The span of the block + block_span: &'a Option, + /// Base index on the argument stack to reset to after a call + args_base: usize, + /// Base index on the error handler stack to reset to after a call + error_handler_base: usize, + /// State set by redirect-out + redirect_out: Option, + /// State set by redirect-err + redirect_err: Option, + /// Scratch space to use for `match` + matches: Vec<(VarId, Value)>, + /// Intermediate pipeline data storage used by instructions, indexed by RegId + registers: &'a mut [PipelineData], + /// Holds open files used by redirections + files: &'a mut [Option>], +} + +impl<'a> EvalContext<'a> { + /// Replace the contents of a register with a new value + #[inline] + fn put_reg(&mut self, reg_id: RegId, new_value: PipelineData) { + // log::trace!("{reg_id} <- {new_value:?}"); + self.registers[reg_id.0 as usize] = new_value; + } + + /// Borrow the contents of a register. + #[inline] + fn borrow_reg(&self, reg_id: RegId) -> &PipelineData { + &self.registers[reg_id.0 as usize] + } + + /// Replace the contents of a register with `Empty` and then return the value that it contained + #[inline] + fn take_reg(&mut self, reg_id: RegId) -> PipelineData { + // log::trace!("<- {reg_id}"); + std::mem::replace(&mut self.registers[reg_id.0 as usize], PipelineData::Empty) + } + + /// Clone data from a register. Must be collected first. + fn clone_reg(&mut self, reg_id: RegId, error_span: Span) -> Result { + match &self.registers[reg_id.0 as usize] { + PipelineData::Empty => Ok(PipelineData::Empty), + PipelineData::Value(val, meta) => Ok(PipelineData::Value(val.clone(), meta.clone())), + _ => Err(ShellError::IrEvalError { + msg: "Must collect to value before using instruction that clones from a register" + .into(), + span: Some(error_span), + }), + } + } + + /// Clone a value from a register. Must be collected first. + fn clone_reg_value(&mut self, reg_id: RegId, fallback_span: Span) -> Result { + match self.clone_reg(reg_id, fallback_span)? { + PipelineData::Empty => Ok(Value::nothing(fallback_span)), + PipelineData::Value(val, _) => Ok(val), + _ => unreachable!("clone_reg should never return stream data"), + } + } + + /// Take and implicitly collect a register to a value + fn collect_reg(&mut self, reg_id: RegId, fallback_span: Span) -> Result { + let data = self.take_reg(reg_id); + let span = data.span().unwrap_or(fallback_span); + data.into_value(span) + } + + /// Get a string from data or produce evaluation error if it's invalid UTF-8 + fn get_str(&self, slice: DataSlice, error_span: Span) -> Result<&'a str, ShellError> { + std::str::from_utf8(&self.data[slice]).map_err(|_| ShellError::IrEvalError { + msg: format!("data slice does not refer to valid UTF-8: {slice:?}"), + span: Some(error_span), + }) + } +} + +/// Eval an IR block on the provided slice of registers. +fn eval_ir_block_impl( + ctx: &mut EvalContext<'_>, + ir_block: &IrBlock, + input: PipelineData, +) -> Result { + if !ctx.registers.is_empty() { + ctx.registers[0] = input; + } + + // Program counter, starts at zero. + let mut pc = 0; + + while pc < ir_block.instructions.len() { + let instruction = &ir_block.instructions[pc]; + let span = &ir_block.spans[pc]; + let ast = &ir_block.ast[pc]; + + D::enter_instruction(ctx.engine_state, ir_block, pc, ctx.registers); + + let result = eval_instruction::(ctx, instruction, span, ast); + + D::leave_instruction( + ctx.engine_state, + ir_block, + pc, + ctx.registers, + result.as_ref().err(), + ); + + match result { + Ok(InstructionResult::Continue) => { + pc += 1; + } + Ok(InstructionResult::Branch(next_pc)) => { + pc = next_pc; + } + Ok(InstructionResult::Return(reg_id)) => { + return Ok(ctx.take_reg(reg_id)); + } + Ok(InstructionResult::ExitCode(exit_code)) => { + if let Some(error_handler) = ctx.stack.error_handlers.pop(ctx.error_handler_base) { + // If an error handler is set, branch there + prepare_error_handler(ctx, error_handler, None); + pc = error_handler.handler_index; + } else { + // If not, exit the block with the exit code + return Ok(PipelineData::new_external_stream_with_only_exit_code( + exit_code, + )); + } + } + Err( + err @ (ShellError::Return { .. } + | ShellError::Continue { .. } + | ShellError::Break { .. }), + ) => { + // These block control related errors should be passed through + return Err(err); + } + Err(err) => { + if let Some(error_handler) = ctx.stack.error_handlers.pop(ctx.error_handler_base) { + // If an error handler is set, branch there + prepare_error_handler(ctx, error_handler, Some(err.into_spanned(*span))); + pc = error_handler.handler_index; + } else { + // If not, exit the block with the error + return Err(err); + } + } + } + } + + // Fell out of the loop, without encountering a Return. + Err(ShellError::IrEvalError { + msg: format!( + "Program counter out of range (pc={pc}, len={len})", + len = ir_block.instructions.len(), + ), + span: *ctx.block_span, + }) +} + +/// Prepare the context for an error handler +fn prepare_error_handler( + ctx: &mut EvalContext<'_>, + error_handler: ErrorHandler, + error: Option>, +) { + if let Some(reg_id) = error_handler.error_register { + if let Some(error) = error { + // Create the error value and put it in the register + let value = Value::record( + record! { + "msg" => Value::string(format!("{}", error.item), error.span), + "debug" => Value::string(format!("{:?}", error.item), error.span), + "raw" => Value::error(error.item, error.span), + }, + error.span, + ); + ctx.put_reg(reg_id, PipelineData::Value(value, None)); + } else { + // Set the register to empty + ctx.put_reg(reg_id, PipelineData::Empty); + } + } +} + +/// The result of performing an instruction. Describes what should happen next +#[derive(Debug)] +enum InstructionResult { + Continue, + Branch(usize), + Return(RegId), + ExitCode(i32), +} + +/// Perform an instruction +fn eval_instruction( + ctx: &mut EvalContext<'_>, + instruction: &Instruction, + span: &Span, + ast: &Option, +) -> Result { + use self::InstructionResult::*; + + // See the docs for `Instruction` for more information on what these instructions are supposed + // to do. + match instruction { + Instruction::Unreachable => Err(ShellError::IrEvalError { + msg: "Reached unreachable code".into(), + span: Some(*span), + }), + Instruction::LoadLiteral { dst, lit } => load_literal(ctx, *dst, lit, *span), + Instruction::LoadValue { dst, val } => { + ctx.put_reg(*dst, Value::clone(val).into_pipeline_data()); + Ok(Continue) + } + Instruction::Move { dst, src } => { + let val = ctx.take_reg(*src); + ctx.put_reg(*dst, val); + Ok(Continue) + } + Instruction::Clone { dst, src } => { + let data = ctx.clone_reg(*src, *span)?; + ctx.put_reg(*dst, data); + Ok(Continue) + } + Instruction::Collect { src_dst } => { + let data = ctx.take_reg(*src_dst); + let value = collect(data, *span)?; + ctx.put_reg(*src_dst, value); + Ok(Continue) + } + Instruction::Span { src_dst } => { + let data = ctx.take_reg(*src_dst); + let spanned = data.with_span(*span); + ctx.put_reg(*src_dst, spanned); + Ok(Continue) + } + Instruction::Drop { src } => { + ctx.take_reg(*src); + Ok(Continue) + } + Instruction::Drain { src } => { + let data = ctx.take_reg(*src); + drain(ctx, data) + } + Instruction::LoadVariable { dst, var_id } => { + let value = get_var(ctx, *var_id, *span)?; + ctx.put_reg(*dst, value.into_pipeline_data()); + Ok(Continue) + } + Instruction::StoreVariable { var_id, src } => { + let value = ctx.collect_reg(*src, *span)?; + ctx.stack.add_var(*var_id, value); + Ok(Continue) + } + Instruction::LoadEnv { dst, key } => { + let key = ctx.get_str(*key, *span)?; + if let Some(value) = get_env_var_case_insensitive(ctx, key) { + let new_value = value.clone().into_pipeline_data(); + ctx.put_reg(*dst, new_value); + Ok(Continue) + } else { + // FIXME: using the same span twice, shouldn't this really be + // EnvVarNotFoundAtRuntime? There are tests that depend on CantFindColumn though... + Err(ShellError::CantFindColumn { + col_name: key.into(), + span: Some(*span), + src_span: *span, + }) + } + } + Instruction::LoadEnvOpt { dst, key } => { + let key = ctx.get_str(*key, *span)?; + let value = get_env_var_case_insensitive(ctx, key) + .cloned() + .unwrap_or(Value::nothing(*span)); + ctx.put_reg(*dst, value.into_pipeline_data()); + Ok(Continue) + } + Instruction::StoreEnv { key, src } => { + let key = ctx.get_str(*key, *span)?; + let value = ctx.collect_reg(*src, *span)?; + + let key = get_env_var_name_case_insensitive(ctx, key); + + if !is_automatic_env_var(&key) { + let is_config = key == "config"; + ctx.stack.add_env_var(key.into_owned(), value); + if is_config { + ctx.stack.update_config(ctx.engine_state)?; + } + Ok(Continue) + } else { + Err(ShellError::AutomaticEnvVarSetManually { + envvar_name: key.into(), + span: *span, + }) + } + } + Instruction::PushPositional { src } => { + let val = ctx.collect_reg(*src, *span)?.with_span(*span); + ctx.stack.arguments.push(Argument::Positional { + span: *span, + val, + ast: ast.clone().map(|ast_ref| ast_ref.0), + }); + Ok(Continue) + } + Instruction::AppendRest { src } => { + let vals = ctx.collect_reg(*src, *span)?.with_span(*span); + ctx.stack.arguments.push(Argument::Spread { + span: *span, + vals, + ast: ast.clone().map(|ast_ref| ast_ref.0), + }); + Ok(Continue) + } + Instruction::PushFlag { name } => { + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::Flag { + data, + name: *name, + short: DataSlice::empty(), + span: *span, + }); + Ok(Continue) + } + Instruction::PushShortFlag { short } => { + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::Flag { + data, + name: DataSlice::empty(), + short: *short, + span: *span, + }); + Ok(Continue) + } + Instruction::PushNamed { name, src } => { + let val = ctx.collect_reg(*src, *span)?.with_span(*span); + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::Named { + data, + name: *name, + short: DataSlice::empty(), + span: *span, + val, + ast: ast.clone().map(|ast_ref| ast_ref.0), + }); + Ok(Continue) + } + Instruction::PushShortNamed { short, src } => { + let val = ctx.collect_reg(*src, *span)?.with_span(*span); + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::Named { + data, + name: DataSlice::empty(), + short: *short, + span: *span, + val, + ast: ast.clone().map(|ast_ref| ast_ref.0), + }); + Ok(Continue) + } + Instruction::PushParserInfo { name, info } => { + let data = ctx.data.clone(); + ctx.stack.arguments.push(Argument::ParserInfo { + data, + name: *name, + info: info.clone(), + }); + Ok(Continue) + } + Instruction::RedirectOut { mode } => { + ctx.redirect_out = eval_redirection(ctx, mode, *span, RedirectionStream::Out)?; + Ok(Continue) + } + Instruction::RedirectErr { mode } => { + ctx.redirect_err = eval_redirection(ctx, mode, *span, RedirectionStream::Err)?; + Ok(Continue) + } + Instruction::CheckErrRedirected { src } => match ctx.borrow_reg(*src) { + PipelineData::ByteStream(stream, _) + if matches!(stream.source(), ByteStreamSource::Child(_)) => + { + Ok(Continue) + } + _ => Err(ShellError::GenericError { + error: "Can't redirect stderr of internal command output".into(), + msg: "piping stderr only works on external commands".into(), + span: Some(*span), + help: None, + inner: vec![], + }), + }, + Instruction::OpenFile { + file_num, + path, + append, + } => { + let path = ctx.collect_reg(*path, *span)?; + let file = open_file(ctx, &path, *append)?; + ctx.files[*file_num as usize] = Some(file); + Ok(Continue) + } + Instruction::WriteFile { file_num, src } => { + let src = ctx.take_reg(*src); + let file = ctx + .files + .get(*file_num as usize) + .cloned() + .flatten() + .ok_or_else(|| ShellError::IrEvalError { + msg: format!("Tried to write to file #{file_num}, but it is not open"), + span: Some(*span), + })?; + let result = { + let mut stack = ctx + .stack + .push_redirection(Some(Redirection::File(file)), None); + src.write_to_out_dests(ctx.engine_state, &mut stack)? + }; + // Abort execution if there's an exit code from a failed external + drain(ctx, result) + } + Instruction::CloseFile { file_num } => { + if ctx.files[*file_num as usize].take().is_some() { + Ok(Continue) + } else { + Err(ShellError::IrEvalError { + msg: format!("Tried to close file #{file_num}, but it is not open"), + span: Some(*span), + }) + } + } + Instruction::Call { decl_id, src_dst } => { + let input = ctx.take_reg(*src_dst); + let result = eval_call::(ctx, *decl_id, *span, input)?; + ctx.put_reg(*src_dst, result); + Ok(Continue) + } + Instruction::StringAppend { src_dst, val } => { + let string_value = ctx.collect_reg(*src_dst, *span)?; + let operand_value = ctx.collect_reg(*val, *span)?; + let string_span = string_value.span(); + + let mut string = string_value.into_string()?; + let operand = if let Value::String { val, .. } = operand_value { + // Small optimization, so we don't have to copy the string *again* + val + } else { + operand_value.to_expanded_string(", ", ctx.engine_state.get_config()) + }; + string.push_str(&operand); + + let new_string_value = Value::string(string, string_span); + ctx.put_reg(*src_dst, new_string_value.into_pipeline_data()); + Ok(Continue) + } + Instruction::GlobFrom { src_dst, no_expand } => { + let string_value = ctx.collect_reg(*src_dst, *span)?; + let glob_value = if matches!(string_value, Value::Glob { .. }) { + // It already is a glob, so don't touch it. + string_value + } else { + // Treat it as a string, then cast + let string = string_value.into_string()?; + Value::glob(string, *no_expand, *span) + }; + ctx.put_reg(*src_dst, glob_value.into_pipeline_data()); + Ok(Continue) + } + Instruction::ListPush { src_dst, item } => { + let list_value = ctx.collect_reg(*src_dst, *span)?; + let item = ctx.collect_reg(*item, *span)?; + let list_span = list_value.span(); + let mut list = list_value.into_list()?; + list.push(item); + ctx.put_reg(*src_dst, Value::list(list, list_span).into_pipeline_data()); + Ok(Continue) + } + Instruction::ListSpread { src_dst, items } => { + let list_value = ctx.collect_reg(*src_dst, *span)?; + let items = ctx.collect_reg(*items, *span)?; + let list_span = list_value.span(); + let items_span = items.span(); + let mut list = list_value.into_list()?; + list.extend( + items + .into_list() + .map_err(|_| ShellError::CannotSpreadAsList { span: items_span })?, + ); + ctx.put_reg(*src_dst, Value::list(list, list_span).into_pipeline_data()); + Ok(Continue) + } + Instruction::RecordInsert { src_dst, key, val } => { + let record_value = ctx.collect_reg(*src_dst, *span)?; + let key = ctx.collect_reg(*key, *span)?; + let val = ctx.collect_reg(*val, *span)?; + let record_span = record_value.span(); + let mut record = record_value.into_record()?; + + let key = key.coerce_into_string()?; + if let Some(old_value) = record.insert(&key, val) { + return Err(ShellError::ColumnDefinedTwice { + col_name: key, + second_use: *span, + first_use: old_value.span(), + }); + } + + ctx.put_reg( + *src_dst, + Value::record(record, record_span).into_pipeline_data(), + ); + Ok(Continue) + } + Instruction::RecordSpread { src_dst, items } => { + let record_value = ctx.collect_reg(*src_dst, *span)?; + let items = ctx.collect_reg(*items, *span)?; + let record_span = record_value.span(); + let items_span = items.span(); + let mut record = record_value.into_record()?; + // Not using .extend() here because it doesn't handle duplicates + for (key, val) in items + .into_record() + .map_err(|_| ShellError::CannotSpreadAsRecord { span: items_span })? + { + if let Some(first_value) = record.insert(&key, val) { + return Err(ShellError::ColumnDefinedTwice { + col_name: key, + second_use: *span, + first_use: first_value.span(), + }); + } + } + ctx.put_reg( + *src_dst, + Value::record(record, record_span).into_pipeline_data(), + ); + Ok(Continue) + } + Instruction::Not { src_dst } => { + let bool = ctx.collect_reg(*src_dst, *span)?; + let negated = !bool.as_bool()?; + ctx.put_reg( + *src_dst, + Value::bool(negated, bool.span()).into_pipeline_data(), + ); + Ok(Continue) + } + Instruction::BinaryOp { lhs_dst, op, rhs } => binary_op(ctx, *lhs_dst, op, *rhs, *span), + Instruction::FollowCellPath { src_dst, path } => { + let data = ctx.take_reg(*src_dst); + let path = ctx.take_reg(*path); + if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path { + let value = data.follow_cell_path(&path.members, *span, true)?; + ctx.put_reg(*src_dst, value.into_pipeline_data()); + Ok(Continue) + } else if let PipelineData::Value(Value::Error { error, .. }, _) = path { + Err(*error) + } else { + Err(ShellError::TypeMismatch { + err_message: "expected cell path".into(), + span: path.span().unwrap_or(*span), + }) + } + } + Instruction::CloneCellPath { dst, src, path } => { + let value = ctx.clone_reg_value(*src, *span)?; + let path = ctx.take_reg(*path); + if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path { + // TODO: make follow_cell_path() not have to take ownership, probably using Cow + let value = value.follow_cell_path(&path.members, true)?; + ctx.put_reg(*dst, value.into_pipeline_data()); + Ok(Continue) + } else if let PipelineData::Value(Value::Error { error, .. }, _) = path { + Err(*error) + } else { + Err(ShellError::TypeMismatch { + err_message: "expected cell path".into(), + span: path.span().unwrap_or(*span), + }) + } + } + Instruction::UpsertCellPath { + src_dst, + path, + new_value, + } => { + let data = ctx.take_reg(*src_dst); + let metadata = data.metadata(); + // Change the span because we're modifying it + let mut value = data.into_value(*span)?; + let path = ctx.take_reg(*path); + let new_value = ctx.collect_reg(*new_value, *span)?; + if let PipelineData::Value(Value::CellPath { val: path, .. }, _) = path { + value.upsert_data_at_cell_path(&path.members, new_value)?; + ctx.put_reg(*src_dst, value.into_pipeline_data_with_metadata(metadata)); + Ok(Continue) + } else if let PipelineData::Value(Value::Error { error, .. }, _) = path { + Err(*error) + } else { + Err(ShellError::TypeMismatch { + err_message: "expected cell path".into(), + span: path.span().unwrap_or(*span), + }) + } + } + Instruction::Jump { index } => Ok(Branch(*index)), + Instruction::BranchIf { cond, index } => { + let data = ctx.take_reg(*cond); + let data_span = data.span(); + let val = match data { + PipelineData::Value(Value::Bool { val, .. }, _) => val, + PipelineData::Value(Value::Error { error, .. }, _) => { + return Err(*error); + } + _ => { + return Err(ShellError::TypeMismatch { + err_message: "expected bool".into(), + span: data_span.unwrap_or(*span), + }); + } + }; + if val { + Ok(Branch(*index)) + } else { + Ok(Continue) + } + } + Instruction::BranchIfEmpty { src, index } => { + let is_empty = matches!( + ctx.borrow_reg(*src), + PipelineData::Empty | PipelineData::Value(Value::Nothing { .. }, _) + ); + + if is_empty { + Ok(Branch(*index)) + } else { + Ok(Continue) + } + } + Instruction::Match { + pattern, + src, + index, + } => { + let value = ctx.clone_reg_value(*src, *span)?; + ctx.matches.clear(); + if pattern.match_value(&value, &mut ctx.matches) { + // Match succeeded: set variables and branch + for (var_id, match_value) in ctx.matches.drain(..) { + ctx.stack.add_var(var_id, match_value); + } + Ok(Branch(*index)) + } else { + // Failed to match, put back original value + ctx.matches.clear(); + Ok(Continue) + } + } + Instruction::CheckMatchGuard { src } => { + if matches!( + ctx.borrow_reg(*src), + PipelineData::Value(Value::Bool { .. }, _) + ) { + Ok(Continue) + } else { + Err(ShellError::MatchGuardNotBool { span: *span }) + } + } + Instruction::Iterate { + dst, + stream, + end_index, + } => eval_iterate(ctx, *dst, *stream, *end_index), + Instruction::OnError { index } => { + ctx.stack.error_handlers.push(ErrorHandler { + handler_index: *index, + error_register: None, + }); + Ok(Continue) + } + Instruction::OnErrorInto { index, dst } => { + ctx.stack.error_handlers.push(ErrorHandler { + handler_index: *index, + error_register: Some(*dst), + }); + Ok(Continue) + } + Instruction::PopErrorHandler => { + ctx.stack.error_handlers.pop(ctx.error_handler_base); + Ok(Continue) + } + Instruction::CheckExternalFailed { dst, src } => { + let data = ctx.take_reg(*src); + let (data, failed) = data.check_external_failed()?; + ctx.put_reg(*src, data); + ctx.put_reg(*dst, Value::bool(failed, *span).into_pipeline_data()); + Ok(Continue) + } + Instruction::ReturnEarly { src } => { + let val = ctx.collect_reg(*src, *span)?; + Err(ShellError::Return { + span: *span, + value: Box::new(val), + }) + } + Instruction::Return { src } => Ok(Return(*src)), + } +} + +/// Load a literal value into a register +fn load_literal( + ctx: &mut EvalContext<'_>, + dst: RegId, + lit: &Literal, + span: Span, +) -> Result { + let value = literal_value(ctx, lit, span)?; + ctx.put_reg(dst, PipelineData::Value(value, None)); + Ok(InstructionResult::Continue) +} + +fn literal_value( + ctx: &mut EvalContext<'_>, + lit: &Literal, + span: Span, +) -> Result { + Ok(match lit { + Literal::Bool(b) => Value::bool(*b, span), + Literal::Int(i) => Value::int(*i, span), + Literal::Float(f) => Value::float(*f, span), + Literal::Filesize(q) => Value::filesize(*q, span), + Literal::Duration(q) => Value::duration(*q, span), + Literal::Binary(bin) => Value::binary(&ctx.data[*bin], span), + Literal::Block(block_id) | Literal::RowCondition(block_id) | Literal::Closure(block_id) => { + let block = ctx.engine_state.get_block(*block_id); + let captures = block + .captures + .iter() + .map(|var_id| get_var(ctx, *var_id, span).map(|val| (*var_id, val))) + .collect::, ShellError>>()?; + Value::closure( + Closure { + block_id: *block_id, + captures, + }, + span, + ) + } + Literal::Range { + start, + step, + end, + inclusion, + } => { + let start = ctx.collect_reg(*start, span)?; + let step = ctx.collect_reg(*step, span)?; + let end = ctx.collect_reg(*end, span)?; + let range = Range::new(start, step, end, *inclusion, span)?; + Value::range(range, span) + } + Literal::List { capacity } => Value::list(Vec::with_capacity(*capacity), span), + Literal::Record { capacity } => Value::record(Record::with_capacity(*capacity), span), + Literal::Filepath { + val: path, + no_expand, + } => { + let path = ctx.get_str(*path, span)?; + if *no_expand { + Value::string(path, span) + } else { + let cwd = ctx.engine_state.cwd(Some(ctx.stack))?; + let path = expand_path_with(path, cwd, true); + + Value::string(path.to_string_lossy(), span) + } + } + Literal::Directory { + val: path, + no_expand, + } => { + let path = ctx.get_str(*path, span)?; + if path == "-" { + Value::string("-", span) + } else if *no_expand { + Value::string(path, span) + } else { + let cwd = ctx + .engine_state + .cwd(Some(ctx.stack)) + .map(AbsolutePathBuf::into_std_path_buf) + .unwrap_or_default(); + let path = expand_path_with(path, cwd, true); + + Value::string(path.to_string_lossy(), span) + } + } + Literal::GlobPattern { val, no_expand } => { + Value::glob(ctx.get_str(*val, span)?, *no_expand, span) + } + Literal::String(s) => Value::string(ctx.get_str(*s, span)?, span), + Literal::RawString(s) => Value::string(ctx.get_str(*s, span)?, span), + Literal::CellPath(path) => Value::cell_path(CellPath::clone(path), span), + Literal::Date(dt) => Value::date(**dt, span), + Literal::Nothing => Value::nothing(span), + }) +} + +fn binary_op( + ctx: &mut EvalContext<'_>, + lhs_dst: RegId, + op: &Operator, + rhs: RegId, + span: Span, +) -> Result { + let lhs_val = ctx.collect_reg(lhs_dst, span)?; + let rhs_val = ctx.collect_reg(rhs, span)?; + + // Handle binary op errors early + if let Value::Error { error, .. } = lhs_val { + return Err(*error); + } + if let Value::Error { error, .. } = rhs_val { + return Err(*error); + } + + // We only have access to one span here, but the generated code usually adds a `span` + // instruction to set the output span to the right span. + let op_span = span; + + let result = match op { + Operator::Comparison(cmp) => match cmp { + Comparison::Equal => lhs_val.eq(op_span, &rhs_val, span)?, + Comparison::NotEqual => lhs_val.ne(op_span, &rhs_val, span)?, + Comparison::LessThan => lhs_val.lt(op_span, &rhs_val, span)?, + Comparison::GreaterThan => lhs_val.gt(op_span, &rhs_val, span)?, + Comparison::LessThanOrEqual => lhs_val.lte(op_span, &rhs_val, span)?, + Comparison::GreaterThanOrEqual => lhs_val.gte(op_span, &rhs_val, span)?, + Comparison::RegexMatch => { + lhs_val.regex_match(ctx.engine_state, op_span, &rhs_val, false, span)? + } + Comparison::NotRegexMatch => { + lhs_val.regex_match(ctx.engine_state, op_span, &rhs_val, true, span)? + } + Comparison::In => lhs_val.r#in(op_span, &rhs_val, span)?, + Comparison::NotIn => lhs_val.not_in(op_span, &rhs_val, span)?, + Comparison::StartsWith => lhs_val.starts_with(op_span, &rhs_val, span)?, + Comparison::EndsWith => lhs_val.ends_with(op_span, &rhs_val, span)?, + }, + Operator::Math(mat) => match mat { + Math::Plus => lhs_val.add(op_span, &rhs_val, span)?, + Math::Append => lhs_val.append(op_span, &rhs_val, span)?, + Math::Minus => lhs_val.sub(op_span, &rhs_val, span)?, + Math::Multiply => lhs_val.mul(op_span, &rhs_val, span)?, + Math::Divide => lhs_val.div(op_span, &rhs_val, span)?, + Math::Modulo => lhs_val.modulo(op_span, &rhs_val, span)?, + Math::FloorDivision => lhs_val.floor_div(op_span, &rhs_val, span)?, + Math::Pow => lhs_val.pow(op_span, &rhs_val, span)?, + }, + Operator::Boolean(bl) => match bl { + Boolean::And => lhs_val.and(op_span, &rhs_val, span)?, + Boolean::Or => lhs_val.or(op_span, &rhs_val, span)?, + Boolean::Xor => lhs_val.xor(op_span, &rhs_val, span)?, + }, + Operator::Bits(bit) => match bit { + Bits::BitOr => lhs_val.bit_or(op_span, &rhs_val, span)?, + Bits::BitXor => lhs_val.bit_xor(op_span, &rhs_val, span)?, + Bits::BitAnd => lhs_val.bit_and(op_span, &rhs_val, span)?, + Bits::ShiftLeft => lhs_val.bit_shl(op_span, &rhs_val, span)?, + Bits::ShiftRight => lhs_val.bit_shr(op_span, &rhs_val, span)?, + }, + Operator::Assignment(_asg) => { + return Err(ShellError::IrEvalError { + msg: "can't eval assignment with the `binary-op` instruction".into(), + span: Some(span), + }) + } + }; + + ctx.put_reg(lhs_dst, PipelineData::Value(result, None)); + + Ok(InstructionResult::Continue) +} + +/// Evaluate a call +fn eval_call( + ctx: &mut EvalContext<'_>, + decl_id: DeclId, + head: Span, + input: PipelineData, +) -> Result { + let EvalContext { + engine_state, + stack: caller_stack, + args_base, + redirect_out, + redirect_err, + .. + } = ctx; + + let args_len = caller_stack.arguments.get_len(*args_base); + let decl = engine_state.get_decl(decl_id); + + // Set up redirect modes + let mut caller_stack = caller_stack.push_redirection(redirect_out.take(), redirect_err.take()); + + let result; + + if let Some(block_id) = decl.block_id() { + // If the decl is a custom command + let block = engine_state.get_block(block_id); + + // Set up a callee stack with the captures and move arguments from the stack into variables + let mut callee_stack = caller_stack.gather_captures(engine_state, &block.captures); + + gather_arguments( + engine_state, + block, + &mut caller_stack, + &mut callee_stack, + *args_base, + args_len, + head, + )?; + + // Add one to the recursion count, so we don't recurse too deep. Stack overflows are not + // recoverable in Rust. + callee_stack.recursion_count += 1; + + result = eval_block_with_early_return::(engine_state, &mut callee_stack, block, input); + + // Move environment variables back into the caller stack scope if requested to do so + if block.redirect_env { + redirect_env(engine_state, &mut caller_stack, &callee_stack); + } + } else { + // FIXME: precalculate this and save it somewhere + let span = Span::merge_many( + std::iter::once(head).chain( + caller_stack + .arguments + .get_args(*args_base, args_len) + .iter() + .flat_map(|arg| arg.span()), + ), + ); + + let call = Call { + decl_id, + head, + span, + args_base: *args_base, + args_len, + }; + + // Run the call + result = decl.run(engine_state, &mut caller_stack, &(&call).into(), input); + }; + + drop(caller_stack); + + // Important that this runs, to reset state post-call: + ctx.stack.arguments.leave_frame(ctx.args_base); + ctx.redirect_out = None; + ctx.redirect_err = None; + + result +} + +fn find_named_var_id( + sig: &Signature, + name: &[u8], + short: &[u8], + span: Span, +) -> Result { + sig.named + .iter() + .find(|n| { + if !n.long.is_empty() { + n.long.as_bytes() == name + } else { + // It's possible to only have a short name and no long name + n.short + .is_some_and(|s| s.encode_utf8(&mut [0; 4]).as_bytes() == short) + } + }) + .ok_or_else(|| ShellError::IrEvalError { + msg: format!( + "block does not have an argument named `{}`", + String::from_utf8_lossy(name) + ), + span: Some(span), + }) + .and_then(|flag| expect_named_var_id(flag, span)) +} + +fn expect_named_var_id(arg: &Flag, span: Span) -> Result { + arg.var_id.ok_or_else(|| ShellError::IrEvalError { + msg: format!( + "block signature is missing var id for named arg `{}`", + arg.long + ), + span: Some(span), + }) +} + +fn expect_positional_var_id(arg: &PositionalArg, span: Span) -> Result { + arg.var_id.ok_or_else(|| ShellError::IrEvalError { + msg: format!( + "block signature is missing var id for positional arg `{}`", + arg.name + ), + span: Some(span), + }) +} + +/// Move arguments from the stack into variables for a custom command +fn gather_arguments( + engine_state: &EngineState, + block: &Block, + caller_stack: &mut Stack, + callee_stack: &mut Stack, + args_base: usize, + args_len: usize, + call_head: Span, +) -> Result<(), ShellError> { + let mut positional_iter = block + .signature + .required_positional + .iter() + .map(|p| (p, true)) + .chain( + block + .signature + .optional_positional + .iter() + .map(|p| (p, false)), + ); + + // Arguments that didn't get consumed by required/optional + let mut rest = vec![]; + + // If we encounter a spread, all further positionals should go to rest + let mut always_spread = false; + + for arg in caller_stack.arguments.drain_args(args_base, args_len) { + match arg { + Argument::Positional { span, val, .. } => { + // Don't check next positional arg if we encountered a spread previously + let next = (!always_spread).then(|| positional_iter.next()).flatten(); + if let Some((positional_arg, required)) = next { + let var_id = expect_positional_var_id(positional_arg, span)?; + if required { + // By checking the type of the bound variable rather than converting the + // SyntaxShape here, we might be able to save some allocations and effort + let variable = engine_state.get_var(var_id); + check_type(&val, &variable.ty)?; + } + callee_stack.add_var(var_id, val); + } else { + rest.push(val); + } + } + Argument::Spread { vals, .. } => { + if let Value::List { vals, .. } = vals { + rest.extend(vals); + // All further positional args should go to spread + always_spread = true; + } else if let Value::Error { error, .. } = vals { + return Err(*error); + } else { + return Err(ShellError::CannotSpreadAsList { span: vals.span() }); + } + } + Argument::Flag { + data, + name, + short, + span, + } => { + let var_id = find_named_var_id(&block.signature, &data[name], &data[short], span)?; + callee_stack.add_var(var_id, Value::bool(true, span)) + } + Argument::Named { + data, + name, + short, + span, + val, + .. + } => { + let var_id = find_named_var_id(&block.signature, &data[name], &data[short], span)?; + callee_stack.add_var(var_id, val) + } + Argument::ParserInfo { .. } => (), + } + } + + // Add the collected rest of the arguments if a spread argument exists + if let Some(rest_arg) = &block.signature.rest_positional { + let rest_span = rest.first().map(|v| v.span()).unwrap_or(call_head); + let var_id = expect_positional_var_id(rest_arg, rest_span)?; + callee_stack.add_var(var_id, Value::list(rest, rest_span)); + } + + // Check for arguments that haven't yet been set and set them to their defaults + for (positional_arg, _) in positional_iter { + let var_id = expect_positional_var_id(positional_arg, call_head)?; + callee_stack.add_var( + var_id, + positional_arg + .default_value + .clone() + .unwrap_or(Value::nothing(call_head)), + ); + } + + for named_arg in &block.signature.named { + if let Some(var_id) = named_arg.var_id { + // For named arguments, we do this check by looking to see if the variable was set yet on + // the stack. This assumes that the stack's variables was previously empty, but that's a + // fair assumption for a brand new callee stack. + if !callee_stack.vars.iter().any(|(id, _)| *id == var_id) { + let val = if named_arg.arg.is_none() { + Value::bool(false, call_head) + } else if let Some(value) = &named_arg.default_value { + value.clone() + } else { + Value::nothing(call_head) + }; + callee_stack.add_var(var_id, val); + } + } + } + + Ok(()) +} + +/// Type check helper. Produces `CantConvert` error if `val` is not compatible with `ty`. +fn check_type(val: &Value, ty: &Type) -> Result<(), ShellError> { + if match val { + // An empty list is compatible with any list or table type + Value::List { vals, .. } if vals.is_empty() => { + matches!(ty, Type::Any | Type::List(_) | Type::Table(_)) + } + // FIXME: the allocation that might be required here is not great, it would be nice to be + // able to just directly check whether a value is compatible with a type + _ => val.get_type().is_subtype(ty), + } { + Ok(()) + } else { + Err(ShellError::CantConvert { + to_type: ty.to_string(), + from_type: val.get_type().to_string(), + span: val.span(), + help: None, + }) + } +} + +/// Get variable from [`Stack`] or [`EngineState`] +fn get_var(ctx: &EvalContext<'_>, var_id: VarId, span: Span) -> Result { + match var_id { + // $env + ENV_VARIABLE_ID => { + let env_vars = ctx.stack.get_env_vars(ctx.engine_state); + let env_columns = env_vars.keys(); + let env_values = env_vars.values(); + + let mut pairs = env_columns + .map(|x| x.to_string()) + .zip(env_values.cloned()) + .collect::>(); + + pairs.sort_by(|a, b| a.0.cmp(&b.0)); + + Ok(Value::record(pairs.into_iter().collect(), span)) + } + _ => ctx.stack.get_var(var_id, span).or_else(|err| { + // $nu is handled by getting constant + if let Some(const_val) = ctx.engine_state.get_constant(var_id).cloned() { + Ok(const_val.with_span(span)) + } else { + Err(err) + } + }), + } +} + +/// Get an environment variable, case-insensitively +fn get_env_var_case_insensitive<'a>(ctx: &'a mut EvalContext<'_>, key: &str) -> Option<&'a Value> { + // Read scopes in order + ctx.stack + .env_vars + .iter() + .rev() + .chain(std::iter::once(&ctx.engine_state.env_vars)) + .flat_map(|overlays| { + // Read overlays in order + ctx.stack + .active_overlays + .iter() + .rev() + .filter_map(|name| overlays.get(name)) + }) + .find_map(|map| { + // Use the hashmap first to try to be faster? + map.get(key).or_else(|| { + // Check to see if it exists at all in the map + map.iter() + .find_map(|(k, v)| k.eq_ignore_case(key).then_some(v)) + }) + }) +} + +/// Get the existing name of an environment variable, case-insensitively. This is used to implement +/// case preservation of environment variables, so that changing an environment variable that +/// already exists always uses the same case. +fn get_env_var_name_case_insensitive<'a>(ctx: &mut EvalContext<'_>, key: &'a str) -> Cow<'a, str> { + // Read scopes in order + ctx.stack + .env_vars + .iter() + .rev() + .chain(std::iter::once(&ctx.engine_state.env_vars)) + .flat_map(|overlays| { + // Read overlays in order + ctx.stack + .active_overlays + .iter() + .rev() + .filter_map(|name| overlays.get(name)) + }) + .find_map(|map| { + // Use the hashmap first to try to be faster? + if map.contains_key(key) { + Some(Cow::Borrowed(key)) + } else { + map.keys().find(|k| k.eq_ignore_case(key)).map(|k| { + // it exists, but with a different case + Cow::Owned(k.to_owned()) + }) + } + }) + // didn't exist. + .unwrap_or(Cow::Borrowed(key)) +} + +/// Helper to collect values into [`PipelineData`], preserving original span and metadata +fn collect(data: PipelineData, fallback_span: Span) -> Result { + let span = data.span().unwrap_or(fallback_span); + let metadata = data.metadata(); + let value = data.into_value(span)?; + Ok(PipelineData::Value(value, metadata)) +} + +/// Helper for drain behavior. Returns `Ok(ExitCode)` on failed external. +fn drain(ctx: &mut EvalContext<'_>, data: PipelineData) -> Result { + use self::InstructionResult::*; + let span = data.span().unwrap_or(Span::unknown()); + if let Some(exit_status) = data.drain()? { + ctx.stack.add_env_var( + "LAST_EXIT_CODE".into(), + Value::int(exit_status.code() as i64, span), + ); + if exit_status.code() == 0 { + Ok(Continue) + } else { + Ok(ExitCode(exit_status.code())) + } + } else { + Ok(Continue) + } +} + +enum RedirectionStream { + Out, + Err, +} + +/// Open a file for redirection +fn open_file(ctx: &EvalContext<'_>, path: &Value, append: bool) -> Result, ShellError> { + let path_expanded = + expand_path_with(path.as_str()?, ctx.engine_state.cwd(Some(ctx.stack))?, true); + let mut options = File::options(); + if append { + options.append(true); + } else { + options.write(true).truncate(true); + } + let file = options + .create(true) + .open(path_expanded) + .err_span(path.span())?; + Ok(Arc::new(file)) +} + +/// Set up a [`Redirection`] from a [`RedirectMode`] +fn eval_redirection( + ctx: &mut EvalContext<'_>, + mode: &RedirectMode, + span: Span, + which: RedirectionStream, +) -> Result, ShellError> { + match mode { + RedirectMode::Pipe => Ok(Some(Redirection::Pipe(OutDest::Pipe))), + RedirectMode::Capture => Ok(Some(Redirection::Pipe(OutDest::Capture))), + RedirectMode::Null => Ok(Some(Redirection::Pipe(OutDest::Null))), + RedirectMode::Inherit => Ok(Some(Redirection::Pipe(OutDest::Inherit))), + RedirectMode::File { file_num } => { + let file = ctx + .files + .get(*file_num as usize) + .cloned() + .flatten() + .ok_or_else(|| ShellError::IrEvalError { + msg: format!("Tried to redirect to file #{file_num}, but it is not open"), + span: Some(span), + })?; + Ok(Some(Redirection::File(file))) + } + RedirectMode::Caller => Ok(match which { + RedirectionStream::Out => ctx.stack.pipe_stdout().cloned().map(Redirection::Pipe), + RedirectionStream::Err => ctx.stack.pipe_stderr().cloned().map(Redirection::Pipe), + }), + } +} + +/// Do an `iterate` instruction. This can be called repeatedly to get more values from an iterable +fn eval_iterate( + ctx: &mut EvalContext<'_>, + dst: RegId, + stream: RegId, + end_index: usize, +) -> Result { + let mut data = ctx.take_reg(stream); + if let PipelineData::ListStream(list_stream, _) = &mut data { + // Modify the stream, taking one value off, and branching if it's empty + if let Some(val) = list_stream.next_value() { + ctx.put_reg(dst, val.into_pipeline_data()); + ctx.put_reg(stream, data); // put the stream back so it can be iterated on again + Ok(InstructionResult::Continue) + } else { + ctx.put_reg(dst, PipelineData::Empty); + Ok(InstructionResult::Branch(end_index)) + } + } else { + // Convert the PipelineData to an iterator, and wrap it in a ListStream so it can be + // iterated on + let metadata = data.metadata(); + let span = data.span().unwrap_or(Span::unknown()); + ctx.put_reg( + stream, + PipelineData::ListStream( + ListStream::new(data.into_iter(), span, Signals::EMPTY), + metadata, + ), + ); + eval_iterate(ctx, dst, stream, end_index) + } +} + +/// Redirect environment from the callee stack to the caller stack +fn redirect_env(engine_state: &EngineState, caller_stack: &mut Stack, callee_stack: &Stack) { + // TODO: make this more efficient + // Grab all environment variables from the callee + let caller_env_vars = caller_stack.get_env_var_names(engine_state); + + // remove env vars that are present in the caller but not in the callee + // (the callee hid them) + for var in caller_env_vars.iter() { + if !callee_stack.has_env_var(engine_state, var) { + caller_stack.remove_env_var(engine_state, var); + } + } + + // add new env vars from callee to caller + for (var, value) in callee_stack.get_stack_env_vars() { + caller_stack.add_env_var(var, value); + } +} diff --git a/crates/nu-engine/src/lib.rs b/crates/nu-engine/src/lib.rs index e3c8f8eede..5a5bb9a5d0 100644 --- a/crates/nu-engine/src/lib.rs +++ b/crates/nu-engine/src/lib.rs @@ -1,17 +1,21 @@ +#![doc = include_str!("../README.md")] mod call_ext; mod closure_eval; pub mod column; pub mod command_prelude; +mod compile; pub mod documentation; pub mod env; mod eval; mod eval_helpers; +mod eval_ir; mod glob_from; pub mod scope; pub use call_ext::CallExt; pub use closure_eval::*; pub use column::get_columns; +pub use compile::compile; pub use documentation::get_full_help; pub use env::*; pub use eval::{ @@ -19,4 +23,5 @@ pub use eval::{ eval_expression_with_input, eval_subexpression, eval_variable, redirect_env, }; pub use eval_helpers::*; +pub use eval_ir::eval_ir_block; pub use glob_from::glob_from; diff --git a/crates/nu-explore/README.md b/crates/nu-explore/README.md new file mode 100644 index 0000000000..d18a576c43 --- /dev/null +++ b/crates/nu-explore/README.md @@ -0,0 +1,5 @@ +Implementation of the interactive `explore` command pager. + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-explore/src/commands/expand.rs b/crates/nu-explore/src/commands/expand.rs index d7d75af6c5..38f92c0b50 100644 --- a/crates/nu-explore/src/commands/expand.rs +++ b/crates/nu-explore/src/commands/expand.rs @@ -1,7 +1,7 @@ use super::ViewCommand; use crate::{ nu_common::{self, collect_input}, - views::Preview, + views::{Preview, ViewConfig}, }; use anyhow::Result; use nu_color_config::StyleComputer; @@ -43,6 +43,7 @@ impl ViewCommand for ExpandCmd { engine_state: &EngineState, stack: &mut Stack, value: Option, + _: &ViewConfig, ) -> Result { if let Some(value) = value { let value_as_string = convert_value_to_string(value, engine_state, stack)?; @@ -63,15 +64,14 @@ fn convert_value_to_string( let has_no_head = cols.is_empty() || (cols.len() == 1 && cols[0].is_empty()); let has_single_value = vals.len() == 1 && vals[0].len() == 1; if !has_no_head && has_single_value { - let config = engine_state.get_config(); - Ok(vals[0][0].to_abbreviated_string(config)) + let config = stack.get_config(engine_state); + Ok(vals[0][0].to_abbreviated_string(&config)) } else { - let ctrlc = engine_state.ctrlc.clone(); let config = engine_state.get_config(); let style_computer = StyleComputer::from_config(engine_state, stack); Ok(nu_common::try_build_table( - ctrlc, + engine_state.signals(), config, &style_computer, value, diff --git a/crates/nu-explore/src/commands/help.rs b/crates/nu-explore/src/commands/help.rs index 8be468383e..684e713939 100644 --- a/crates/nu-explore/src/commands/help.rs +++ b/crates/nu-explore/src/commands/help.rs @@ -1,5 +1,5 @@ use super::ViewCommand; -use crate::views::Preview; +use crate::views::{Preview, ViewConfig}; use anyhow::Result; use nu_ansi_term::Color; use nu_protocol::{ @@ -99,7 +99,13 @@ impl ViewCommand for HelpCmd { Ok(()) } - fn spawn(&mut self, _: &EngineState, _: &mut Stack, _: Option) -> Result { + fn spawn( + &mut self, + _: &EngineState, + _: &mut Stack, + _: Option, + _: &ViewConfig, + ) -> Result { Ok(HelpCmd::view()) } } diff --git a/crates/nu-explore/src/commands/mod.rs b/crates/nu-explore/src/commands/mod.rs index 141dc25f56..a748ddaaa3 100644 --- a/crates/nu-explore/src/commands/mod.rs +++ b/crates/nu-explore/src/commands/mod.rs @@ -1,3 +1,5 @@ +use crate::views::ViewConfig; + use super::pager::{Pager, Transition}; use anyhow::Result; use nu_protocol::{ @@ -49,5 +51,6 @@ pub trait ViewCommand { engine_state: &EngineState, stack: &mut Stack, value: Option, + config: &ViewConfig, ) -> Result; } diff --git a/crates/nu-explore/src/commands/nu.rs b/crates/nu-explore/src/commands/nu.rs index f8aaf44c24..84e1fb9706 100644 --- a/crates/nu-explore/src/commands/nu.rs +++ b/crates/nu-explore/src/commands/nu.rs @@ -48,6 +48,7 @@ impl ViewCommand for NuCmd { engine_state: &EngineState, stack: &mut Stack, value: Option, + config: &ViewConfig, ) -> Result { let value = value.unwrap_or_default(); @@ -62,10 +63,10 @@ impl ViewCommand for NuCmd { return Ok(NuView::Preview(Preview::new(&text))); } - let mut view = RecordView::new(columns, values); + let mut view = RecordView::new(columns, values, config.explore_config.clone()); if is_record { - view.set_orientation_current(Orientation::Left); + view.set_top_layer_orientation(Orientation::Left); } Ok(NuView::Records(view)) @@ -119,11 +120,4 @@ impl View for NuView { NuView::Preview(v) => v.exit(), } } - - fn setup(&mut self, config: ViewConfig<'_>) { - match self { - NuView::Records(v) => v.setup(config), - NuView::Preview(v) => v.setup(config), - } - } } diff --git a/crates/nu-explore/src/commands/table.rs b/crates/nu-explore/src/commands/table.rs index e5a3ae5599..9ab1e39b98 100644 --- a/crates/nu-explore/src/commands/table.rs +++ b/crates/nu-explore/src/commands/table.rs @@ -1,7 +1,7 @@ use super::ViewCommand; use crate::{ nu_common::collect_input, - views::{Orientation, RecordView}, + views::{Orientation, RecordView, ViewConfig}, }; use anyhow::Result; use nu_protocol::{ @@ -49,20 +49,21 @@ impl ViewCommand for TableCmd { _: &EngineState, _: &mut Stack, value: Option, + config: &ViewConfig, ) -> Result { let value = value.unwrap_or_default(); let is_record = matches!(value, Value::Record { .. }); let (columns, data) = collect_input(value)?; - let mut view = RecordView::new(columns, data); + let mut view = RecordView::new(columns, data, config.explore_config.clone()); if is_record { - view.set_orientation_current(Orientation::Left); + view.set_top_layer_orientation(Orientation::Left); } if let Some(o) = self.settings.orientation { - view.set_orientation_current(o); + view.set_top_layer_orientation(o); } if self.settings.turn_on_cursor_mode { diff --git a/crates/nu-explore/src/commands/try.rs b/crates/nu-explore/src/commands/try.rs index e70b4237d9..e1290651c8 100644 --- a/crates/nu-explore/src/commands/try.rs +++ b/crates/nu-explore/src/commands/try.rs @@ -1,5 +1,5 @@ use super::ViewCommand; -use crate::views::TryView; +use crate::views::{TryView, ViewConfig}; use anyhow::Result; use nu_protocol::{ engine::{EngineState, Stack}, @@ -43,9 +43,10 @@ impl ViewCommand for TryCmd { engine_state: &EngineState, stack: &mut Stack, value: Option, + config: &ViewConfig, ) -> Result { let value = value.unwrap_or_default(); - let mut view = TryView::new(value); + let mut view = TryView::new(value, config.explore_config.clone()); view.init(self.command.clone()); view.try_run(engine_state, stack)?; diff --git a/crates/nu-explore/src/explore.rs b/crates/nu-explore/src/explore.rs index e5498ce5b3..28a3f04a30 100644 --- a/crates/nu-explore/src/explore.rs +++ b/crates/nu-explore/src/explore.rs @@ -63,11 +63,10 @@ impl Command for Explore { let tail: bool = call.has_flag(engine_state, stack, "tail")?; let peek_value: bool = call.has_flag(engine_state, stack, "peek")?; - let ctrlc = engine_state.ctrlc.clone(); - let nu_config = engine_state.get_config(); + let nu_config = stack.get_config(engine_state); let style_computer = StyleComputer::from_config(engine_state, stack); - let mut explore_config = ExploreConfig::from_nu_config(nu_config); + let mut explore_config = ExploreConfig::from_nu_config(&nu_config); explore_config.table.show_header = show_head; explore_config.table.show_index = show_index; explore_config.table.separator_style = lookup_color(&style_computer, "separator"); @@ -75,7 +74,7 @@ impl Command for Explore { let lscolors = create_lscolors(engine_state, stack); let config = PagerConfig::new( - nu_config, + &nu_config, &explore_config, &style_computer, &lscolors, @@ -83,7 +82,7 @@ impl Command for Explore { tail, ); - let result = run_pager(engine_state, &mut stack.clone(), ctrlc, input, config); + let result = run_pager(engine_state, &mut stack.clone(), input, config); match result { Ok(Some(value)) => Ok(PipelineData::Value(value, None)), diff --git a/crates/nu-explore/src/lib.rs b/crates/nu-explore/src/lib.rs index 562602c394..d645572c89 100644 --- a/crates/nu-explore/src/lib.rs +++ b/crates/nu-explore/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod commands; mod default_context; mod explore; @@ -10,7 +11,8 @@ use anyhow::Result; use commands::{ExpandCmd, HelpCmd, NuCmd, QuitCmd, TableCmd, TryCmd}; pub use default_context::add_explore_context; pub use explore::Explore; -use nu_common::{collect_pipeline, has_simple_value, CtrlC}; +use explore::ExploreConfig; +use nu_common::{collect_pipeline, has_simple_value}; use nu_protocol::{ engine::{EngineState, Stack}, PipelineData, Value, @@ -27,7 +29,6 @@ mod util { fn run_pager( engine_state: &EngineState, stack: &mut Stack, - ctrlc: CtrlC, input: PipelineData, config: PagerConfig, ) -> Result> { @@ -43,15 +44,15 @@ fn run_pager( if is_binary { p.show_message("For help type :help"); - let view = binary_view(input)?; - return p.run(engine_state, stack, ctrlc, Some(view), commands); + let view = binary_view(input, config.explore_config)?; + return p.run(engine_state, stack, Some(view), commands); } let (columns, data) = collect_pipeline(input)?; let has_no_input = columns.is_empty() && data.is_empty(); if has_no_input { - return p.run(engine_state, stack, ctrlc, help_view(), commands); + return p.run(engine_state, stack, help_view(), commands); } p.show_message("For help type :help"); @@ -59,11 +60,11 @@ fn run_pager( if let Some(value) = has_simple_value(&data) { let text = value.to_abbreviated_string(config.nu_config); let view = Some(Page::new(Preview::new(&text), false)); - return p.run(engine_state, stack, ctrlc, view, commands); + return p.run(engine_state, stack, view, commands); } let view = create_record_view(columns, data, is_record, config); - p.run(engine_state, stack, ctrlc, view, commands) + p.run(engine_state, stack, view, commands) } fn create_record_view( @@ -73,9 +74,9 @@ fn create_record_view( is_record: bool, config: PagerConfig, ) -> Option { - let mut view = RecordView::new(columns, data); + let mut view = RecordView::new(columns, data, config.explore_config.clone()); if is_record { - view.set_orientation_current(Orientation::Left); + view.set_top_layer_orientation(Orientation::Left); } if config.tail { @@ -91,14 +92,14 @@ fn help_view() -> Option { Some(Page::new(HelpCmd::view(), false)) } -fn binary_view(input: PipelineData) -> Result { +fn binary_view(input: PipelineData, config: &ExploreConfig) -> Result { let data = match input { PipelineData::Value(Value::Binary { val, .. }, _) => val, PipelineData::ByteStream(bs, _) => bs.into_bytes()?, _ => unreachable!("checked beforehand"), }; - let view = BinaryView::new(data); + let view = BinaryView::new(data, config); Ok(Page::new(view, true)) } diff --git a/crates/nu-explore/src/nu_common/mod.rs b/crates/nu-explore/src/nu_common/mod.rs index 091a7d6f36..42ba2f0896 100644 --- a/crates/nu-explore/src/nu_common/mod.rs +++ b/crates/nu-explore/src/nu_common/mod.rs @@ -6,13 +6,11 @@ mod value; use nu_color_config::TextStyle; use nu_protocol::Value; -use std::sync::{atomic::AtomicBool, Arc}; pub use nu_ansi_term::{Color as NuColor, Style as NuStyle}; pub use nu_protocol::{Config as NuConfig, Span as NuSpan}; pub type NuText = (String, TextStyle); -pub type CtrlC = Option>; pub use command::run_command_with_value; pub use lscolor::{create_lscolors, lscolorize}; diff --git a/crates/nu-explore/src/nu_common/table.rs b/crates/nu-explore/src/nu_common/table.rs index cb580e404b..164c837bf9 100644 --- a/crates/nu-explore/src/nu_common/table.rs +++ b/crates/nu-explore/src/nu_common/table.rs @@ -1,22 +1,21 @@ use crate::nu_common::NuConfig; use nu_color_config::StyleComputer; -use nu_protocol::{Record, Span, Value}; +use nu_protocol::{Record, Signals, Span, Value}; use nu_table::{ common::{nu_value_to_string, nu_value_to_string_clean}, ExpandedTable, TableOpts, }; -use std::sync::{atomic::AtomicBool, Arc}; pub fn try_build_table( - ctrlc: Option>, + signals: &Signals, config: &NuConfig, style_computer: &StyleComputer, value: Value, ) -> String { let span = value.span(); match value { - Value::List { vals, .. } => try_build_list(vals, ctrlc, config, span, style_computer), - Value::Record { val, .. } => try_build_map(&val, span, style_computer, ctrlc, config), + Value::List { vals, .. } => try_build_list(vals, signals, config, span, style_computer), + Value::Record { val, .. } => try_build_map(&val, span, style_computer, signals, config), val if matches!(val, Value::String { .. }) => { nu_value_to_string_clean(&val, config, style_computer).0 } @@ -28,13 +27,13 @@ fn try_build_map( record: &Record, span: Span, style_computer: &StyleComputer, - ctrlc: Option>, + signals: &Signals, config: &NuConfig, ) -> String { let opts = TableOpts::new( config, style_computer, - ctrlc, + signals, Span::unknown(), usize::MAX, (config.table_indent.left, config.table_indent.right), @@ -53,7 +52,7 @@ fn try_build_map( fn try_build_list( vals: Vec, - ctrlc: Option>, + signals: &Signals, config: &NuConfig, span: Span, style_computer: &StyleComputer, @@ -61,7 +60,7 @@ fn try_build_list( let opts = TableOpts::new( config, style_computer, - ctrlc, + signals, Span::unknown(), usize::MAX, (config.table_indent.left, config.table_indent.right), diff --git a/crates/nu-explore/src/pager/mod.rs b/crates/nu-explore/src/pager/mod.rs index a56df95bc7..4a043476bb 100644 --- a/crates/nu-explore/src/pager/mod.rs +++ b/crates/nu-explore/src/pager/mod.rs @@ -11,7 +11,7 @@ use self::{ use super::views::{Layout, View}; use crate::{ explore::ExploreConfig, - nu_common::{CtrlC, NuColor, NuConfig, NuStyle}, + nu_common::{NuColor, NuConfig, NuStyle}, registry::{Command, CommandRegistry}, views::{util::nu_style_to_tui, ViewConfig}, }; @@ -36,7 +36,6 @@ use std::{ cmp::min, io::{self, Stdout}, result, - sync::atomic::Ordering, }; pub type Frame<'a> = ratatui::Frame<'a>; @@ -89,20 +88,41 @@ impl<'a> Pager<'a> { &mut self, engine_state: &EngineState, stack: &mut Stack, - ctrlc: CtrlC, - mut view: Option, + view: Option, commands: CommandRegistry, ) -> Result> { - if let Some(page) = &mut view { - page.view.setup(ViewConfig::new( - self.config.nu_config, - self.config.explore_config, - self.config.style_computer, - self.config.lscolors, - )) + // setup terminal + enable_raw_mode()?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, Clear(ClearType::All))?; + + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let mut info = ViewInfo { + status: Some(Report::default()), + ..Default::default() + }; + + if let Some(text) = self.message.take() { + info.status = Some(Report::message(text, Severity::Info)); } - run_pager(engine_state, stack, ctrlc, self, view, commands) + let result = render_ui( + &mut terminal, + engine_state, + stack, + self, + &mut info, + view, + commands, + )?; + + // restore terminal + disable_raw_mode()?; + execute!(io::stdout(), LeaveAlternateScreen)?; + + Ok(result) } } @@ -145,55 +165,11 @@ impl<'a> PagerConfig<'a> { } } -fn run_pager( - engine_state: &EngineState, - stack: &mut Stack, - ctrlc: CtrlC, - pager: &mut Pager, - view: Option, - commands: CommandRegistry, -) -> Result> { - // setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, Clear(ClearType::All))?; - - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - let mut info = ViewInfo { - status: Some(Report::default()), - ..Default::default() - }; - - if let Some(text) = pager.message.take() { - info.status = Some(Report::message(text, Severity::Info)); - } - - let result = render_ui( - &mut terminal, - engine_state, - stack, - ctrlc, - pager, - &mut info, - view, - commands, - )?; - - // restore terminal - disable_raw_mode()?; - execute!(io::stdout(), LeaveAlternateScreen)?; - - Ok(result) -} - #[allow(clippy::too_many_arguments)] fn render_ui( term: &mut Terminal, engine_state: &EngineState, stack: &mut Stack, - ctrlc: CtrlC, pager: &mut Pager<'_>, info: &mut ViewInfo, view: Option, @@ -203,11 +179,8 @@ fn render_ui( let mut view_stack = ViewStack::new(view, Vec::new()); loop { - // handle CTRLC event - if let Some(ctrlc) = ctrlc.clone() { - if ctrlc.load(Ordering::SeqCst) { - break Ok(None); - } + if engine_state.signals().interrupted() { + break Ok(None); } let mut layout = Layout::default(); @@ -440,15 +413,20 @@ fn run_command( Command::View { mut cmd, stackable } => { // what we do we just replace the view. let value = view_stack.curr_view.as_mut().and_then(|p| p.view.exit()); - let mut new_view = cmd.spawn(engine_state, stack, value)?; + let view_cfg = ViewConfig::new( + pager.config.nu_config, + pager.config.explore_config, + pager.config.style_computer, + pager.config.lscolors, + ); + + let new_view = cmd.spawn(engine_state, stack, value, &view_cfg)?; if let Some(view) = view_stack.curr_view.take() { if !view.stackable { view_stack.stack.push(view); } } - setup_view(&mut new_view, &pager.config); - view_stack.curr_view = Some(Page::raw(new_view, stackable)); Ok(CmdResult::new(false, true, cmd.name().to_owned())) @@ -456,16 +434,6 @@ fn run_command( } } -fn setup_view(view: &mut Box, cfg: &PagerConfig<'_>) { - let cfg = ViewConfig::new( - cfg.nu_config, - cfg.explore_config, - cfg.style_computer, - cfg.lscolors, - ); - view.setup(cfg); -} - fn set_cursor_cmd_bar(f: &mut Frame, area: Rect, pager: &Pager) { if pager.cmd_buf.is_cmd_input { // todo: deal with a situation where we exceed the bar width diff --git a/crates/nu-explore/src/registry/command.rs b/crates/nu-explore/src/registry/command.rs index 7e0bc131b5..6a3e9a128f 100644 --- a/crates/nu-explore/src/registry/command.rs +++ b/crates/nu-explore/src/registry/command.rs @@ -1,6 +1,6 @@ use crate::{ commands::{SimpleCommand, ViewCommand}, - views::View, + views::{View, ViewConfig}, }; use anyhow::Result; @@ -78,8 +78,9 @@ where engine_state: &nu_protocol::engine::EngineState, stack: &mut nu_protocol::engine::Stack, value: Option, + cfg: &ViewConfig, ) -> Result { - let view = self.0.spawn(engine_state, stack, value)?; + let view = self.0.spawn(engine_state, stack, value, cfg)?; Ok(Box::new(view) as Box) } } diff --git a/crates/nu-explore/src/views/binary/mod.rs b/crates/nu-explore/src/views/binary/mod.rs index f288861be8..2a35b7c180 100644 --- a/crates/nu-explore/src/views/binary/mod.rs +++ b/crates/nu-explore/src/views/binary/mod.rs @@ -40,11 +40,15 @@ struct Settings { } impl BinaryView { - pub fn new(data: Vec) -> Self { + pub fn new(data: Vec, cfg: &ExploreConfig) -> Self { + let settings = settings_from_config(cfg); + // There's gotta be a nicer way of doing this than creating a widget just to count lines + let count_rows = BinaryWidget::new(&data, settings.opts, Default::default()).count_lines(); + Self { data, - cursor: WindowCursor2D::default(), - settings: Settings::default(), + cursor: WindowCursor2D::new(count_rows, 1).expect("Failed to create XYCursor"), + settings, } } } @@ -87,15 +91,6 @@ impl View for BinaryView { // todo: impl Cursor + peek of a value None } - - fn setup(&mut self, cfg: ViewConfig<'_>) { - self.settings = settings_from_config(cfg.explore_config); - - let count_rows = - BinaryWidget::new(&self.data, self.settings.opts, Default::default()).count_lines(); - // TODO: refactor View so setup() is fallible and we don't have to panic here - self.cursor = WindowCursor2D::new(count_rows, 1).expect("Failed to create XYCursor"); - } } fn create_binary_widget(v: &BinaryView) -> BinaryWidget<'_> { diff --git a/crates/nu-explore/src/views/mod.rs b/crates/nu-explore/src/views/mod.rs index 7aab197eb5..bdbf3129db 100644 --- a/crates/nu-explore/src/views/mod.rs +++ b/crates/nu-explore/src/views/mod.rs @@ -99,8 +99,6 @@ pub trait View { fn exit(&mut self) -> Option { None } - - fn setup(&mut self, _: ViewConfig<'_>) {} } impl View for Box { @@ -131,8 +129,4 @@ impl View for Box { fn show_data(&mut self, i: usize) -> bool { self.as_mut().show_data(i) } - - fn setup(&mut self, cfg: ViewConfig<'_>) { - self.as_mut().setup(cfg) - } } diff --git a/crates/nu-explore/src/views/record/mod.rs b/crates/nu-explore/src/views/record/mod.rs index 87b0952a68..4f617686f1 100644 --- a/crates/nu-explore/src/views/record/mod.rs +++ b/crates/nu-explore/src/views/record/mod.rs @@ -20,7 +20,7 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use nu_color_config::StyleComputer; use nu_protocol::{ engine::{EngineState, Stack}, - Config, Record, Span, Value, + Config, Record, Value, }; use ratatui::{layout::Rect, widgets::Block}; use std::collections::HashMap; @@ -36,14 +36,12 @@ pub struct RecordView { } impl RecordView { - pub fn new(columns: Vec, records: Vec>) -> Self { + pub fn new(columns: Vec, records: Vec>, cfg: ExploreConfig) -> Self { Self { layer_stack: vec![RecordLayer::new(columns, records)], mode: UIMode::View, orientation: Orientation::Top, - // TODO: It's kind of gross how this temporarily has an incorrect/default config. - // See if we can pass correct config in through the constructor - cfg: ExploreConfig::default(), + cfg, } } @@ -54,63 +52,45 @@ impl RecordView { } pub fn transpose(&mut self) { - let layer = self.get_layer_last_mut(); + let layer = self.get_top_layer_mut(); transpose_table(layer); layer.reset_cursor(); } - // todo: rename to get_layer - pub fn get_layer_last(&self) -> &RecordLayer { + pub fn get_top_layer(&self) -> &RecordLayer { self.layer_stack .last() .expect("we guarantee that 1 entry is always in a list") } - pub fn get_layer_last_mut(&mut self) -> &mut RecordLayer { + pub fn get_top_layer_mut(&mut self) -> &mut RecordLayer { self.layer_stack .last_mut() .expect("we guarantee that 1 entry is always in a list") } - pub fn get_orientation_current(&mut self) -> Orientation { - self.get_layer_last().orientation - } - - pub fn set_orientation(&mut self, orientation: Orientation) { - self.orientation = orientation; - - // we need to reset all indexes as we can't no more use them. - self.reset_cursors(); - } - - fn reset_cursors(&mut self) { - for layer in &mut self.layer_stack { - layer.reset_cursor(); - } - } - - pub fn set_orientation_current(&mut self, orientation: Orientation) { - let layer = self.get_layer_last_mut(); + pub fn set_top_layer_orientation(&mut self, orientation: Orientation) { + let layer = self.get_top_layer_mut(); layer.orientation = orientation; layer.reset_cursor(); } /// Get the current position of the cursor in the table as a whole pub fn get_cursor_position(&self) -> Position { - let layer = self.get_layer_last(); + let layer = self.get_top_layer(); layer.cursor.position() } /// Get the current position of the cursor in the window being shown pub fn get_cursor_position_in_window(&self) -> Position { - let layer = self.get_layer_last(); + let layer = self.get_top_layer(); layer.cursor.window_relative_position() } /// Get the origin of the window being shown. (0,0), top left corner. pub fn get_window_origin(&self) -> Position { - let layer = self.get_layer_last(); + let layer = self.get_top_layer(); layer.cursor.window_origin() } @@ -122,22 +102,20 @@ impl RecordView { self.mode = UIMode::View; } - pub fn get_current_value(&self) -> Value { + pub fn get_current_value(&self) -> &Value { let Position { row, column } = self.get_cursor_position(); - let layer = self.get_layer_last(); + let layer = self.get_top_layer(); let (row, column) = match layer.orientation { Orientation::Top => (row, column), Orientation::Left => (column, row), }; - if row >= layer.record_values.len() || column >= layer.column_names.len() { - // actually must never happen; unless cursor works incorrectly - // if being sure about cursor it can be deleted; - return Value::nothing(Span::unknown()); - } + // These should never happen as long as the cursor is working correctly + assert!(row < layer.record_values.len(), "row out of bounds"); + assert!(column < layer.column_names.len(), "column out of bounds"); - layer.record_values[row][column].clone() + &layer.record_values[row][column] } fn create_table_widget<'a>(&'a mut self, cfg: ViewConfig<'a>) -> TableWidget<'a> { @@ -145,7 +123,7 @@ impl RecordView { let style_computer = cfg.style_computer; let Position { row, column } = self.get_window_origin(); - let layer = self.get_layer_last_mut(); + let layer = self.get_top_layer_mut(); if layer.record_text.is_none() { let mut data = convert_records_to_string(&layer.record_values, cfg.nu_config, cfg.style_computer); @@ -169,17 +147,17 @@ impl RecordView { } fn update_cursors(&mut self, rows: usize, columns: usize) { - match self.get_layer_last().orientation { + match self.get_top_layer().orientation { Orientation::Top => { let _ = self - .get_layer_last_mut() + .get_top_layer_mut() .cursor .set_window_size(rows, columns); } Orientation::Left => { let _ = self - .get_layer_last_mut() + .get_top_layer_mut() .cursor .set_window_size(rows, columns); } @@ -187,7 +165,7 @@ impl RecordView { } fn create_records_report(&self) -> Report { - let layer = self.get_layer_last(); + let layer = self.get_top_layer(); let covered_percent = report_row_position(layer.cursor); let cursor = report_cursor_position(self.mode, layer.cursor); let message = layer.name.clone().unwrap_or_default(); @@ -204,9 +182,6 @@ impl RecordView { impl View for RecordView { fn draw(&mut self, f: &mut Frame, area: Rect, cfg: ViewConfig<'_>, layout: &mut Layout) { let mut table_layout = TableWidgetState::default(); - // TODO: creating the table widget is O(N) where N is the number of cells in the grid. - // Way too slow to do on every draw call! - // To make explore work for larger data sets, this needs to be improved. let table = self.create_table_widget(cfg); f.render_stateful_widget(table, area, &mut table_layout); @@ -221,7 +196,7 @@ impl View for RecordView { row, column, table_layout.count_rows, - self.get_layer_last().orientation, + self.get_top_layer().orientation, self.cfg.table.show_header, ); @@ -269,7 +244,7 @@ impl View for RecordView { let style_computer = StyleComputer::new(&dummy_engine_state, &dummy_stack, HashMap::new()); let data = convert_records_to_string( - &self.get_layer_last().record_values, + &self.get_top_layer().record_values, &nu_protocol::Config::default(), &style_computer, ); @@ -278,7 +253,7 @@ impl View for RecordView { } fn show_data(&mut self, pos: usize) -> bool { - let data = &self.get_layer_last().record_values; + let data = &self.get_top_layer().record_values; let mut i = 0; for (row, cells) in data.iter().enumerate() { @@ -289,7 +264,7 @@ impl View for RecordView { for (column, _) in cells.iter().enumerate() { if i == pos { - self.get_layer_last_mut() + self.get_top_layer_mut() .cursor .set_window_start_position(row, column); return true; @@ -305,11 +280,6 @@ impl View for RecordView { fn exit(&mut self) -> Option { Some(build_last_value(self)) } - - // todo: move the method to Command? - fn setup(&mut self, cfg: ViewConfig<'_>) { - self.cfg = cfg.explore_config.clone(); - } } fn get_element_info( @@ -413,7 +383,7 @@ fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option { - view.get_layer_last_mut().cursor.prev_row_page(); + view.get_top_layer_mut().cursor.prev_row_page(); return Some(Transition::Ok); } @@ -426,7 +396,7 @@ fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option { - view.get_layer_last_mut().cursor.next_row_page(); + view.get_top_layer_mut().cursor.next_row_page(); return Some(Transition::Ok); } @@ -456,32 +426,32 @@ fn handle_key_event_view_mode(view: &mut RecordView, key: &KeyEvent) -> Option Some(Transition::Cmd(String::from("expand"))), KeyCode::Up | KeyCode::Char('k') => { - view.get_layer_last_mut().cursor.prev_row_i(); + view.get_top_layer_mut().cursor.prev_row_i(); Some(Transition::Ok) } KeyCode::Down | KeyCode::Char('j') => { - view.get_layer_last_mut().cursor.next_row_i(); + view.get_top_layer_mut().cursor.next_row_i(); Some(Transition::Ok) } KeyCode::Left | KeyCode::Char('h') => { - view.get_layer_last_mut().cursor.prev_column_i(); + view.get_top_layer_mut().cursor.prev_column_i(); Some(Transition::Ok) } KeyCode::Right | KeyCode::Char('l') => { - view.get_layer_last_mut().cursor.next_column_i(); + view.get_top_layer_mut().cursor.next_column_i(); Some(Transition::Ok) } KeyCode::Home | KeyCode::Char('g') => { - view.get_layer_last_mut().cursor.row_move_to_start(); + view.get_top_layer_mut().cursor.row_move_to_start(); Some(Transition::Ok) } KeyCode::End | KeyCode::Char('G') => { - view.get_layer_last_mut().cursor.row_move_to_end(); + view.get_top_layer_mut().cursor.row_move_to_end(); Some(Transition::Ok) } @@ -503,7 +473,7 @@ fn handle_key_event_cursor_mode( code: KeyCode::PageUp, .. } => { - view.get_layer_last_mut().cursor.prev_row_page(); + view.get_top_layer_mut().cursor.prev_row_page(); return Ok(Some(Transition::Ok)); } @@ -516,7 +486,7 @@ fn handle_key_event_cursor_mode( code: KeyCode::PageDown, .. } => { - view.get_layer_last_mut().cursor.next_row_page(); + view.get_top_layer_mut().cursor.next_row_page(); return Ok(Some(Transition::Ok)); } @@ -530,47 +500,55 @@ fn handle_key_event_cursor_mode( Ok(Some(Transition::Ok)) } KeyCode::Up | KeyCode::Char('k') => { - view.get_layer_last_mut().cursor.prev_row(); + view.get_top_layer_mut().cursor.prev_row(); Ok(Some(Transition::Ok)) } KeyCode::Down | KeyCode::Char('j') => { - view.get_layer_last_mut().cursor.next_row(); + view.get_top_layer_mut().cursor.next_row(); Ok(Some(Transition::Ok)) } KeyCode::Left | KeyCode::Char('h') => { - view.get_layer_last_mut().cursor.prev_column(); + view.get_top_layer_mut().cursor.prev_column(); Ok(Some(Transition::Ok)) } KeyCode::Right | KeyCode::Char('l') => { - view.get_layer_last_mut().cursor.next_column(); + view.get_top_layer_mut().cursor.next_column(); Ok(Some(Transition::Ok)) } KeyCode::Home | KeyCode::Char('g') => { - view.get_layer_last_mut().cursor.row_move_to_start(); + view.get_top_layer_mut().cursor.row_move_to_start(); Ok(Some(Transition::Ok)) } KeyCode::End | KeyCode::Char('G') => { - view.get_layer_last_mut().cursor.row_move_to_end(); + view.get_top_layer_mut().cursor.row_move_to_end(); Ok(Some(Transition::Ok)) } + // Try to "drill down" into the selected value KeyCode::Enter => { let value = view.get_current_value(); + + // ...but it only makes sense to drill down into a few types of values + if !matches!( + value, + Value::Record { .. } | Value::List { .. } | Value::Custom { .. } + ) { + return Ok(None); + } + let is_record = matches!(value, Value::Record { .. }); - let next_layer = create_layer(value)?; + let next_layer = create_layer(value.clone())?; push_layer(view, next_layer); if is_record { - view.set_orientation_current(Orientation::Left); - } else if view.orientation == view.get_layer_last().orientation { - view.get_layer_last_mut().orientation = view.orientation; + view.set_top_layer_orientation(Orientation::Left); } else { - view.set_orientation_current(view.orientation); + view.set_top_layer_orientation(view.orientation); } Ok(Some(Transition::Ok)) @@ -586,7 +564,7 @@ fn create_layer(value: Value) -> Result { } fn push_layer(view: &mut RecordView, mut next_layer: RecordLayer) { - let layer = view.get_layer_last(); + let layer = view.get_top_layer(); let header = layer.get_column_header(); if let Some(header) = header { @@ -609,7 +587,7 @@ fn estimate_page_size(area: Rect, show_head: bool) -> u16 { /// scroll to the end of the data fn tail_data(state: &mut RecordView, page_size: usize) { - let layer = state.get_layer_last_mut(); + let layer = state.get_top_layer_mut(); let count_rows = layer.record_values.len(); if count_rows > page_size { layer @@ -648,8 +626,8 @@ fn highlight_selected_cell(f: &mut Frame, info: ElementInfo, cfg: &ExploreConfig fn build_last_value(v: &RecordView) -> Value { if v.mode == UIMode::Cursor { - v.get_current_value() - } else if v.get_layer_last().count_rows() < 2 { + v.get_current_value().clone() + } else if v.get_top_layer().count_rows() < 2 { build_table_as_record(v) } else { build_table_as_list(v) @@ -657,7 +635,7 @@ fn build_last_value(v: &RecordView) -> Value { } fn build_table_as_list(v: &RecordView) -> Value { - let layer = v.get_layer_last(); + let layer = v.get_top_layer(); let vals = layer .record_values @@ -677,7 +655,7 @@ fn build_table_as_list(v: &RecordView) -> Value { } fn build_table_as_record(v: &RecordView) -> Value { - let layer = v.get_layer_last(); + let layer = v.get_top_layer(); let mut record = Record::new(); if let Some(row) = layer.record_values.first() { diff --git a/crates/nu-explore/src/views/try.rs b/crates/nu-explore/src/views/try.rs index 7ad6d38aef..099e792e38 100644 --- a/crates/nu-explore/src/views/try.rs +++ b/crates/nu-explore/src/views/try.rs @@ -1,5 +1,6 @@ use super::{record::RecordView, util::nu_style_to_tui, Layout, Orientation, View, ViewConfig}; use crate::{ + explore::ExploreConfig, nu_common::{collect_pipeline, run_command_with_value}, pager::{report::Report, Frame, Transition, ViewInfo}, }; @@ -23,17 +24,19 @@ pub struct TryView { table: Option, view_mode: bool, border_color: Style, + config: ExploreConfig, } impl TryView { - pub fn new(input: Value) -> Self { + pub fn new(input: Value, config: ExploreConfig) -> Self { Self { input, table: None, - immediate: false, - border_color: Style::default(), + immediate: config.try_reactive, + border_color: nu_style_to_tui(config.table.separator_style), view_mode: false, command: String::new(), + config, } } @@ -42,7 +45,13 @@ impl TryView { } pub fn try_run(&mut self, engine_state: &EngineState, stack: &mut Stack) -> Result<()> { - let view = run_command(&self.command, &self.input, engine_state, stack)?; + let view = run_command( + &self.command, + &self.input, + engine_state, + stack, + &self.config, + )?; self.table = Some(view); Ok(()) } @@ -122,7 +131,6 @@ impl View for TryView { f.render_widget(table_block, table_area); if let Some(table) = &mut self.table { - table.setup(cfg); let area = Rect::new( area.x + 2, area.y + 4, @@ -232,20 +240,6 @@ impl View for TryView { fn show_data(&mut self, i: usize) -> bool { self.table.as_mut().map_or(false, |v| v.show_data(i)) } - - fn setup(&mut self, config: ViewConfig<'_>) { - self.border_color = nu_style_to_tui(config.explore_config.table.separator_style); - self.immediate = config.explore_config.try_reactive; - - let mut r = RecordView::new(vec![], vec![]); - r.setup(config); - - if let Some(view) = &mut self.table { - view.setup(config); - view.set_orientation(r.get_orientation_current()); - view.set_orientation_current(r.get_orientation_current()); - } - } } fn run_command( @@ -253,6 +247,7 @@ fn run_command( input: &Value, engine_state: &EngineState, stack: &mut Stack, + config: &ExploreConfig, ) -> Result { let pipeline = run_command_with_value(command, input, engine_state, stack)?; @@ -260,9 +255,9 @@ fn run_command( let (columns, values) = collect_pipeline(pipeline)?; - let mut view = RecordView::new(columns, values); + let mut view = RecordView::new(columns, values, config.clone()); if is_record { - view.set_orientation_current(Orientation::Left); + view.set_top_layer_orientation(Orientation::Left); } Ok(view) diff --git a/crates/nu-json/Cargo.toml b/crates/nu-json/Cargo.toml index 04c39bdac5..a3b61042b1 100644 --- a/crates/nu-json/Cargo.toml +++ b/crates/nu-json/Cargo.toml @@ -1,5 +1,8 @@ [package] -authors = ["The Nushell Project Developers", "Christian Zangl "] +authors = [ + "The Nushell Project Developers", + "Christian Zangl ", +] description = "Fork of serde-hjson" repository = "https://github.com/nushell/nushell/tree/main/crates/nu-json" edition = "2021" @@ -19,9 +22,11 @@ default = ["preserve_order"] [dependencies] linked-hash-map = { version = "0.5", optional = true } num-traits = { workspace = true } -serde = { workspace = true } +serde = { workspace = true } serde_json = { workspace = true } [dev-dependencies] -# nu-path = { path="../nu-path", version = "0.95.1" } -# serde_json = "1.0" \ No newline at end of file +nu-test-support = { path = "../nu-test-support", version = "0.95.1" } +nu-path = { path = "../nu-path", version = "0.95.1" } +serde_json = "1.0" +fancy-regex = "0.13.0" diff --git a/crates/nu-json/README.md b/crates/nu-json/README.md index acf3c6b296..c982f1a818 100644 --- a/crates/nu-json/README.md +++ b/crates/nu-json/README.md @@ -24,7 +24,7 @@ nu-json = "0.76" ## From the Commandline Add with: -``` +```sh cargo add serde cargo add nu-json ``` @@ -43,7 +43,7 @@ fn main() { let sample_text=r#" { - # specify rate in requests/second + ## specify rate in requests/second rate: 1000 array: [ diff --git a/crates/nu-json/src/lib.rs b/crates/nu-json/src/lib.rs index eead282786..1cf2c0a9fd 100644 --- a/crates/nu-json/src/lib.rs +++ b/crates/nu-json/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] pub use self::de::{ from_iter, from_reader, from_slice, from_str, Deserializer, StreamDeserializer, }; diff --git a/crates/nu-json/src/ser.rs b/crates/nu-json/src/ser.rs index d9df1aea56..4b15740602 100644 --- a/crates/nu-json/src/ser.rs +++ b/crates/nu-json/src/ser.rs @@ -714,7 +714,7 @@ impl<'a> HjsonFormatter<'a> { stack: Vec::new(), at_colon: false, indent, - braces_same_line: false, + braces_same_line: true, } } } diff --git a/crates/nu-json/tests/main.rs b/crates/nu-json/tests/main.rs index c209bc0a52..73c68f3d8e 100644 --- a/crates/nu-json/tests/main.rs +++ b/crates/nu-json/tests/main.rs @@ -1,7 +1,5 @@ -// FIXME: re-enable tests -/* -use nu_json::Value; use fancy_regex::Regex; +use nu_json::Value; use std::fs; use std::io; use std::path::{Path, PathBuf}; @@ -11,7 +9,7 @@ fn txt(text: &str) -> String { #[cfg(windows)] { - out.replace("\r\n", "").replace("\n", "") + out.replace("\r\n", "").replace('\n', "") } #[cfg(not(windows))] @@ -21,15 +19,7 @@ fn txt(text: &str) -> String { } fn hjson_expectations() -> PathBuf { - let assets = nu_test_support::fs::assets().join("nu_json"); - - nu_path::canonicalize(assets.clone()).unwrap_or_else(|e| { - panic!( - "Couldn't canonicalize hjson assets path {}: {:?}", - assets.display(), - e - ) - }) + nu_test_support::fs::assets().join("nu_json").into() } fn get_test_content(name: &str) -> io::Result { @@ -50,7 +40,7 @@ fn get_result_content(name: &str) -> io::Result<(String, String)> { let p1 = format!("{}/{}_result.json", expectations.display(), name); let p2 = format!("{}/{}_result.hjson", expectations.display(), name); - Ok((fs::read_to_string(&p1)?, fs::read_to_string(&p2)?)) + Ok((fs::read_to_string(p1)?, fs::read_to_string(p2)?)) } macro_rules! run_test { @@ -73,7 +63,8 @@ macro_rules! run_test { let actual_hjson = txt(&actual_hjson); let actual_json = $fix(serde_json::to_string_pretty(&udata).unwrap()); let actual_json = txt(&actual_json); - if rhjson != actual_hjson { + // nu_json::to_string now outputs json instead of hjson! + if rjson != actual_hjson { println!( "{:?}\n---hjson expected\n{}\n---hjson actual\n{}\n---\n", name, rhjson, actual_hjson @@ -85,7 +76,7 @@ macro_rules! run_test { name, rjson, actual_json ); } - assert!(rhjson == actual_hjson && rjson == actual_json); + assert!(rjson == actual_hjson && rjson == actual_json); } }}; } @@ -198,7 +189,7 @@ fn test_hjson() { let missing = all .into_iter() - .filter(|x| done.iter().find(|y| &x == y) == None) + .filter(|x| !done.iter().any(|y| x == y)) .collect::>(); if !missing.is_empty() { @@ -208,5 +199,3 @@ fn test_hjson() { panic!(); } } - -*/ diff --git a/crates/nu-lsp/README.md b/crates/nu-lsp/README.md new file mode 100644 index 0000000000..d9e666d6c2 --- /dev/null +++ b/crates/nu-lsp/README.md @@ -0,0 +1,7 @@ +Implementation of the Nushell language server. + +See [the Language Server Protocol specification](https://microsoft.github.io/language-server-protocol/) + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-lsp/src/lib.rs b/crates/nu-lsp/src/lib.rs index 44eeeb5756..aee2ab240d 100644 --- a/crates/nu-lsp/src/lib.rs +++ b/crates/nu-lsp/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] use lsp_server::{Connection, IoThreads, Message, Response, ResponseError}; use lsp_types::{ request::{Completion, GotoDefinition, HoverRequest, Request}, @@ -17,10 +18,7 @@ use ropey::Rope; use std::{ collections::BTreeMap, path::{Path, PathBuf}, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, + sync::Arc, time::Duration, }; @@ -57,11 +55,7 @@ impl LanguageServer { }) } - pub fn serve_requests( - mut self, - engine_state: EngineState, - ctrlc: Arc, - ) -> Result<()> { + pub fn serve_requests(mut self, engine_state: EngineState) -> Result<()> { let server_capabilities = serde_json::to_value(ServerCapabilities { text_document_sync: Some(lsp_types::TextDocumentSyncCapability::Kind( TextDocumentSyncKind::INCREMENTAL, @@ -75,10 +69,12 @@ impl LanguageServer { let _initialization_params = self .connection - .initialize_while(server_capabilities, || !ctrlc.load(Ordering::SeqCst)) + .initialize_while(server_capabilities, || { + !engine_state.signals().interrupted() + }) .into_diagnostic()?; - while !ctrlc.load(Ordering::SeqCst) { + while !engine_state.signals().interrupted() { let msg = match self .connection .receiver @@ -631,7 +627,7 @@ mod tests { std::thread::spawn(move || { let engine_state = nu_cmd_lang::create_default_context(); let engine_state = nu_command::add_shell_command_context(engine_state); - send.send(lsp_server.serve_requests(engine_state, Arc::new(AtomicBool::new(false)))) + send.send(lsp_server.serve_requests(engine_state)) }); client_connection diff --git a/crates/nu-parser/README.md b/crates/nu-parser/README.md index 02f61a31a6..cbb7729e4d 100644 --- a/crates/nu-parser/README.md +++ b/crates/nu-parser/README.md @@ -4,7 +4,7 @@ Nushell's parser is a type-directed parser, meaning that the parser will use typ Nushell's base language is whitespace-separated tokens with the command (Nushell's term for a function) name in the head position: -``` +```nushell head1 arg1 arg2 | head2 ``` @@ -12,7 +12,7 @@ head1 arg1 arg2 | head2 The first job of the parser is to a lexical analysis to find where the tokens start and end in the input. This turns the above into: -``` +```text , , , , ``` @@ -24,7 +24,7 @@ As Nushell is a language of pipelines, pipes form a key role in both separating The above tokens are converted the following during the lite parse phase: -``` +```text Pipeline: Command #1: , , @@ -45,7 +45,7 @@ Each command has a shape assigned to each of the arguments it reads in. These sh For example, if the command is written as: -```sql +```text where $x > 10 ``` @@ -53,7 +53,7 @@ When the parsing happens, the parser will look up the `where` command and find i In the above example, if the Signature of `where` said that it took three String values, the result would be: -``` +```text CallInfo: Name: `where` Args: @@ -64,7 +64,7 @@ CallInfo: Or, the Signature could state that it takes in three positional arguments: a Variable, an Operator, and a Number, which would give: -``` +```text CallInfo: Name: `where` Args: @@ -77,7 +77,7 @@ Note that in this case, each would be checked at compile time to confirm that th Finally, some Shapes can consume more than one token. In the above, if the `where` command stated it took in a single required argument, and that the Shape of this argument was a MathExpression, then the parser would treat the remaining tokens as part of the math expression. -``` +```text CallInfo: Name: `where` Args: diff --git a/crates/nu-parser/src/known_external.rs b/crates/nu-parser/src/known_external.rs index 453112aa32..a41cf3a4e8 100644 --- a/crates/nu-parser/src/known_external.rs +++ b/crates/nu-parser/src/known_external.rs @@ -1,7 +1,8 @@ use nu_engine::command_prelude::*; use nu_protocol::{ - ast::{Argument, Expr, Expression}, - engine::{CommandType, UNKNOWN_SPAN_ID}, + ast::{self, Expr, Expression}, + engine::{self, CallImpl, CommandType, UNKNOWN_SPAN_ID}, + ir::{self, DataSlice}, }; #[derive(Clone)] @@ -43,8 +44,6 @@ impl Command for KnownExternal { let command = engine_state.get_decl(decl_id); - let mut extern_call = Call::new(head_span); - let extern_name = if let Some(name_bytes) = engine_state.find_decl_name(call.decl_id, &[]) { String::from_utf8_lossy(name_bytes) } else { @@ -56,59 +55,166 @@ impl Command for KnownExternal { }; let extern_name: Vec<_> = extern_name.split(' ').collect(); - let call_head_id = engine_state - .find_span_id(call.head) - .unwrap_or(UNKNOWN_SPAN_ID); - let arg_extern_name = Expression::new_existing( - Expr::String(extern_name[0].to_string()), + match &call.inner { + CallImpl::AstRef(call) => { + let extern_call = ast_call_to_extern_call(engine_state, call, &extern_name)?; + command.run(engine_state, stack, &(&extern_call).into(), input) + } + CallImpl::AstBox(call) => { + let extern_call = ast_call_to_extern_call(engine_state, call, &extern_name)?; + command.run(engine_state, stack, &(&extern_call).into(), input) + } + CallImpl::IrRef(call) => { + let extern_call = ir_call_to_extern_call(stack, call, &extern_name)?; + command.run(engine_state, stack, &(&extern_call).into(), input) + } + CallImpl::IrBox(call) => { + let extern_call = ir_call_to_extern_call(stack, call, &extern_name)?; + command.run(engine_state, stack, &(&extern_call).into(), input) + } + } + } +} + +/// Transform the args from an `ast::Call` onto a `run-external` call +fn ast_call_to_extern_call( + engine_state: &EngineState, + call: &ast::Call, + extern_name: &[&str], +) -> Result { + let head_span = call.head; + + let mut extern_call = ast::Call::new(head_span); + + let call_head_id = engine_state + .find_span_id(call.head) + .unwrap_or(UNKNOWN_SPAN_ID); + + let arg_extern_name = Expression::new_existing( + Expr::String(extern_name[0].to_string()), + call.head, + call_head_id, + Type::String, + ); + + extern_call.add_positional(arg_extern_name); + + for subcommand in extern_name.iter().skip(1) { + extern_call.add_positional(Expression::new_existing( + Expr::String(subcommand.to_string()), call.head, call_head_id, Type::String, - ); + )); + } - extern_call.add_positional(arg_extern_name); - - for subcommand in extern_name.into_iter().skip(1) { - extern_call.add_positional(Expression::new_existing( - Expr::String(subcommand.to_string()), - call.head, - call_head_id, - Type::String, - )); - } - - for arg in &call.arguments { - match arg { - Argument::Positional(positional) => extern_call.add_positional(positional.clone()), - Argument::Named(named) => { - let named_span_id = engine_state - .find_span_id(named.0.span) - .unwrap_or(UNKNOWN_SPAN_ID); - if let Some(short) = &named.1 { - extern_call.add_positional(Expression::new_existing( - Expr::String(format!("-{}", short.item)), - named.0.span, - named_span_id, - Type::String, - )); - } else { - extern_call.add_positional(Expression::new_existing( - Expr::String(format!("--{}", named.0.item)), - named.0.span, - named_span_id, - Type::String, - )); - } - if let Some(arg) = &named.2 { - extern_call.add_positional(arg.clone()); - } + for arg in &call.arguments { + match arg { + ast::Argument::Positional(positional) => extern_call.add_positional(positional.clone()), + ast::Argument::Named(named) => { + let named_span_id = engine_state + .find_span_id(named.0.span) + .unwrap_or(UNKNOWN_SPAN_ID); + if let Some(short) = &named.1 { + extern_call.add_positional(Expression::new_existing( + Expr::String(format!("-{}", short.item)), + named.0.span, + named_span_id, + Type::String, + )); + } else { + extern_call.add_positional(Expression::new_existing( + Expr::String(format!("--{}", named.0.item)), + named.0.span, + named_span_id, + Type::String, + )); } - Argument::Unknown(unknown) => extern_call.add_unknown(unknown.clone()), - Argument::Spread(args) => extern_call.add_spread(args.clone()), + if let Some(arg) = &named.2 { + extern_call.add_positional(arg.clone()); + } + } + ast::Argument::Unknown(unknown) => extern_call.add_unknown(unknown.clone()), + ast::Argument::Spread(args) => extern_call.add_spread(args.clone()), + } + } + + Ok(extern_call) +} + +/// Transform the args from an `ir::Call` onto a `run-external` call +fn ir_call_to_extern_call( + stack: &mut Stack, + call: &ir::Call, + extern_name: &[&str], +) -> Result { + let mut extern_call = ir::Call::build(call.decl_id, call.head); + + // Add the command and subcommands + for name in extern_name { + extern_call.add_positional(stack, call.head, Value::string(*name, call.head)); + } + + // Add the arguments, reformatting named arguments into string positionals + for index in 0..call.args_len { + match &call.arguments(stack)[index] { + engine::Argument::Flag { + data, + name, + short, + span, + } => { + let name_arg = engine::Argument::Positional { + span: *span, + val: Value::string(known_external_option_name(data, *name, *short), *span), + ast: None, + }; + extern_call.add_argument(stack, name_arg); + } + engine::Argument::Named { + data, + name, + short, + span, + val, + .. + } => { + let name_arg = engine::Argument::Positional { + span: *span, + val: Value::string(known_external_option_name(data, *name, *short), *span), + ast: None, + }; + let val_arg = engine::Argument::Positional { + span: *span, + val: val.clone(), + ast: None, + }; + extern_call.add_argument(stack, name_arg); + extern_call.add_argument(stack, val_arg); + } + a @ (engine::Argument::Positional { .. } + | engine::Argument::Spread { .. } + | engine::Argument::ParserInfo { .. }) => { + let argument = a.clone(); + extern_call.add_argument(stack, argument); } } + } - command.run(engine_state, stack, &extern_call, input) + Ok(extern_call.finish()) +} + +fn known_external_option_name(data: &[u8], name: DataSlice, short: DataSlice) -> String { + if !data[name].is_empty() { + format!( + "--{}", + std::str::from_utf8(&data[name]).expect("invalid utf-8 in flag name") + ) + } else { + format!( + "-{}", + std::str::from_utf8(&data[short]).expect("invalid utf-8 in flag short name") + ) } } diff --git a/crates/nu-parser/src/lex.rs b/crates/nu-parser/src/lex.rs index 4639cae25d..3290a774f4 100644 --- a/crates/nu-parser/src/lex.rs +++ b/crates/nu-parser/src/lex.rs @@ -238,6 +238,10 @@ pub fn lex_item( Some(e), ); } + } else if c == b'|' && is_redirection(&input[token_start..*curr_offset]) { + // matches err>| etc. + *curr_offset += 1; + break; } else if is_item_terminator(&block_level, c, additional_whitespace, special_tokens) { break; } @@ -301,6 +305,16 @@ pub fn lex_item( contents: TokenContents::OutGreaterGreaterThan, span, }, + b"out>|" | b"o>|" => { + err = Some(ParseError::Expected( + "`|`. Redirecting stdout to a pipe is the same as normal piping.", + span, + )); + Token { + contents: TokenContents::Item, + span, + } + } b"err>" | b"e>" => Token { contents: TokenContents::ErrGreaterThan, span, @@ -309,6 +323,10 @@ pub fn lex_item( contents: TokenContents::ErrGreaterGreaterThan, span, }, + b"err>|" | b"e>|" => Token { + contents: TokenContents::ErrGreaterPipe, + span, + }, b"out+err>" | b"err+out>" | b"o+e>" | b"e+o>" => Token { contents: TokenContents::OutErrGreaterThan, span, @@ -317,6 +335,10 @@ pub fn lex_item( contents: TokenContents::OutErrGreaterGreaterThan, span, }, + b"out+err>|" | b"err+out>|" | b"o+e>|" | b"e+o>|" => Token { + contents: TokenContents::OutErrGreaterPipe, + span, + }, b"&&" => { err = Some(ParseError::ShellAndAnd(span)); Token { @@ -580,14 +602,6 @@ fn lex_internal( // If the next character is non-newline whitespace, skip it. curr_offset += 1; } else { - let token = try_lex_special_piped_item(input, &mut curr_offset, span_offset); - if let Some(token) = token { - output.push(token); - is_complete = false; - continue; - } - - // Otherwise, try to consume an unclassified token. let (token, err) = lex_item( input, &mut curr_offset, @@ -606,48 +620,10 @@ fn lex_internal( (output, error) } -/// trying to lex for the following item: -/// e>|, e+o>|, o+e>| -/// -/// It returns Some(token) if we find the item, or else return None. -fn try_lex_special_piped_item( - input: &[u8], - curr_offset: &mut usize, - span_offset: usize, -) -> Option { - let c = input[*curr_offset]; - let e_pipe_len = 3; - let eo_pipe_len = 5; - let offset = *curr_offset; - if c == b'e' { - // expect `e>|` - if (offset + e_pipe_len <= input.len()) && (&input[offset..offset + e_pipe_len] == b"e>|") { - *curr_offset += e_pipe_len; - return Some(Token::new( - TokenContents::ErrGreaterPipe, - Span::new(span_offset + offset, span_offset + offset + e_pipe_len), - )); - } - if (offset + eo_pipe_len <= input.len()) - && (&input[offset..offset + eo_pipe_len] == b"e+o>|") - { - *curr_offset += eo_pipe_len; - return Some(Token::new( - TokenContents::OutErrGreaterPipe, - Span::new(span_offset + offset, span_offset + offset + eo_pipe_len), - )); - } - } else if c == b'o' { - // it can be the following case: `o+e>|` - if (offset + eo_pipe_len <= input.len()) - && (&input[offset..offset + eo_pipe_len] == b"o+e>|") - { - *curr_offset += eo_pipe_len; - return Some(Token::new( - TokenContents::OutErrGreaterPipe, - Span::new(span_offset + offset, span_offset + offset + eo_pipe_len), - )); - } - } - None +/// True if this the start of a redirection. Does not match `>>` or `>|` forms. +fn is_redirection(token: &[u8]) -> bool { + matches!( + token, + b"o>" | b"out>" | b"e>" | b"err>" | b"o+e>" | b"e+o>" | b"out+err>" | b"err+out>" + ) } diff --git a/crates/nu-parser/src/lib.rs b/crates/nu-parser/src/lib.rs index 89cba8d243..8f871aa815 100644 --- a/crates/nu-parser/src/lib.rs +++ b/crates/nu-parser/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod deparse; mod exportable; mod flatten; @@ -24,6 +25,3 @@ pub use parser::{ is_math_expression_like, parse, parse_block, parse_expression, parse_external_call, parse_unit_value, trim_quotes, trim_quotes_str, unescape_unquote_string, DURATION_UNIT_GROUPS, }; - -#[cfg(feature = "plugin")] -pub use parse_keywords::parse_register; diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 70d217c564..4ac1d20699 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -82,7 +82,6 @@ pub const UNALIASABLE_PARSER_KEYWORDS: &[&[u8]] = &[ b"source-env", b"source", b"where", - b"register", b"plugin use", ]; @@ -3627,244 +3626,6 @@ pub fn parse_where(working_set: &mut StateWorkingSet, lite_command: &LiteCommand } } -/// `register` is deprecated and will be removed in 0.94. Use `plugin add` and `plugin use` instead. -#[cfg(feature = "plugin")] -pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteCommand) -> Pipeline { - use nu_plugin_engine::PluginDeclaration; - use nu_protocol::{ - engine::Stack, ErrSpan, ParseWarning, PluginIdentity, PluginRegistryItem, PluginSignature, - RegisteredPlugin, - }; - - let spans = &lite_command.parts; - - #[allow(deprecated)] - let cwd = working_set.get_cwd(); - - // Checking that the function is used with the correct name - // Maybe this is not necessary but it is a sanity check - if working_set.get_span_contents(spans[0]) != b"register" { - working_set.error(ParseError::UnknownState( - "internal error: Wrong call name for 'register' function".into(), - Span::concat(spans), - )); - return garbage_pipeline(working_set, spans); - } - if let Some(redirection) = lite_command.redirection.as_ref() { - working_set.error(redirecting_builtin_error("register", redirection)); - return garbage_pipeline(working_set, spans); - } - - // Parsing the spans and checking that they match the register signature - // Using a parsed call makes more sense than checking for how many spans are in the call - // Also, by creating a call, it can be checked if it matches the declaration signature - let (call, call_span) = match working_set.find_decl(b"register") { - None => { - working_set.error(ParseError::UnknownState( - "internal error: Register declaration not found".into(), - Span::concat(spans), - )); - return garbage_pipeline(working_set, spans); - } - Some(decl_id) => { - let ParsedInternalCall { call, output } = - parse_internal_call(working_set, spans[0], &spans[1..], decl_id); - let decl = working_set.get_decl(decl_id); - - let call_span = Span::concat(spans); - - let starting_error_count = working_set.parse_errors.len(); - check_call(working_set, call_span, &decl.signature(), &call); - - let Ok(is_help) = has_flag_const(working_set, &call, "help") else { - return garbage_pipeline(working_set, spans); - }; - - if starting_error_count != working_set.parse_errors.len() || is_help { - return Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - output, - )]); - } - - (call, call_span) - } - }; - - // Now that the call is parsed, add the deprecation warning - working_set - .parse_warnings - .push(ParseWarning::DeprecatedWarning { - old_command: "register".into(), - new_suggestion: "use `plugin add` and `plugin use`".into(), - span: call.head, - url: "https://www.nushell.sh/book/plugins.html".into(), - }); - - // Extracting the required arguments from the call and keeping them together in a tuple - let arguments = call - .positional_nth(0) - .map(|expr| { - let val = - eval_constant(working_set, expr).map_err(|err| err.wrap(working_set, call.head))?; - let filename = val - .coerce_into_string() - .map_err(|err| err.wrap(working_set, call.head))?; - - let Some(path) = find_in_dirs(&filename, working_set, &cwd, Some(PLUGIN_DIRS_VAR)) - else { - return Err(ParseError::RegisteredFileNotFound(filename, expr.span)); - }; - - if path.exists() && path.is_file() { - Ok((path, expr.span)) - } else { - Err(ParseError::RegisteredFileNotFound(filename, expr.span)) - } - }) - .expect("required positional has being checked"); - - // Signature is an optional value from the call and will be used to decide if - // the plugin is called to get the signatures or to use the given signature - let signature = call.positional_nth(1).map(|expr| { - let signature = working_set.get_span_contents(expr.span); - serde_json::from_slice::(signature).map_err(|e| { - ParseError::LabeledError( - "Signature deserialization error".into(), - format!("unable to deserialize signature: {e}"), - spans[0], - ) - }) - }); - - // Shell is another optional value used as base to call shell to plugins - let shell = call.get_flag_expr("shell").map(|expr| { - let shell_expr = working_set.get_span_contents(expr.span); - - String::from_utf8(shell_expr.to_vec()) - .map_err(|_| ParseError::NonUtf8(expr.span)) - .and_then(|name| { - canonicalize_with(&name, cwd) - .map_err(|_| ParseError::RegisteredFileNotFound(name, expr.span)) - }) - .and_then(|path| { - if path.exists() & path.is_file() { - Ok(path) - } else { - Err(ParseError::RegisteredFileNotFound( - format!("{path:?}"), - expr.span, - )) - } - }) - }); - - let shell = match shell { - None => None, - Some(path) => match path { - Ok(path) => Some(path), - Err(err) => { - working_set.error(err); - return Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - Type::Any, - )]); - } - }, - }; - - // We need the current environment variables for `python` based plugins - // Or we'll likely have a problem when a plugin is implemented in a virtual Python environment. - let get_envs = || { - let stack = Stack::new().capture(); - nu_engine::env::env_to_strings(working_set.permanent_state, &stack) - }; - - let error = arguments.and_then(|(path, path_span)| { - let path = path.path_buf(); - - // Create the plugin identity. This validates that the plugin name starts with `nu_plugin_` - let identity = PluginIdentity::new(path, shell).err_span(path_span)?; - - let plugin = nu_plugin_engine::add_plugin_to_working_set(working_set, &identity) - .map_err(|err| err.wrap(working_set, call.head))?; - - let signatures = signature.map_or_else( - || { - // It's important that the plugin is restarted if we're going to get signatures - // - // The user would expect that `register` would always run the binary to get new - // signatures, in case it was replaced with an updated binary - plugin.reset().map_err(|err| { - ParseError::LabeledError( - "Failed to restart plugin to get new signatures".into(), - err.to_string(), - spans[0], - ) - })?; - - let metadata_and_signatures = plugin - .clone() - .get(get_envs) - .and_then(|p| { - let meta = p.get_metadata()?; - let sigs = p.get_signature()?; - Ok((meta, sigs)) - }) - .map_err(|err| { - log::warn!("Error getting metadata and signatures: {err:?}"); - ParseError::LabeledError( - "Error getting metadata and signatures".into(), - err.to_string(), - spans[0], - ) - }); - - match metadata_and_signatures { - Ok((meta, sigs)) => { - // Set the metadata on the plugin - plugin.set_metadata(Some(meta.clone())); - // Add the loaded plugin to the delta - working_set.update_plugin_registry(PluginRegistryItem::new( - &identity, - meta, - sigs.clone(), - )); - Ok(sigs) - } - Err(err) => Err(err), - } - }, - |sig| sig.map(|sig| vec![sig]), - )?; - - for signature in signatures { - // create plugin command declaration (need struct impl Command) - // store declaration in working set - let plugin_decl = PluginDeclaration::new(plugin.clone(), signature); - - working_set.add_decl(Box::new(plugin_decl)); - } - - Ok(()) - }); - - if let Err(err) = error { - working_set.error(err); - } - - Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - Type::Nothing, - )]) -} - #[cfg(feature = "plugin")] pub fn parse_plugin_use(working_set: &mut StateWorkingSet, call: Box) -> Pipeline { use nu_protocol::{FromValue, PluginRegistryFile}; diff --git a/crates/nu-parser/src/parse_patterns.rs b/crates/nu-parser/src/parse_patterns.rs index 73668b7d04..dc4a64ce37 100644 --- a/crates/nu-parser/src/parse_patterns.rs +++ b/crates/nu-parser/src/parse_patterns.rs @@ -39,7 +39,7 @@ pub fn parse_pattern(working_set: &mut StateWorkingSet, span: Span) -> MatchPatt let value = parse_value(working_set, span, &SyntaxShape::Any); MatchPattern { - pattern: Pattern::Value(value), + pattern: Pattern::Value(Box::new(value)), guard: None, span, } diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index c3d18b55ac..fc2131aad7 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -3302,6 +3302,8 @@ pub fn parse_row_condition(working_set: &mut StateWorkingSet, spans: &[Span]) -> default_value: None, }); + compile_block(working_set, &mut block); + working_set.add_block(Arc::new(block)) } }; @@ -4445,7 +4447,7 @@ pub fn parse_match_block_expression(working_set: &mut StateWorkingSet, span: Spa &SyntaxShape::MathExpression, ); - pattern.guard = Some(guard); + pattern.guard = Some(Box::new(guard)); position += if found { start + 1 } else { start }; connector = working_set.get_span_contents(output[position].span); } @@ -4950,7 +4952,7 @@ pub fn parse_math_expression( let mut expr_stack: Vec = vec![]; let mut idx = 0; - let mut last_prec = 1000000; + let mut last_prec = u8::MAX; let first_span = working_set.get_span_contents(spans[0]); @@ -5275,15 +5277,6 @@ pub fn parse_expression(working_set: &mut StateWorkingSet, spans: &[Span]) -> Ex } b"where" => parse_where_expr(working_set, &spans[pos..]), #[cfg(feature = "plugin")] - b"register" => { - working_set.error(ParseError::BuiltinCommandInPipeline( - "register".into(), - spans[0], - )); - - parse_call(working_set, &spans[pos..], spans[0]) - } - #[cfg(feature = "plugin")] b"plugin" => { if spans.len() > 1 && working_set.get_span_contents(spans[1]) == b"use" { // only 'plugin use' is banned @@ -5307,6 +5300,8 @@ pub fn parse_expression(working_set: &mut StateWorkingSet, spans: &[Span]) -> Ex let ty = output.ty.clone(); block.pipelines = vec![Pipeline::from_vec(vec![output])]; + compile_block(working_set, &mut block); + let block_id = working_set.add_block(Arc::new(block)); let mut env_vars = vec![]; @@ -5419,8 +5414,6 @@ pub fn parse_builtin_commands( b"export" => parse_export_in_block(working_set, lite_command), b"hide" => parse_hide(working_set, lite_command), b"where" => parse_where(working_set, lite_command), - #[cfg(feature = "plugin")] - b"register" => parse_register(working_set, lite_command), // Only "plugin use" is a keyword #[cfg(feature = "plugin")] b"plugin" @@ -5864,9 +5857,25 @@ pub fn parse_block( working_set.parse_errors.extend_from_slice(&errors); } + // Do not try to compile blocks that are subexpressions, or when we've already had a parse + // failure as that definitely will fail to compile + if !is_subexpression && working_set.parse_errors.is_empty() { + compile_block(working_set, &mut block); + } + block } +/// Compile an IR block for the `Block`, adding a compile error on failure +fn compile_block(working_set: &mut StateWorkingSet<'_>, block: &mut Block) { + match nu_engine::compile(working_set, block) { + Ok(ir_block) => { + block.ir_block = Some(ir_block); + } + Err(err) => working_set.compile_errors.push(err), + } +} + pub fn discover_captures_in_closure( working_set: &StateWorkingSet, block: &Block, @@ -6309,12 +6318,14 @@ fn wrap_expr_with_collect(working_set: &mut StateWorkingSet, expr: &Expression) default_value: None, }); - let block = Block { + 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( diff --git a/crates/nu-parser/src/type_check.rs b/crates/nu-parser/src/type_check.rs index 1c76afb621..6cfe8f64b3 100644 --- a/crates/nu-parser/src/type_check.rs +++ b/crates/nu-parser/src/type_check.rs @@ -278,8 +278,7 @@ pub fn math_result_type( ) } }, - Operator::Math(Math::Divide) | Operator::Math(Math::Modulo) => match (&lhs.ty, &rhs.ty) - { + Operator::Math(Math::Divide) => match (&lhs.ty, &rhs.ty) { (Type::Int, Type::Int) => (Type::Int, None), (Type::Float, Type::Int) => (Type::Float, None), (Type::Int, Type::Float) => (Type::Float, None), @@ -328,6 +327,55 @@ pub fn math_result_type( ) } }, + Operator::Math(Math::Modulo) => match (&lhs.ty, &rhs.ty) { + (Type::Int, Type::Int) => (Type::Int, None), + (Type::Float, Type::Int) => (Type::Float, None), + (Type::Int, Type::Float) => (Type::Float, None), + (Type::Float, Type::Float) => (Type::Float, None), + (Type::Number, Type::Number) => (Type::Number, None), + (Type::Number, Type::Int) => (Type::Number, None), + (Type::Int, Type::Number) => (Type::Number, None), + (Type::Number, Type::Float) => (Type::Number, None), + (Type::Float, Type::Number) => (Type::Number, None), + (Type::Filesize, Type::Filesize) => (Type::Filesize, None), + (Type::Filesize, Type::Int) => (Type::Filesize, None), + (Type::Filesize, Type::Float) => (Type::Filesize, None), + (Type::Duration, Type::Duration) => (Type::Duration, None), + (Type::Duration, Type::Int) => (Type::Duration, None), + (Type::Duration, Type::Float) => (Type::Duration, None), + + (Type::Custom(a), Type::Custom(b)) if a == b => (Type::Custom(a.clone()), None), + (Type::Custom(a), _) => (Type::Custom(a.clone()), None), + + (Type::Any, _) => (Type::Any, None), + (_, Type::Any) => (Type::Any, None), + (Type::Int | Type::Float | Type::Filesize | Type::Duration, _) => { + *op = Expression::garbage(working_set, op.span); + ( + Type::Any, + Some(ParseError::UnsupportedOperationRHS( + "division".into(), + op.span, + lhs.span, + lhs.ty.clone(), + rhs.span, + rhs.ty.clone(), + )), + ) + } + _ => { + *op = Expression::garbage(working_set, op.span); + ( + Type::Any, + Some(ParseError::UnsupportedOperationLHS( + "division".into(), + op.span, + lhs.span, + lhs.ty.clone(), + )), + ) + } + }, Operator::Math(Math::FloorDivision) => match (&lhs.ty, &rhs.ty) { (Type::Int, Type::Int) => (Type::Int, None), (Type::Float, Type::Int) => (Type::Int, None), @@ -1007,7 +1055,11 @@ pub fn check_block_input_output(working_set: &StateWorkingSet, block: &Block) -> .span }; - output_errors.push(ParseError::OutputMismatch(output_type.clone(), span)) + output_errors.push(ParseError::OutputMismatch( + output_type.clone(), + current_output_type.clone(), + span, + )) } } diff --git a/crates/nu-parser/tests/test_parser.rs b/crates/nu-parser/tests/test_parser.rs index 0784fe69d4..7762863ba1 100644 --- a/crates/nu-parser/tests/test_parser.rs +++ b/crates/nu-parser/tests/test_parser.rs @@ -1,7 +1,7 @@ use nu_parser::*; use nu_protocol::{ - ast::{Argument, Call, Expr, Expression, ExternalArgument, PathMember, Range}, - engine::{Command, EngineState, Stack, StateWorkingSet}, + ast::{Argument, Expr, Expression, ExternalArgument, PathMember, Range}, + engine::{Call, Command, EngineState, Stack, StateWorkingSet}, ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, }; use rstest::rstest; @@ -1759,10 +1759,7 @@ mod range { #[cfg(test)] mod input_types { use super::*; - use nu_protocol::{ - ast::{Argument, Call}, - Category, PipelineData, ShellError, Type, - }; + use nu_protocol::{ast::Argument, engine::Call, Category, PipelineData, ShellError, Type}; #[derive(Clone)] pub struct LsTest; diff --git a/crates/nu-path/Cargo.toml b/crates/nu-path/Cargo.toml index dc2f870a9d..62908e7cb7 100644 --- a/crates/nu-path/Cargo.toml +++ b/crates/nu-path/Cargo.toml @@ -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 } \ No newline at end of file +pwd = { workspace = true } diff --git a/crates/nu-path/src/form.rs b/crates/nu-path/src/form.rs index 4266905a20..edec72c08c 100644 --- a/crates/nu-path/src/form.rs +++ b/crates/nu-path/src/form.rs @@ -75,9 +75,6 @@ impl private::Sealed for Canonical { /// A marker trait for [`PathForm`]s that may be relative paths. /// This includes only the [`Any`] and [`Relative`] path forms. -/// -/// [`push`](crate::PathBuf::push) and [`join`](crate::Path::join) -/// operations only support [`MaybeRelative`] path forms as input. pub trait MaybeRelative: PathForm {} impl MaybeRelative for Any {} impl MaybeRelative for Relative {} @@ -149,13 +146,13 @@ impl PathSet for Any {} impl PathSet for Relative {} impl PathSet for Absolute {} -/// A marker trait for [`PathForm`]s that support pushing [`MaybeRelative`] paths. +/// A marker trait for [`PathForm`]s that support pushing paths. /// /// This includes only [`Any`] and [`Absolute`] path forms. /// Pushing onto a [`Relative`] path could cause it to become [`Absolute`], /// which is why they do not support pushing. /// In the future, a `push_rel` and/or a `try_push` method could be added as an alternative. -/// Similarly, [`Canonical`] paths may become uncanonical if a non-canonical path is pushed onto it. +/// Similarly, [`Canonical`] paths may become uncanonical if a path is pushed onto it. pub trait PathPush: PathSet {} impl PathPush for Any {} impl PathPush for Absolute {} diff --git a/crates/nu-path/src/helpers.rs b/crates/nu-path/src/helpers.rs index 5b389410e4..a6e35bddfe 100644 --- a/crates/nu-path/src/helpers.rs +++ b/crates/nu-path/src/helpers.rs @@ -3,14 +3,14 @@ use omnipath::WinPathExt; use std::path::PathBuf; pub fn home_dir() -> Option { - 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 { 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 { 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 { 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()), } } diff --git a/crates/nu-path/src/lib.rs b/crates/nu-path/src/lib.rs index 8553495439..2cb415264a 100644 --- a/crates/nu-path/src/lib.rs +++ b/crates/nu-path/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod assert_path_eq; mod components; pub mod dots; diff --git a/crates/nu-path/src/tilde.rs b/crates/nu-path/src/tilde.rs index 60cc7d11eb..95c91addf8 100644 --- a/crates/nu-path/src/tilde.rs +++ b/crates/nu-path/src/tilde.rs @@ -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) -> 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)] diff --git a/crates/nu-plugin-core/src/interface/mod.rs b/crates/nu-plugin-core/src/interface/mod.rs index a1b35f06ad..d05f26c1a2 100644 --- a/crates/nu-plugin-core/src/interface/mod.rs +++ b/crates/nu-plugin-core/src/interface/mod.rs @@ -3,10 +3,11 @@ use nu_plugin_protocol::{ByteStreamInfo, ListStreamInfo, PipelineDataHeader, StreamMessage}; use nu_protocol::{ engine::Sequence, ByteStream, IntoSpanned, ListStream, PipelineData, Reader, ShellError, + Signals, }; use std::{ io::{Read, Write}, - sync::{atomic::AtomicBool, Arc, Mutex}, + sync::Mutex, thread, }; @@ -172,7 +173,7 @@ pub trait InterfaceManager { fn read_pipeline_data( &self, header: PipelineDataHeader, - ctrlc: Option<&Arc>, + signals: &Signals, ) -> Result { self.prepare_pipeline_data(match header { PipelineDataHeader::Empty => PipelineData::Empty, @@ -180,12 +181,12 @@ pub trait InterfaceManager { PipelineDataHeader::ListStream(info) => { let handle = self.stream_manager().get_handle(); let reader = handle.read_stream(info.id, self.get_interface())?; - ListStream::new(reader, info.span, ctrlc.cloned()).into() + ListStream::new(reader, info.span, signals.clone()).into() } PipelineDataHeader::ByteStream(info) => { let handle = self.stream_manager().get_handle(); let reader = handle.read_stream(info.id, self.get_interface())?; - ByteStream::from_result_iter(reader, info.span, ctrlc.cloned(), info.type_).into() + ByteStream::from_result_iter(reader, info.span, signals.clone(), info.type_).into() } }) } diff --git a/crates/nu-plugin-core/src/interface/tests.rs b/crates/nu-plugin-core/src/interface/tests.rs index 3071f41da0..cb28692706 100644 --- a/crates/nu-plugin-core/src/interface/tests.rs +++ b/crates/nu-plugin-core/src/interface/tests.rs @@ -9,7 +9,7 @@ use nu_plugin_protocol::{ }; use nu_protocol::{ engine::Sequence, ByteStream, ByteStreamSource, ByteStreamType, DataSource, ListStream, - PipelineData, PipelineMetadata, ShellError, Span, Value, + PipelineData, PipelineMetadata, ShellError, Signals, Span, Value, }; use std::{path::Path, sync::Arc}; @@ -127,7 +127,7 @@ fn read_pipeline_data_empty() -> Result<(), ShellError> { let header = PipelineDataHeader::Empty; assert!(matches!( - manager.read_pipeline_data(header, None)?, + manager.read_pipeline_data(header, &Signals::empty())?, PipelineData::Empty )); Ok(()) @@ -139,7 +139,7 @@ fn read_pipeline_data_value() -> Result<(), ShellError> { let value = Value::test_int(4); let header = PipelineDataHeader::Value(value.clone()); - match manager.read_pipeline_data(header, None)? { + match manager.read_pipeline_data(header, &Signals::empty())? { PipelineData::Value(read_value, ..) => assert_eq!(value, read_value), PipelineData::ListStream(..) => panic!("unexpected ListStream"), PipelineData::ByteStream(..) => panic!("unexpected ByteStream"), @@ -166,7 +166,7 @@ fn read_pipeline_data_list_stream() -> Result<(), ShellError> { span: Span::test_data(), }); - let pipe = manager.read_pipeline_data(header, None)?; + let pipe = manager.read_pipeline_data(header, &Signals::empty())?; assert!( matches!(pipe, PipelineData::ListStream(..)), "unexpected PipelineData: {pipe:?}" @@ -210,7 +210,7 @@ fn read_pipeline_data_byte_stream() -> Result<(), ShellError> { type_: ByteStreamType::Unknown, }); - let pipe = manager.read_pipeline_data(header, None)?; + let pipe = manager.read_pipeline_data(header, &Signals::empty())?; // need to consume input manager.consume_all()?; @@ -255,7 +255,7 @@ fn read_pipeline_data_prepared_properly() -> Result<(), ShellError> { id: 0, span: Span::test_data(), }); - match manager.read_pipeline_data(header, None)? { + match manager.read_pipeline_data(header, &Signals::empty())? { PipelineData::ListStream(_, meta) => match meta { Some(PipelineMetadata { data_source, .. }) => match data_source { DataSource::FilePath(path) => { @@ -351,7 +351,11 @@ fn write_pipeline_data_list_stream() -> Result<(), ShellError> { // Set up pipeline data for a list stream let pipe = PipelineData::ListStream( - ListStream::new(values.clone().into_iter(), Span::test_data(), None), + ListStream::new( + values.clone().into_iter(), + Span::test_data(), + Signals::empty(), + ), None, ); @@ -404,7 +408,7 @@ fn write_pipeline_data_byte_stream() -> Result<(), ShellError> { ByteStream::read( std::io::Cursor::new(expected), span, - None, + Signals::empty(), ByteStreamType::Unknown, ), None, diff --git a/crates/nu-plugin-engine/Cargo.toml b/crates/nu-plugin-engine/Cargo.toml index f86c1ae703..a6dfa471b8 100644 --- a/crates/nu-plugin-engine/Cargo.toml +++ b/crates/nu-plugin-engine/Cargo.toml @@ -16,6 +16,7 @@ nu-protocol = { path = "../nu-protocol", version = "0.95.1" } nu-system = { path = "../nu-system", version = "0.95.1" } nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.95.1" } nu-plugin-core = { path = "../nu-plugin-core", version = "0.95.1", default-features = false } +nu-utils = { path = "../nu-utils", version = "0.95.1" } serde = { workspace = true } log = { workspace = true } @@ -31,4 +32,4 @@ local-socket = ["nu-plugin-core/local-socket"] windows = { workspace = true, features = [ # For setting process creation flags "Win32_System_Threading", -] } \ No newline at end of file +] } diff --git a/crates/nu-plugin-engine/src/context.rs b/crates/nu-plugin-engine/src/context.rs index d5be6ad4b6..aa504a246a 100644 --- a/crates/nu-plugin-engine/src/context.rs +++ b/crates/nu-plugin-engine/src/context.rs @@ -1,29 +1,26 @@ use crate::util::MutableCow; use nu_engine::{get_eval_block_with_early_return, get_full_help, ClosureEvalOnce}; use nu_protocol::{ - ast::Call, - engine::{Closure, EngineState, Redirection, Stack}, - Config, IntoSpanned, OutDest, PipelineData, PluginIdentity, ShellError, Span, Spanned, Value, + engine::{Call, Closure, EngineState, Redirection, Stack}, + Config, IntoSpanned, OutDest, PipelineData, PluginIdentity, ShellError, Signals, Span, Spanned, + Value, }; use std::{ borrow::Cow, collections::HashMap, - sync::{ - atomic::{AtomicBool, AtomicU32}, - Arc, - }, + sync::{atomic::AtomicU32, Arc}, }; /// Object safe trait for abstracting operations required of the plugin context. pub trait PluginExecutionContext: Send + Sync { /// A span pointing to the command being executed fn span(&self) -> Span; - /// The interrupt signal, if present - fn ctrlc(&self) -> Option<&Arc>; + /// The [`Signals`] struct, if present + fn signals(&self) -> &Signals; /// The pipeline externals state, for tracking the foreground process group, if present fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>>; /// Get engine configuration - fn get_config(&self) -> Result; + fn get_config(&self) -> Result, ShellError>; /// Get plugin configuration fn get_plugin_config(&self) -> Result, ShellError>; /// Get an environment variable from `$env` @@ -56,7 +53,7 @@ pub struct PluginExecutionCommandContext<'a> { identity: Arc, engine_state: Cow<'a, EngineState>, stack: MutableCow<'a, Stack>, - call: Cow<'a, Call>, + call: Call<'a>, } impl<'a> PluginExecutionCommandContext<'a> { @@ -64,13 +61,13 @@ impl<'a> PluginExecutionCommandContext<'a> { identity: Arc, engine_state: &'a EngineState, stack: &'a mut Stack, - call: &'a Call, + call: &'a Call<'a>, ) -> PluginExecutionCommandContext<'a> { PluginExecutionCommandContext { identity, engine_state: Cow::Borrowed(engine_state), stack: MutableCow::Borrowed(stack), - call: Cow::Borrowed(call), + call: call.clone(), } } } @@ -80,23 +77,23 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> { self.call.head } - fn ctrlc(&self) -> Option<&Arc> { - self.engine_state.ctrlc.as_ref() + fn signals(&self) -> &Signals { + self.engine_state.signals() } fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>> { Some(&self.engine_state.pipeline_externals_state) } - fn get_config(&self) -> Result { - Ok(nu_engine::get_config(&self.engine_state, &self.stack)) + fn get_config(&self) -> Result, ShellError> { + Ok(self.stack.get_config(&self.engine_state)) } fn get_plugin_config(&self) -> Result, ShellError> { // Fetch the configuration for a plugin // - // The `plugin` must match the registered name of a plugin. For - // `register nu_plugin_example` the plugin config lookup uses `"example"` + // The `plugin` must match the registered name of a plugin. For `plugin add + // nu_plugin_example` the plugin config lookup uses `"example"` Ok(self .get_config()? .plugins @@ -219,7 +216,7 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> { identity: self.identity.clone(), engine_state: Cow::Owned(self.engine_state.clone().into_owned()), stack: self.stack.owned(), - call: Cow::Owned(self.call.clone().into_owned()), + call: self.call.to_owned(), }) } } @@ -234,15 +231,15 @@ impl PluginExecutionContext for PluginExecutionBogusContext { Span::test_data() } - fn ctrlc(&self) -> Option<&Arc> { - None + fn signals(&self) -> &Signals { + &Signals::EMPTY } fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>> { None } - fn get_config(&self) -> Result { + fn get_config(&self) -> Result, ShellError> { Err(ShellError::NushellFailed { msg: "get_config not implemented on bogus".into(), }) diff --git a/crates/nu-plugin-engine/src/declaration.rs b/crates/nu-plugin-engine/src/declaration.rs index d48fa39b85..745ba9a998 100644 --- a/crates/nu-plugin-engine/src/declaration.rs +++ b/crates/nu-plugin-engine/src/declaration.rs @@ -76,7 +76,7 @@ impl Command for PluginDeclaration { EvaluatedCall::try_from_call(call, engine_state, stack, eval_expression)?; // Get the engine config - let engine_config = nu_engine::get_config(engine_state, stack); + let engine_config = stack.get_config(engine_state); // Get, or start, the plugin. let plugin = self diff --git a/crates/nu-plugin-engine/src/interface/mod.rs b/crates/nu-plugin-engine/src/interface/mod.rs index d619826e4b..a82d694d7d 100644 --- a/crates/nu-plugin-engine/src/interface/mod.rs +++ b/crates/nu-plugin-engine/src/interface/mod.rs @@ -12,11 +12,12 @@ use nu_plugin_protocol::{ }; use nu_protocol::{ ast::Operator, engine::Sequence, CustomValue, IntoSpanned, PipelineData, PluginMetadata, - PluginSignature, ShellError, Span, Spanned, Value, + PluginSignature, ShellError, Signals, Span, Spanned, Value, }; +use nu_utils::SharedCow; use std::{ collections::{btree_map, BTreeMap}, - sync::{atomic::AtomicBool, mpsc, Arc, OnceLock}, + sync::{mpsc, Arc, OnceLock}, }; use crate::{ @@ -103,8 +104,8 @@ struct PluginCallState { /// Don't try to send the plugin call response. This is only used for `Dropped` to avoid an /// error dont_send_response: bool, - /// Interrupt signal to be used for stream iterators - ctrlc: Option>, + /// Signals to be used for stream iterators + signals: Signals, /// Channel to receive context on to be used if needed context_rx: Option>, /// Span associated with the call, if any @@ -231,14 +232,14 @@ impl PluginInterfaceManager { } } - /// Find the ctrlc signal corresponding to the given plugin call id - fn get_ctrlc(&mut self, id: PluginCallId) -> Result>, ShellError> { + /// Find the [`Signals`] struct corresponding to the given plugin call id + fn get_signals(&mut self, id: PluginCallId) -> Result { // Make sure we're up to date self.receive_plugin_call_subscriptions(); // Find the subscription and return the context self.plugin_call_states .get(&id) - .map(|state| state.ctrlc.clone()) + .map(|state| state.signals.clone()) .ok_or_else(|| ShellError::PluginFailedToDecode { msg: format!("Unknown plugin call ID: {id}"), }) @@ -517,14 +518,14 @@ impl InterfaceManager for PluginInterfaceManager { // Handle reading the pipeline data, if any let response = response .map_data(|data| { - let ctrlc = self.get_ctrlc(id)?; + let signals = self.get_signals(id)?; // Register the stream in the response if let Some(stream_id) = data.stream_id() { self.recv_stream_started(id, stream_id); } - self.read_pipeline_data(data, ctrlc.as_ref()) + self.read_pipeline_data(data, &signals) }) .unwrap_or_else(|err| { // If there's an error with initializing this stream, change it to a plugin @@ -544,8 +545,8 @@ impl InterfaceManager for PluginInterfaceManager { let call = call // Handle reading the pipeline data, if any .map_data(|input| { - let ctrlc = self.get_ctrlc(context)?; - self.read_pipeline_data(input, ctrlc.as_ref()) + let signals = self.get_signals(context)?; + self.read_pipeline_data(input, &signals) }) // Do anything extra needed for each engine call setup .and_then(|mut engine_call| { @@ -704,7 +705,9 @@ impl PluginInterface { context: Option<&dyn PluginExecutionContext>, ) -> Result { let id = self.state.plugin_call_id_sequence.next()?; - let ctrlc = context.and_then(|c| c.ctrlc().cloned()); + let signals = context + .map(|c| c.signals().clone()) + .unwrap_or_else(Signals::empty); let (tx, rx) = mpsc::channel(); let (context_tx, context_rx) = mpsc::channel(); let keep_plugin_custom_values = mpsc::channel(); @@ -752,7 +755,7 @@ impl PluginInterface { PluginCallState { sender: Some(tx).filter(|_| !dont_send_response), dont_send_response, - ctrlc, + signals, context_rx: Some(context_rx), span: call.span(), keep_plugin_custom_values, @@ -1264,7 +1267,7 @@ pub(crate) fn handle_engine_call( match call { EngineCall::GetConfig => { - let config = Box::new(context.get_config()?); + let config = SharedCow::from(context.get_config()?); Ok(EngineCallResponse::Config(config)) } EngineCall::GetPluginConfig => { diff --git a/crates/nu-plugin-engine/src/interface/tests.rs b/crates/nu-plugin-engine/src/interface/tests.rs index 5665beb92b..2bab67f25f 100644 --- a/crates/nu-plugin-engine/src/interface/tests.rs +++ b/crates/nu-plugin-engine/src/interface/tests.rs @@ -18,7 +18,7 @@ use nu_protocol::{ ast::{Math, Operator}, engine::Closure, ByteStreamType, CustomValue, IntoInterruptiblePipelineData, IntoSpanned, PipelineData, - PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value, + PluginMetadata, PluginSignature, ShellError, Signals, Span, Spanned, Value, }; use serde::{Deserialize, Serialize}; use std::{ @@ -56,7 +56,7 @@ fn manager_consume_all_exits_after_streams_and_interfaces_are_dropped() -> Resul id: 0, span: Span::test_data(), }), - None, + &Signals::empty(), )?; // and an interface... @@ -112,7 +112,7 @@ fn manager_consume_all_propagates_io_error_to_readers() -> Result<(), ShellError id: 0, span: Span::test_data(), }), - None, + &Signals::empty(), )?; manager @@ -159,7 +159,7 @@ fn manager_consume_all_propagates_message_error_to_readers() -> Result<(), Shell span: Span::test_data(), type_: ByteStreamType::Unknown, }), - None, + &Signals::empty(), )?; manager @@ -190,7 +190,7 @@ fn fake_plugin_call( PluginCallState { sender: Some(tx), dont_send_response: false, - ctrlc: None, + signals: Signals::empty(), context_rx: None, span: None, keep_plugin_custom_values: mpsc::channel(), @@ -493,7 +493,7 @@ fn manager_handle_engine_call_after_response_received() -> Result<(), ShellError PluginCallState { sender: None, dont_send_response: false, - ctrlc: None, + signals: Signals::empty(), context_rx: Some(context_rx), span: None, keep_plugin_custom_values: mpsc::channel(), @@ -559,7 +559,7 @@ fn manager_send_plugin_call_response_removes_context_only_if_no_streams_to_read( PluginCallState { sender: None, dont_send_response: false, - ctrlc: None, + signals: Signals::empty(), context_rx: None, span: None, keep_plugin_custom_values: mpsc::channel(), @@ -595,7 +595,7 @@ fn manager_consume_stream_end_removes_context_only_if_last_stream() -> Result<() PluginCallState { sender: None, dont_send_response: false, - ctrlc: None, + signals: Signals::empty(), context_rx: None, span: None, keep_plugin_custom_values: mpsc::channel(), @@ -678,7 +678,7 @@ fn manager_prepare_pipeline_data_adds_source_to_list_streams() -> Result<(), She [Value::test_custom_value(Box::new( test_plugin_custom_value(), ))] - .into_pipeline_data(Span::test_data(), None), + .into_pipeline_data(Span::test_data(), Signals::empty()), )?; let value = data @@ -852,7 +852,9 @@ fn interface_write_plugin_call_writes_run_with_stream_input() -> Result<(), Shel positional: vec![], named: vec![], }, - input: values.clone().into_pipeline_data(Span::test_data(), None), + input: values + .clone() + .into_pipeline_data(Span::test_data(), Signals::empty()), }), None, )?; @@ -1148,7 +1150,9 @@ fn interface_prepare_pipeline_data_accepts_normal_streams() -> Result<(), ShellE let values = normal_values(&interface); let state = CurrentCallState::default(); let data = interface.prepare_pipeline_data( - values.clone().into_pipeline_data(Span::test_data(), None), + values + .clone() + .into_pipeline_data(Span::test_data(), Signals::empty()), &state, )?; @@ -1211,7 +1215,9 @@ fn interface_prepare_pipeline_data_rejects_bad_custom_value_in_a_stream() -> Res let values = bad_custom_values(); let state = CurrentCallState::default(); let data = interface.prepare_pipeline_data( - values.clone().into_pipeline_data(Span::test_data(), None), + values + .clone() + .into_pipeline_data(Span::test_data(), Signals::empty()), &state, )?; diff --git a/crates/nu-plugin-protocol/src/evaluated_call.rs b/crates/nu-plugin-protocol/src/evaluated_call.rs index 19f9049340..58f8987865 100644 --- a/crates/nu-plugin-protocol/src/evaluated_call.rs +++ b/crates/nu-plugin-protocol/src/evaluated_call.rs @@ -1,7 +1,7 @@ use nu_protocol::{ - ast::{Call, Expression}, - engine::{EngineState, Stack}, - FromValue, ShellError, Span, Spanned, Value, + ast::{self, Expression}, + engine::{Call, CallImpl, EngineState, Stack}, + ir, FromValue, ShellError, Span, Spanned, Value, }; use serde::{Deserialize, Serialize}; @@ -33,6 +33,24 @@ impl EvaluatedCall { engine_state: &EngineState, stack: &mut Stack, eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result, + ) -> Result { + match &call.inner { + CallImpl::AstRef(call) => { + Self::try_from_ast_call(call, engine_state, stack, eval_expression_fn) + } + CallImpl::AstBox(call) => { + Self::try_from_ast_call(call, engine_state, stack, eval_expression_fn) + } + CallImpl::IrRef(call) => Self::try_from_ir_call(call, stack), + CallImpl::IrBox(call) => Self::try_from_ir_call(call, stack), + } + } + + fn try_from_ast_call( + call: &ast::Call, + engine_state: &EngineState, + stack: &mut Stack, + eval_expression_fn: fn(&EngineState, &mut Stack, &Expression) -> Result, ) -> Result { let positional = call.rest_iter_flattened(0, |expr| eval_expression_fn(engine_state, stack, expr))?; @@ -54,6 +72,22 @@ impl EvaluatedCall { }) } + fn try_from_ir_call(call: &ir::Call, stack: &Stack) -> Result { + let positional = call.rest_iter_flattened(stack, 0)?; + + let mut named = Vec::with_capacity(call.named_len(stack)); + named.extend( + call.named_iter(stack) + .map(|(name, value)| (name.map(|s| s.to_owned()), value.cloned())), + ); + + Ok(Self { + head: call.head, + positional, + named, + }) + } + /// Check if a flag (named parameter that does not take a value) is set /// Returns Ok(true) if flag is set or passed true value /// Returns Ok(false) if flag is not set or passed false value diff --git a/crates/nu-plugin-protocol/src/lib.rs b/crates/nu-plugin-protocol/src/lib.rs index 5d52404dce..336a11ba91 100644 --- a/crates/nu-plugin-protocol/src/lib.rs +++ b/crates/nu-plugin-protocol/src/lib.rs @@ -25,6 +25,7 @@ use nu_protocol::{ ast::Operator, engine::Closure, ByteStreamType, Config, LabeledError, PipelineData, PluginMetadata, PluginSignature, ShellError, Span, Spanned, Value, }; +use nu_utils::SharedCow; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -555,7 +556,7 @@ impl EngineCall { pub enum EngineCallResponse { Error(ShellError), PipelineData(D), - Config(Box), + Config(SharedCow), ValueMap(HashMap), } diff --git a/crates/nu-plugin-test-support/src/lib.rs b/crates/nu-plugin-test-support/src/lib.rs index a53b353981..998c31ac0d 100644 --- a/crates/nu-plugin-test-support/src/lib.rs +++ b/crates/nu-plugin-test-support/src/lib.rs @@ -8,8 +8,8 @@ //! use nu_plugin::*; //! use nu_plugin_test_support::PluginTest; //! use nu_protocol::{ -//! Example, IntoInterruptiblePipelineData, LabeledError, PipelineData, ShellError, Signature, -//! Span, Type, Value, +//! Example, IntoInterruptiblePipelineData, LabeledError, PipelineData, ShellError, Signals, +//! Signature, Span, Type, Value, //! }; //! //! struct LowercasePlugin; @@ -60,7 +60,7 @@ //! // Errors in a stream should be returned as values. //! .unwrap_or_else(|err| Value::error(err, span)) //! }, -//! None, +//! &Signals::empty(), //! )?) //! } //! } @@ -83,7 +83,7 @@ //! //! // #[test] //! fn test_lowercase() -> Result<(), ShellError> { -//! let input = vec![Value::test_string("FooBar")].into_pipeline_data(Span::test_data(), None); +//! let input = vec![Value::test_string("FooBar")].into_pipeline_data(Span::test_data(), Signals::empty()); //! let output = PluginTest::new("lowercase", LowercasePlugin.into())? //! .eval_with("lowercase", input)? //! .into_value(Span::test_data())?; diff --git a/crates/nu-plugin-test-support/src/plugin_test.rs b/crates/nu-plugin-test-support/src/plugin_test.rs index 3d6b3eec23..5e3334c78b 100644 --- a/crates/nu-plugin-test-support/src/plugin_test.rs +++ b/crates/nu-plugin-test-support/src/plugin_test.rs @@ -11,7 +11,7 @@ use nu_protocol::{ debugger::WithoutDebug, engine::{EngineState, Stack, StateWorkingSet}, report_error_new, CustomValue, Example, IntoSpanned as _, LabeledError, PipelineData, - ShellError, Span, Value, + ShellError, Signals, Span, Value, }; use crate::{diff::diff_by_line, fake_register::fake_register}; @@ -85,13 +85,13 @@ impl PluginTest { /// /// ```rust,no_run /// # use nu_plugin_test_support::PluginTest; - /// # use nu_protocol::{ShellError, Span, Value, IntoInterruptiblePipelineData}; + /// # use nu_protocol::{IntoInterruptiblePipelineData, ShellError, Signals, Span, Value}; /// # use nu_plugin::*; /// # fn test(MyPlugin: impl Plugin + Send + 'static) -> Result<(), ShellError> { /// let result = PluginTest::new("my_plugin", MyPlugin.into())? /// .eval_with( /// "my-command", - /// vec![Value::test_int(42)].into_pipeline_data(Span::test_data(), None) + /// vec![Value::test_int(42)].into_pipeline_data(Span::test_data(), Signals::empty()) /// )? /// .into_value(Span::test_data())?; /// assert_eq!(Value::test_string("42"), result); @@ -151,7 +151,7 @@ impl PluginTest { Err(err) => Value::error(err, value.span()), } }, - None, + &Signals::empty(), )? }; @@ -171,7 +171,7 @@ impl PluginTest { Err(err) => Value::error(err, value.span()), } }, - None, + &Signals::empty(), ) } } diff --git a/crates/nu-plugin-test-support/tests/lowercase/mod.rs b/crates/nu-plugin-test-support/tests/lowercase/mod.rs index 50271a8cc2..5be127e7f5 100644 --- a/crates/nu-plugin-test-support/tests/lowercase/mod.rs +++ b/crates/nu-plugin-test-support/tests/lowercase/mod.rs @@ -1,8 +1,8 @@ use nu_plugin::*; use nu_plugin_test_support::PluginTest; use nu_protocol::{ - Example, IntoInterruptiblePipelineData, LabeledError, PipelineData, ShellError, Signature, - Span, Type, Value, + Example, IntoInterruptiblePipelineData, LabeledError, PipelineData, ShellError, Signals, + Signature, Span, Type, Value, }; struct LowercasePlugin; @@ -53,7 +53,7 @@ impl PluginCommand for Lowercase { // Errors in a stream should be returned as values. .unwrap_or_else(|err| Value::error(err, span)) }, - None, + &Signals::empty(), )?) } } @@ -72,7 +72,8 @@ impl Plugin for LowercasePlugin { fn test_lowercase_using_eval_with() -> Result<(), ShellError> { let result = PluginTest::new("lowercase", LowercasePlugin.into())?.eval_with( "lowercase", - vec![Value::test_string("HeLlO wOrLd")].into_pipeline_data(Span::test_data(), None), + vec![Value::test_string("HeLlO wOrLd")] + .into_pipeline_data(Span::test_data(), Signals::empty()), )?; assert_eq!( diff --git a/crates/nu-plugin/Cargo.toml b/crates/nu-plugin/Cargo.toml index a3b4455d15..d46a3cc9cb 100644 --- a/crates/nu-plugin/Cargo.toml +++ b/crates/nu-plugin/Cargo.toml @@ -15,6 +15,7 @@ nu-engine = { path = "../nu-engine", version = "0.95.1" } nu-protocol = { path = "../nu-protocol", version = "0.95.1" } nu-plugin-protocol = { path = "../nu-plugin-protocol", version = "0.95.1" } nu-plugin-core = { path = "../nu-plugin-core", version = "0.95.1", default-features = false } +nu-utils = { path = "../nu-utils", version = "0.95.1" } log = { workspace = true } thiserror = "1.0" @@ -29,4 +30,4 @@ local-socket = ["nu-plugin-core/local-socket"] [target.'cfg(target_family = "unix")'.dependencies] # For setting the process group ID (EnterForeground / LeaveForeground) -nix = { workspace = true, default-features = false, features = ["process"] } \ No newline at end of file +nix = { workspace = true, default-features = false, features = ["process"] } diff --git a/crates/nu-plugin/src/plugin/command.rs b/crates/nu-plugin/src/plugin/command.rs index 7ccedfd4e5..a90555ac40 100644 --- a/crates/nu-plugin/src/plugin/command.rs +++ b/crates/nu-plugin/src/plugin/command.rs @@ -22,7 +22,7 @@ use crate::{EngineInterface, EvaluatedCall, Plugin}; /// Basic usage: /// ``` /// # use nu_plugin::*; -/// # use nu_protocol::{Signature, PipelineData, Type, Value, LabeledError}; +/// # use nu_protocol::{LabeledError, PipelineData, Signals, Signature, Type, Value}; /// struct LowercasePlugin; /// struct Lowercase; /// @@ -55,7 +55,7 @@ use crate::{EngineInterface, EvaluatedCall, Plugin}; /// .map(|string| Value::string(string.to_lowercase(), span)) /// // Errors in a stream should be returned as values. /// .unwrap_or_else(|err| Value::error(err, span)) -/// }, None)?) +/// }, &Signals::empty())?) /// } /// } /// @@ -340,7 +340,7 @@ where /// Build a [`PluginSignature`] from the signature-related methods on [`PluginCommand`]. /// -/// This is sent to the engine on `register`. +/// This is sent to the engine on `plugin add`. /// /// This is not a public API. #[doc(hidden)] diff --git a/crates/nu-plugin/src/plugin/interface/mod.rs b/crates/nu-plugin/src/plugin/interface/mod.rs index a18653273e..76abb83a56 100644 --- a/crates/nu-plugin/src/plugin/interface/mod.rs +++ b/crates/nu-plugin/src/plugin/interface/mod.rs @@ -12,9 +12,10 @@ use nu_plugin_protocol::{ }; use nu_protocol::{ engine::{ctrlc, Closure, Sequence}, - Config, LabeledError, PipelineData, PluginMetadata, PluginSignature, ShellError, Span, Spanned, - Value, + Config, LabeledError, PipelineData, PluginMetadata, PluginSignature, ShellError, Signals, Span, + Spanned, Value, }; +use nu_utils::SharedCow; use std::{ collections::{btree_map, BTreeMap, HashMap}, sync::{mpsc, Arc}, @@ -278,7 +279,9 @@ impl InterfaceManager for EngineInterfaceManager { PluginInput::Call(id, call) => { let interface = self.interface_for_context(id); // Read streams in the input - let call = match call.map_data(|input| self.read_pipeline_data(input, None)) { + let call = match call + .map_data(|input| self.read_pipeline_data(input, &Signals::empty())) + { Ok(call) => call, Err(err) => { // If there's an error with initialization of the input stream, just send @@ -324,7 +327,7 @@ impl InterfaceManager for EngineInterfaceManager { } PluginInput::EngineCallResponse(id, response) => { let response = response - .map_data(|header| self.read_pipeline_data(header, None)) + .map_data(|header| self.read_pipeline_data(header, &Signals::empty())) .unwrap_or_else(|err| { // If there's an error with initializing this stream, change it to an engine // call error response, but send it anyway @@ -540,9 +543,9 @@ impl EngineInterface { /// # Ok(()) /// # } /// ``` - pub fn get_config(&self) -> Result, ShellError> { + pub fn get_config(&self) -> Result, ShellError> { match self.engine_call(EngineCall::GetConfig)? { - EngineCallResponse::Config(config) => Ok(config), + EngineCallResponse::Config(config) => Ok(SharedCow::into_arc(config)), EngineCallResponse::Error(err) => Err(err), _ => Err(ShellError::PluginFailedToDecode { msg: "Received unexpected response for EngineCall::GetConfig".into(), diff --git a/crates/nu-plugin/src/plugin/interface/tests.rs b/crates/nu-plugin/src/plugin/interface/tests.rs index b195b43197..7065c6f64d 100644 --- a/crates/nu-plugin/src/plugin/interface/tests.rs +++ b/crates/nu-plugin/src/plugin/interface/tests.rs @@ -10,7 +10,7 @@ use nu_plugin_protocol::{ }; use nu_protocol::{ engine::Closure, ByteStreamType, Config, CustomValue, IntoInterruptiblePipelineData, - LabeledError, PipelineData, PluginSignature, ShellError, Span, Spanned, Value, + LabeledError, PipelineData, PluginSignature, ShellError, Signals, Span, Spanned, Value, }; use std::{ collections::HashMap, @@ -59,7 +59,7 @@ fn manager_consume_all_exits_after_streams_and_interfaces_are_dropped() -> Resul id: 0, span: Span::test_data(), }), - None, + &Signals::empty(), )?; // and an interface... @@ -115,7 +115,7 @@ fn manager_consume_all_propagates_io_error_to_readers() -> Result<(), ShellError id: 0, span: Span::test_data(), }), - None, + &Signals::empty(), )?; manager @@ -162,7 +162,7 @@ fn manager_consume_all_propagates_message_error_to_readers() -> Result<(), Shell span: Span::test_data(), type_: ByteStreamType::Unknown, }), - None, + &Signals::empty(), )?; manager @@ -615,7 +615,7 @@ fn manager_prepare_pipeline_data_deserializes_custom_values_in_streams() -> Resu [Value::test_custom_value(Box::new( test_plugin_custom_value(), ))] - .into_pipeline_data(Span::test_data(), None), + .into_pipeline_data(Span::test_data(), Signals::empty()), )?; let value = data @@ -647,7 +647,7 @@ fn manager_prepare_pipeline_data_embeds_deserialization_errors_in_streams() -> R let span = Span::new(20, 30); let data = manager.prepare_pipeline_data( [Value::custom(Box::new(invalid_custom_value), span)] - .into_pipeline_data(Span::test_data(), None), + .into_pipeline_data(Span::test_data(), Signals::empty()), )?; let value = data @@ -730,7 +730,7 @@ fn interface_write_response_with_stream() -> Result<(), ShellError> { interface .write_response(Ok::<_, ShellError>( [Value::test_int(3), Value::test_int(4), Value::test_int(5)] - .into_pipeline_data(Span::test_data(), None), + .into_pipeline_data(Span::test_data(), Signals::empty()), ))? .write()?; @@ -1132,7 +1132,7 @@ fn interface_prepare_pipeline_data_serializes_custom_values_in_streams() -> Resu [Value::test_custom_value(Box::new( expected_test_custom_value(), ))] - .into_pipeline_data(Span::test_data(), None), + .into_pipeline_data(Span::test_data(), Signals::empty()), &(), )?; @@ -1191,7 +1191,7 @@ fn interface_prepare_pipeline_data_embeds_serialization_errors_in_streams() -> R let span = Span::new(40, 60); let data = interface.prepare_pipeline_data( [Value::custom(Box::new(CantSerialize::BadVariant), span)] - .into_pipeline_data(Span::test_data(), None), + .into_pipeline_data(Span::test_data(), Signals::empty()), &(), )?; diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs index 997381f97b..d809318d68 100644 --- a/crates/nu-plugin/src/plugin/mod.rs +++ b/crates/nu-plugin/src/plugin/mod.rs @@ -38,7 +38,7 @@ pub(crate) const OUTPUT_BUFFER_SIZE: usize = 16384; /// The API for a Nushell plugin /// /// A plugin defines multiple commands, which are added to the engine when the user calls -/// `register`. +/// `plugin add`. /// /// The plugin must be able to be safely shared between threads, so that multiple invocations can /// be run in parallel. If interior mutability is desired, consider synchronization primitives such @@ -670,7 +670,7 @@ fn print_help(plugin: &impl Plugin, encoder: impl PluginEncoder) { } }) .and_then(|_| { - let flags = get_flags_section(None, &signature, |v| format!("{:#?}", v)); + let flags = get_flags_section(None, None, &signature, |v| format!("{:#?}", v)); write!(help, "{flags}") }) .and_then(|_| writeln!(help, "\nParameters:")) diff --git a/crates/nu-protocol/Cargo.toml b/crates/nu-protocol/Cargo.toml index ee0f5a8221..eaa861a073 100644 --- a/crates/nu-protocol/Cargo.toml +++ b/crates/nu-protocol/Cargo.toml @@ -33,6 +33,7 @@ serde = { workspace = true, default-features = false } thiserror = "1.0" typetag = "0.2" os_pipe = { workspace = true, features = ["io_safety"] } +log = { workspace = true } [target.'cfg(unix)'.dependencies] nix = { workspace = true, default-features = false, features = ["signal"] } @@ -54,4 +55,4 @@ tempfile = { workspace = true } os_pipe = { workspace = true } [package.metadata.docs.rs] -all-features = true \ No newline at end of file +all-features = true diff --git a/crates/nu-protocol/src/alias.rs b/crates/nu-protocol/src/alias.rs index 24448225d4..701543c57b 100644 --- a/crates/nu-protocol/src/alias.rs +++ b/crates/nu-protocol/src/alias.rs @@ -1,13 +1,19 @@ use crate::{ - ast::{Call, Expression}, - engine::{Command, CommandType, EngineState, Stack}, + ast::Expression, + engine::{Call, Command, CommandType, EngineState, Stack}, PipelineData, ShellError, Signature, }; +/// Command wrapper of an alias. +/// +/// Our current aliases are implemented as wrapping commands +/// This has some limitations compared to text-substitution macro aliases but can reliably use more +/// of our machinery #[derive(Clone)] pub struct Alias { pub name: String, - pub command: Option>, // None if external call + /// Wrapped inner [`Command`]. `None` if alias of external call + pub command: Option>, pub wrapped_call: Expression, pub usage: String, pub extra_usage: String, diff --git a/crates/nu-protocol/src/ast/block.rs b/crates/nu-protocol/src/ast/block.rs index 6e3449af26..8f62ff99ba 100644 --- a/crates/nu-protocol/src/ast/block.rs +++ b/crates/nu-protocol/src/ast/block.rs @@ -1,5 +1,5 @@ use super::Pipeline; -use crate::{engine::EngineState, OutDest, Signature, Span, Type, VarId}; +use crate::{engine::StateWorkingSet, ir::IrBlock, OutDest, Signature, Span, Type, VarId}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -8,6 +8,8 @@ pub struct Block { pub pipelines: Vec, pub captures: Vec, pub redirect_env: bool, + /// The block compiled to IR instructions. Not available for subexpressions. + pub ir_block: Option, pub span: Option, // None option encodes no span to avoid using test_span() } @@ -22,10 +24,10 @@ impl Block { pub fn pipe_redirection( &self, - engine_state: &EngineState, + working_set: &StateWorkingSet, ) -> (Option, Option) { if let Some(first) = self.pipelines.first() { - first.pipe_redirection(engine_state) + first.pipe_redirection(working_set) } else { (None, None) } @@ -45,6 +47,7 @@ impl Block { pipelines: vec![], captures: vec![], redirect_env: false, + ir_block: None, span: None, } } @@ -55,6 +58,7 @@ impl Block { pipelines: Vec::with_capacity(capacity), captures: vec![], redirect_env: false, + ir_block: None, span: None, } } @@ -86,6 +90,7 @@ where pipelines: pipelines.collect(), captures: vec![], redirect_env: false, + ir_block: None, span: None, } } diff --git a/crates/nu-protocol/src/ast/call.rs b/crates/nu-protocol/src/ast/call.rs index 6e8cc64a05..d26d0af300 100644 --- a/crates/nu-protocol/src/ast/call.rs +++ b/crates/nu-protocol/src/ast/call.rs @@ -7,12 +7,30 @@ use crate::{ ShellError, Span, Spanned, Value, }; +/// Parsed command arguments +/// +/// Primarily for internal commands #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Argument { + /// A positional argument (that is not [`Argument::Spread`]) + /// + /// ```nushell + /// my_cmd positional + /// ``` Positional(Expression), + /// A named/flag argument that can optionally receive a [`Value`] as an [`Expression`] + /// + /// The optional second `Spanned` refers to the short-flag version if used + /// ```nushell + /// my_cmd --flag + /// my_cmd -f + /// my_cmd --flag-with-value + /// ``` Named((Spanned, Option>, Option)), - Unknown(Expression), // unknown argument used in "fall-through" signatures - Spread(Expression), // a list spread to fill in rest arguments + /// unknown argument used in "fall-through" signatures + Unknown(Expression), + /// a list spread to fill in rest arguments + Spread(Expression), } impl Argument { @@ -47,12 +65,24 @@ impl Argument { } } +/// Argument passed to an external command +/// +/// Here the parsing rules slightly differ to directly pass strings to the external process #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ExternalArgument { + /// Expression that needs to be evaluated to turn into an external process argument Regular(Expression), + /// Occurrence of a `...` spread operator that needs to be expanded Spread(Expression), } +/// Parsed call of a `Command` +/// +/// As we also implement some internal keywords in terms of the `Command` trait, this type stores the passed arguments as [`Expression`]. +/// Some of its methods lazily evaluate those to [`Value`] while others return the underlying +/// [`Expression`]. +/// +/// For further utilities check the `nu_engine::CallExt` trait that extends [`Call`] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Call { /// identifier of the declaration to call diff --git a/crates/nu-protocol/src/ast/cell_path.rs b/crates/nu-protocol/src/ast/cell_path.rs index 3de37f3992..0880537557 100644 --- a/crates/nu-protocol/src/ast/cell_path.rs +++ b/crates/nu-protocol/src/ast/cell_path.rs @@ -3,16 +3,23 @@ use crate::Span; use serde::{Deserialize, Serialize}; use std::{cmp::Ordering, fmt::Display}; +/// One level of access of a [`CellPath`] #[derive(Debug, Clone, Serialize, Deserialize)] pub enum PathMember { + /// Accessing a member by string (i.e. columns of a table or [`Record`](crate::Record)) String { val: String, span: Span, + /// If marked as optional don't throw an error if not found but perform default handling + /// (e.g. return `Value::Nothing`) optional: bool, }, + /// Accessing a member by index (i.e. row of a table or item in a list) Int { val: usize, span: Span, + /// If marked as optional don't throw an error if not found but perform default handling + /// (e.g. return `Value::Nothing`) optional: bool, }, } @@ -143,6 +150,18 @@ impl PartialOrd for PathMember { } } +/// Represents the potentially nested access to fields/cells of a container type +/// +/// In our current implementation for table access the order of row/column is commutative. +/// This limits the number of possible rows to select in one [`CellPath`] to 1 as it could +/// otherwise be ambiguous +/// +/// ```nushell +/// col1.0 +/// 0.col1 +/// col2 +/// 42 +/// ``` #[derive(Debug, Clone, PartialEq, PartialOrd, Serialize, Deserialize)] pub struct CellPath { pub members: Vec, diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index 0e561e5c8f..fadbffc51f 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -5,8 +5,11 @@ use super::{ Call, CellPath, Expression, ExternalArgument, FullCellPath, Keyword, MatchPattern, Operator, Range, Table, ValueWithUnit, }; -use crate::{ast::ImportPattern, engine::EngineState, BlockId, OutDest, Signature, Span, VarId}; +use crate::{ + ast::ImportPattern, engine::StateWorkingSet, BlockId, OutDest, Signature, Span, VarId, +}; +/// An [`Expression`] AST node #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Expr { Bool(bool), @@ -60,17 +63,17 @@ const _: () = assert!(std::mem::size_of::() <= 40); impl Expr { pub fn pipe_redirection( &self, - engine_state: &EngineState, + working_set: &StateWorkingSet, ) -> (Option, Option) { // 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) => engine_state.get_decl(call.decl_id).pipe_redirection(), - Expr::Subexpression(block_id) | Expr::Block(block_id) => engine_state + Expr::Call(call) => working_set.get_decl(call.decl_id).pipe_redirection(), + Expr::Subexpression(block_id) | Expr::Block(block_id) => working_set .get_block(*block_id) - .pipe_redirection(engine_state), - Expr::FullCellPath(cell_path) => cell_path.head.expr.pipe_redirection(engine_state), + .pipe_redirection(working_set), + Expr::FullCellPath(cell_path) => cell_path.head.expr.pipe_redirection(working_set), Expr::Bool(_) | Expr::Int(_) | Expr::Float(_) @@ -125,6 +128,7 @@ impl Expr { } } +/// Expressions permitted inside a record expression/literal #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum RecordItem { /// A key: val mapping @@ -133,6 +137,7 @@ pub enum RecordItem { Spread(Span, Expression), } +/// Expressions permitted inside a list expression/literal #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ListItem { /// A normal expression diff --git a/crates/nu-protocol/src/ast/expression.rs b/crates/nu-protocol/src/ast/expression.rs index 040e140cab..dfd4187a90 100644 --- a/crates/nu-protocol/src/ast/expression.rs +++ b/crates/nu-protocol/src/ast/expression.rs @@ -6,6 +6,7 @@ use crate::{ use serde::{Deserialize, Serialize}; use std::sync::Arc; +/// Wrapper around [`Expr`] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Expression { pub expr: Expr, @@ -27,42 +28,9 @@ impl Expression { } } - pub fn precedence(&self) -> usize { + pub fn precedence(&self) -> u8 { match &self.expr { - Expr::Operator(operator) => { - use super::operator::*; - // Higher precedence binds tighter - - match operator { - Operator::Math(Math::Pow) => 100, - Operator::Math(Math::Multiply) - | Operator::Math(Math::Divide) - | Operator::Math(Math::Modulo) - | Operator::Math(Math::FloorDivision) => 95, - Operator::Math(Math::Plus) | Operator::Math(Math::Minus) => 90, - Operator::Bits(Bits::ShiftLeft) | Operator::Bits(Bits::ShiftRight) => 85, - Operator::Comparison(Comparison::NotRegexMatch) - | Operator::Comparison(Comparison::RegexMatch) - | Operator::Comparison(Comparison::StartsWith) - | Operator::Comparison(Comparison::EndsWith) - | Operator::Comparison(Comparison::LessThan) - | Operator::Comparison(Comparison::LessThanOrEqual) - | Operator::Comparison(Comparison::GreaterThan) - | Operator::Comparison(Comparison::GreaterThanOrEqual) - | Operator::Comparison(Comparison::Equal) - | Operator::Comparison(Comparison::NotEqual) - | Operator::Comparison(Comparison::In) - | Operator::Comparison(Comparison::NotIn) - | Operator::Math(Math::Append) => 80, - Operator::Bits(Bits::BitAnd) => 75, - Operator::Bits(Bits::BitXor) => 70, - Operator::Bits(Bits::BitOr) => 60, - Operator::Boolean(Boolean::And) => 50, - Operator::Boolean(Boolean::Xor) => 45, - Operator::Boolean(Boolean::Or) => 40, - Operator::Assignment(_) => 10, - } - } + Expr::Operator(operator) => operator.precedence(), _ => 0, } } diff --git a/crates/nu-protocol/src/ast/import_pattern.rs b/crates/nu-protocol/src/ast/import_pattern.rs index 893dd9897b..ad08bfb2d1 100644 --- a/crates/nu-protocol/src/ast/import_pattern.rs +++ b/crates/nu-protocol/src/ast/import_pattern.rs @@ -3,10 +3,14 @@ use serde::{Deserialize, Serialize}; use crate::{ModuleId, Span, VarId}; use std::collections::HashSet; +/// possible patterns after the first module level in an [`ImportPattern`]. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum ImportPatternMember { + /// Wildcard import of items Glob { span: Span }, + /// single specific module or item Name { name: Vec, span: Span }, + /// list of items List { names: Vec<(Vec, Span)> }, } @@ -31,6 +35,7 @@ impl ImportPatternMember { } } +/// The first item of a `use` statement needs to specify an explicit module #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ImportPatternHead { pub name: Vec, @@ -38,6 +43,7 @@ pub struct ImportPatternHead { pub span: Span, } +/// The pattern specifying modules in a `use` statement #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct ImportPattern { pub head: ImportPatternHead, diff --git a/crates/nu-protocol/src/ast/match_pattern.rs b/crates/nu-protocol/src/ast/match_pattern.rs index b8f87c3f63..ca639270aa 100644 --- a/crates/nu-protocol/src/ast/match_pattern.rs +++ b/crates/nu-protocol/src/ast/match_pattern.rs @@ -2,10 +2,11 @@ use super::Expression; use crate::{Span, VarId}; use serde::{Deserialize, Serialize}; +/// AST Node for match arm with optional match guard #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct MatchPattern { pub pattern: Pattern, - pub guard: Option, + pub guard: Option>, pub span: Span, } @@ -15,16 +16,28 @@ impl MatchPattern { } } +/// AST Node for pattern matching rules #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Pattern { + /// Destructuring of records Record(Vec<(String, MatchPattern)>), + /// List destructuring List(Vec), - Value(Expression), + /// Matching against a literal + // TODO: it would be nice if this didn't depend on AST + // maybe const evaluation can get us to a Value instead? + Value(Box), + /// binding to a variable Variable(VarId), + /// the `pattern1 \ pattern2` or-pattern Or(Vec), - Rest(VarId), // the ..$foo pattern - IgnoreRest, // the .. pattern - IgnoreValue, // the _ pattern + /// the `..$foo` pattern + Rest(VarId), + /// the `..` pattern + IgnoreRest, + /// the `_` pattern + IgnoreValue, + /// Failed parsing of a pattern Garbage, } diff --git a/crates/nu-protocol/src/ast/mod.rs b/crates/nu-protocol/src/ast/mod.rs index 7c627997fe..cb09168805 100644 --- a/crates/nu-protocol/src/ast/mod.rs +++ b/crates/nu-protocol/src/ast/mod.rs @@ -1,3 +1,4 @@ +//! Types representing parsed Nushell code (the Abstract Syntax Tree) mod block; mod call; mod cell_path; @@ -9,7 +10,7 @@ mod match_pattern; mod operator; mod pipeline; mod range; -pub mod table; +mod table; mod unit; mod value_with_unit; diff --git a/crates/nu-protocol/src/ast/operator.rs b/crates/nu-protocol/src/ast/operator.rs index 46484761bc..713c6c0060 100644 --- a/crates/nu-protocol/src/ast/operator.rs +++ b/crates/nu-protocol/src/ast/operator.rs @@ -68,6 +68,40 @@ pub enum Operator { Assignment(Assignment), } +impl Operator { + pub fn precedence(&self) -> u8 { + match self { + Self::Math(Math::Pow) => 100, + Self::Math(Math::Multiply) + | Self::Math(Math::Divide) + | Self::Math(Math::Modulo) + | Self::Math(Math::FloorDivision) => 95, + Self::Math(Math::Plus) | Self::Math(Math::Minus) => 90, + Self::Bits(Bits::ShiftLeft) | Self::Bits(Bits::ShiftRight) => 85, + Self::Comparison(Comparison::NotRegexMatch) + | Self::Comparison(Comparison::RegexMatch) + | Self::Comparison(Comparison::StartsWith) + | Self::Comparison(Comparison::EndsWith) + | Self::Comparison(Comparison::LessThan) + | Self::Comparison(Comparison::LessThanOrEqual) + | Self::Comparison(Comparison::GreaterThan) + | Self::Comparison(Comparison::GreaterThanOrEqual) + | Self::Comparison(Comparison::Equal) + | Self::Comparison(Comparison::NotEqual) + | Self::Comparison(Comparison::In) + | Self::Comparison(Comparison::NotIn) + | Self::Math(Math::Append) => 80, + Self::Bits(Bits::BitAnd) => 75, + Self::Bits(Bits::BitXor) => 70, + Self::Bits(Bits::BitOr) => 60, + Self::Boolean(Boolean::And) => 50, + Self::Boolean(Boolean::Xor) => 45, + Self::Boolean(Boolean::Or) => 40, + Self::Assignment(_) => 10, + } + } +} + impl Display for Operator { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -95,10 +129,10 @@ impl Display for Operator { Operator::Math(Math::Multiply) => write!(f, "*"), Operator::Math(Math::Divide) => write!(f, "/"), Operator::Math(Math::Modulo) => write!(f, "mod"), - Operator::Math(Math::FloorDivision) => write!(f, "fdiv"), + Operator::Math(Math::FloorDivision) => write!(f, "//"), Operator::Math(Math::Pow) => write!(f, "**"), - Operator::Boolean(Boolean::And) => write!(f, "&&"), - Operator::Boolean(Boolean::Or) => write!(f, "||"), + Operator::Boolean(Boolean::And) => write!(f, "and"), + Operator::Boolean(Boolean::Or) => write!(f, "or"), Operator::Boolean(Boolean::Xor) => write!(f, "xor"), Operator::Bits(Bits::BitOr) => write!(f, "bit-or"), Operator::Bits(Bits::BitXor) => write!(f, "bit-xor"), diff --git a/crates/nu-protocol/src/ast/pipeline.rs b/crates/nu-protocol/src/ast/pipeline.rs index f03c016daf..3f2a216485 100644 --- a/crates/nu-protocol/src/ast/pipeline.rs +++ b/crates/nu-protocol/src/ast/pipeline.rs @@ -1,8 +1,4 @@ -use crate::{ - ast::Expression, - engine::{EngineState, StateWorkingSet}, - OutDest, Span, -}; +use crate::{ast::Expression, engine::StateWorkingSet, OutDest, Span}; use serde::{Deserialize, Serialize}; use std::fmt::Display; @@ -120,9 +116,9 @@ impl PipelineElement { pub fn pipe_redirection( &self, - engine_state: &EngineState, + working_set: &StateWorkingSet, ) -> (Option, Option) { - self.expr.expr.pipe_redirection(engine_state) + self.expr.expr.pipe_redirection(working_set) } } @@ -166,10 +162,10 @@ impl Pipeline { pub fn pipe_redirection( &self, - engine_state: &EngineState, + working_set: &StateWorkingSet, ) -> (Option, Option) { if let Some(first) = self.elements.first() { - first.pipe_redirection(engine_state) + first.pipe_redirection(working_set) } else { (None, None) } diff --git a/crates/nu-protocol/src/config/mod.rs b/crates/nu-protocol/src/config/mod.rs index 5aa611937b..eda3d9a15e 100644 --- a/crates/nu-protocol/src/config/mod.rs +++ b/crates/nu-protocol/src/config/mod.rs @@ -1,3 +1,4 @@ +//! Module containing the internal representation of user configuration use self::completer::*; use self::helper::*; use self::hooks::*; @@ -104,7 +105,7 @@ pub struct Config { /// Configuration for plugins. /// /// Users can provide configuration for a plugin through this entry. The entry name must - /// match the registered plugin name so `register nu_plugin_example` will be able to place + /// match the registered plugin name so `plugin add nu_plugin_example` will be able to place /// its configuration under a `nu_plugin_example` column. pub plugins: HashMap, /// Configuration for plugin garbage collection. diff --git a/crates/nu-protocol/src/debugger/debugger_trait.rs b/crates/nu-protocol/src/debugger/debugger_trait.rs index 69d395fb98..88cc753548 100644 --- a/crates/nu-protocol/src/debugger/debugger_trait.rs +++ b/crates/nu-protocol/src/debugger/debugger_trait.rs @@ -16,6 +16,7 @@ use crate::{ ast::{Block, PipelineElement}, engine::EngineState, + ir::IrBlock, PipelineData, ShellError, Span, Value, }; use std::{fmt::Debug, ops::DerefMut}; @@ -35,11 +36,11 @@ pub trait DebugContext: Clone + Copy + Debug { #[allow(unused_variables)] fn leave_block(engine_state: &EngineState, block: &Block) {} - /// Called when the evaluator enters a pipeline element + /// Called when the AST evaluator enters a pipeline element #[allow(unused_variables)] fn enter_element(engine_state: &EngineState, element: &PipelineElement) {} - /// Called when the evaluator leaves a pipeline element + /// Called when the AST evaluator leaves a pipeline element #[allow(unused_variables)] fn leave_element( engine_state: &EngineState, @@ -47,6 +48,27 @@ pub trait DebugContext: Clone + Copy + Debug { result: &Result, ) { } + + /// Called before the IR evaluator runs an instruction + #[allow(unused_variables)] + fn enter_instruction( + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + ) { + } + + /// Called after the IR evaluator runs an instruction + #[allow(unused_variables)] + fn leave_instruction( + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + error: Option<&ShellError>, + ) { + } } /// Marker struct signalizing that evaluation should use a Debugger @@ -85,6 +107,40 @@ impl DebugContext for WithDebug { .leave_element(engine_state, element, result); } } + + fn enter_instruction( + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + ) { + if let Ok(mut debugger) = engine_state.debugger.lock() { + debugger.deref_mut().enter_instruction( + engine_state, + ir_block, + instruction_index, + registers, + ) + } + } + + fn leave_instruction( + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + error: Option<&ShellError>, + ) { + if let Ok(mut debugger) = engine_state.debugger.lock() { + debugger.deref_mut().leave_instruction( + engine_state, + ir_block, + instruction_index, + registers, + error, + ) + } + } } /// Marker struct signalizing that evaluation should NOT use a Debugger @@ -118,11 +174,11 @@ pub trait Debugger: Send + Debug { #[allow(unused_variables)] fn leave_block(&mut self, engine_state: &EngineState, block: &Block) {} - /// Called when the evaluator enters a pipeline element + /// Called when the AST evaluator enters a pipeline element #[allow(unused_variables)] fn enter_element(&mut self, engine_state: &EngineState, pipeline_element: &PipelineElement) {} - /// Called when the evaluator leaves a pipeline element + /// Called when the AST evaluator leaves a pipeline element #[allow(unused_variables)] fn leave_element( &mut self, @@ -132,6 +188,29 @@ pub trait Debugger: Send + Debug { ) { } + /// Called before the IR evaluator runs an instruction + #[allow(unused_variables)] + fn enter_instruction( + &mut self, + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + ) { + } + + /// Called after the IR evaluator runs an instruction + #[allow(unused_variables)] + fn leave_instruction( + &mut self, + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + error: Option<&ShellError>, + ) { + } + /// Create a final report as a Value /// /// Intended to be called after deactivate() diff --git a/crates/nu-protocol/src/debugger/mod.rs b/crates/nu-protocol/src/debugger/mod.rs index 03208fb3c7..86768a0a34 100644 --- a/crates/nu-protocol/src/debugger/mod.rs +++ b/crates/nu-protocol/src/debugger/mod.rs @@ -1,3 +1,4 @@ +//! Module containing the trait to instrument the engine for debugging and profiling pub mod debugger_trait; pub mod profiler; diff --git a/crates/nu-protocol/src/debugger/profiler.rs b/crates/nu-protocol/src/debugger/profiler.rs index c3066c0a14..c61ee04d12 100644 --- a/crates/nu-protocol/src/debugger/profiler.rs +++ b/crates/nu-protocol/src/debugger/profiler.rs @@ -7,10 +7,11 @@ use crate::{ ast::{Block, Expr, PipelineElement}, debugger::Debugger, engine::EngineState, + ir::IrBlock, record, PipelineData, ShellError, Span, Value, }; -use std::io::BufRead; use std::time::Instant; +use std::{borrow::Borrow, io::BufRead}; #[derive(Debug, Clone, Copy)] struct ElementId(usize); @@ -24,6 +25,7 @@ struct ElementInfo { element_span: Span, element_output: Option, expr: Option, + instruction: Option<(usize, String)>, children: Vec, } @@ -36,57 +38,53 @@ impl ElementInfo { element_span, element_output: None, expr: None, + instruction: None, children: vec![], } } } +/// Options for [`Profiler`] +#[derive(Debug, Clone)] +pub struct ProfilerOptions { + pub max_depth: i64, + pub collect_spans: bool, + pub collect_source: bool, + pub collect_expanded_source: bool, + pub collect_values: bool, + pub collect_exprs: bool, + pub collect_instructions: bool, + pub collect_lines: bool, +} + /// Basic profiler, used in `debug profile` #[derive(Debug, Clone)] pub struct Profiler { depth: i64, - max_depth: i64, - collect_spans: bool, - collect_source: bool, - collect_expanded_source: bool, - collect_values: bool, - collect_exprs: bool, - collect_lines: bool, + opts: ProfilerOptions, elements: Vec, element_stack: Vec, } impl Profiler { #[allow(clippy::too_many_arguments)] - pub fn new( - max_depth: i64, - collect_spans: bool, - collect_source: bool, - collect_expanded_source: bool, - collect_values: bool, - collect_exprs: bool, - collect_lines: bool, - span: Span, - ) -> Self { + pub fn new(opts: ProfilerOptions, span: Span) -> Self { let first = ElementInfo { start: Instant::now(), duration_sec: 0.0, depth: 0, element_span: span, - element_output: collect_values.then(|| Value::nothing(span)), - expr: collect_exprs.then(|| "call".to_string()), + element_output: opts.collect_values.then(|| Value::nothing(span)), + expr: opts.collect_exprs.then(|| "call".to_string()), + instruction: opts + .collect_instructions + .then(|| (0, "".to_string())), children: vec![], }; Profiler { depth: 0, - max_depth, - collect_spans, - collect_source, - collect_expanded_source, - collect_values, - collect_exprs, - collect_lines, + opts, elements: vec![first], element_stack: vec![ElementId(0)], } @@ -130,7 +128,7 @@ impl Debugger for Profiler { } fn enter_element(&mut self, engine_state: &EngineState, element: &PipelineElement) { - if self.depth > self.max_depth { + if self.depth > self.opts.max_depth { return; } @@ -140,6 +138,7 @@ impl Debugger for Profiler { }; let expr_opt = self + .opts .collect_exprs .then(|| expr_to_string(engine_state, &element.expr.expr)); @@ -165,13 +164,13 @@ impl Debugger for Profiler { element: &PipelineElement, result: &Result, ) { - if self.depth > self.max_depth { + if self.depth > self.opts.max_depth { return; } let element_span = element.expr.span; - let out_opt = self.collect_values.then(|| match result { + let out_opt = self.opts.collect_values.then(|| match result { Ok(pipeline_data) => match pipeline_data { PipelineData::Value(val, ..) => val.clone(), PipelineData::ListStream(..) => Value::string("list stream", element_span), @@ -192,6 +191,91 @@ impl Debugger for Profiler { self.element_stack.pop(); } + fn enter_instruction( + &mut self, + engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + _registers: &[PipelineData], + ) { + if self.depth > self.opts.max_depth { + return; + } + + let Some(parent_id) = self.last_element_id() else { + eprintln!("Profiler Error: Missing parent element ID."); + return; + }; + + let instruction = &ir_block.instructions[instruction_index]; + let span = ir_block.spans[instruction_index]; + + let instruction_opt = self.opts.collect_instructions.then(|| { + ( + instruction_index, + instruction + .display(engine_state, &ir_block.data) + .to_string(), + ) + }); + + let new_id = ElementId(self.elements.len()); + + let mut new_element = ElementInfo::new(self.depth, span); + new_element.instruction = instruction_opt; + + self.elements.push(new_element); + + let Some(parent) = self.elements.get_mut(parent_id.0) else { + eprintln!("Profiler Error: Missing parent element."); + return; + }; + + parent.children.push(new_id); + self.element_stack.push(new_id); + } + + fn leave_instruction( + &mut self, + _engine_state: &EngineState, + ir_block: &IrBlock, + instruction_index: usize, + registers: &[PipelineData], + error: Option<&ShellError>, + ) { + if self.depth > self.opts.max_depth { + return; + } + + let instruction = &ir_block.instructions[instruction_index]; + let span = ir_block.spans[instruction_index]; + + let out_opt = self + .opts + .collect_values + .then(|| { + error + .map(Err) + .or_else(|| { + instruction + .output_register() + .map(|register| Ok(®isters[register.0 as usize])) + }) + .map(|result| format_result(&result, span)) + }) + .flatten(); + + let Some(last_element) = self.last_element_mut() else { + eprintln!("Profiler Error: Missing last element."); + return; + }; + + last_element.duration_sec = last_element.start.elapsed().as_secs_f64(); + last_element.element_output = out_opt; + + self.element_stack.pop(); + } + fn report(&self, engine_state: &EngineState, profiler_span: Span) -> Result { Ok(Value::list( collect_data( @@ -268,6 +352,21 @@ fn expr_to_string(engine_state: &EngineState, expr: &Expr) -> String { } } +fn format_result( + result: &Result, impl Borrow>, + element_span: Span, +) -> Value { + match result { + Ok(pipeline_data) => match pipeline_data.borrow() { + PipelineData::Value(val, ..) => val.clone(), + PipelineData::ListStream(..) => Value::string("list stream", element_span), + PipelineData::ByteStream(..) => Value::string("byte stream", element_span), + _ => Value::nothing(element_span), + }, + Err(e) => Value::error(e.borrow().clone(), element_span), + } +} + // Find a file name and a line number (indexed from 1) of a span fn find_file_of_span(engine_state: &EngineState, span: Span) -> Option<(&str, usize)> { for file in engine_state.files() { @@ -308,7 +407,7 @@ fn collect_data( "parent_id" => Value::int(parent_id.0 as i64, profiler_span), }; - if profiler.collect_lines { + if profiler.opts.collect_lines { if let Some((fname, line_num)) = find_file_of_span(engine_state, element.element_span) { row.push("file", Value::string(fname, profiler_span)); row.push("line", Value::int(line_num as i64, profiler_span)); @@ -318,7 +417,7 @@ fn collect_data( } } - if profiler.collect_spans { + if profiler.opts.collect_spans { let span_start = i64::try_from(element.element_span.start) .map_err(|_| profiler_error("error converting span start to i64", profiler_span))?; let span_end = i64::try_from(element.element_span.end) @@ -336,12 +435,12 @@ fn collect_data( ); } - if profiler.collect_source { + if profiler.opts.collect_source { let val = String::from_utf8_lossy(engine_state.get_span_contents(element.element_span)); let val = val.trim(); let nlines = val.lines().count(); - let fragment = if profiler.collect_expanded_source { + let fragment = if profiler.opts.collect_expanded_source { val.to_string() } else { let mut first_line = val.lines().next().unwrap_or("").to_string(); @@ -360,6 +459,17 @@ fn collect_data( row.push("expr", Value::string(expr_string.clone(), profiler_span)); } + if let Some((instruction_index, instruction)) = &element.instruction { + row.push( + "pc", + (*instruction_index) + .try_into() + .map(|index| Value::int(index, profiler_span)) + .unwrap_or(Value::nothing(profiler_span)), + ); + row.push("instruction", Value::string(instruction, profiler_span)); + } + if let Some(val) = &element.element_output { row.push("output", val.clone()); } diff --git a/crates/nu-protocol/src/engine/argument.rs b/crates/nu-protocol/src/engine/argument.rs new file mode 100644 index 0000000000..043654b761 --- /dev/null +++ b/crates/nu-protocol/src/engine/argument.rs @@ -0,0 +1,124 @@ +use std::sync::Arc; + +use crate::{ast::Expression, ir::DataSlice, Span, Value}; + +/// Represents a fully evaluated argument to a call. +#[derive(Debug, Clone)] +pub enum Argument { + /// A positional argument + Positional { + span: Span, + val: Value, + ast: Option>, + }, + /// A spread argument, e.g. `...$args` + Spread { + span: Span, + vals: Value, + ast: Option>, + }, + /// A named argument with no value, e.g. `--flag` + Flag { + data: Arc<[u8]>, + name: DataSlice, + short: DataSlice, + span: Span, + }, + /// A named argument with a value, e.g. `--flag value` or `--flag=` + Named { + data: Arc<[u8]>, + name: DataSlice, + short: DataSlice, + span: Span, + val: Value, + ast: Option>, + }, + /// Information generated by the parser for use by certain keyword commands + ParserInfo { + data: Arc<[u8]>, + name: DataSlice, + // TODO: rather than `Expression`, this would probably be best served by a specific enum + // type for this purpose. + info: Box, + }, +} + +impl Argument { + /// The span encompassing the argument's usage within the call, distinct from the span of the + /// actual value of the argument. + pub fn span(&self) -> Option { + match self { + Argument::Positional { span, .. } => Some(*span), + Argument::Spread { span, .. } => Some(*span), + Argument::Flag { span, .. } => Some(*span), + Argument::Named { span, .. } => Some(*span), + // Because `ParserInfo` is generated, its span shouldn't be used + Argument::ParserInfo { .. } => None, + } + } + + /// The original AST [`Expression`] for the argument's value. This is not usually available; + /// declarations have to opt-in if they require this. + pub fn ast_expression(&self) -> Option<&Arc> { + match self { + Argument::Positional { ast, .. } => ast.as_ref(), + Argument::Spread { ast, .. } => ast.as_ref(), + Argument::Flag { .. } => None, + Argument::Named { ast, .. } => ast.as_ref(), + Argument::ParserInfo { .. } => None, + } + } +} + +/// Stores the argument context for calls in IR evaluation. +#[derive(Debug, Clone)] +pub struct ArgumentStack { + arguments: Vec, +} + +impl ArgumentStack { + /// Create a new, empty argument stack. + pub const fn new() -> Self { + ArgumentStack { arguments: vec![] } + } + + /// Returns the index of the end of the argument stack. Call and save this before adding + /// arguments. + pub fn get_base(&self) -> usize { + self.arguments.len() + } + + /// Calculates the number of arguments past the given [previously retrieved](.get_base) base + /// pointer. + pub fn get_len(&self, base: usize) -> usize { + self.arguments.len().checked_sub(base).unwrap_or_else(|| { + panic!( + "base ({}) is beyond the end of the arguments stack ({})", + base, + self.arguments.len() + ); + }) + } + + /// Push an argument onto the end of the argument stack. + pub fn push(&mut self, argument: Argument) { + self.arguments.push(argument); + } + + /// Clear all of the arguments after the given base index, to prepare for the next frame. + pub fn leave_frame(&mut self, base: usize) { + self.arguments.truncate(base); + } + + /// Get arguments for the frame based on the given [`base`](`.get_base()`) and + /// [`len`](`.get_len()`) parameters. + pub fn get_args(&self, base: usize, len: usize) -> &[Argument] { + &self.arguments[base..(base + len)] + } + + /// Move arguments for the frame based on the given [`base`](`.get_base()`) and + /// [`len`](`.get_len()`) parameters. + pub fn drain_args(&mut self, base: usize, len: usize) -> impl Iterator + '_ { + self.arguments.drain(base..(base + len)) + } +} diff --git a/crates/nu-protocol/src/engine/call.rs b/crates/nu-protocol/src/engine/call.rs new file mode 100644 index 0000000000..741e2bd87a --- /dev/null +++ b/crates/nu-protocol/src/engine/call.rs @@ -0,0 +1,223 @@ +use crate::{ + ast::{self, Expression}, + ir, DeclId, FromValue, ShellError, Span, Value, +}; + +use super::{EngineState, Stack, StateWorkingSet}; + +/// This is a HACK to help [`Command`](super::Command) support both the old AST evaluator and the +/// new IR evaluator at the same time. It should be removed once we are satisfied with the new +/// evaluator. +#[derive(Debug, Clone)] +pub struct Call<'a> { + pub head: Span, + pub decl_id: DeclId, + pub inner: CallImpl<'a>, +} + +#[derive(Debug, Clone)] +pub enum CallImpl<'a> { + AstRef(&'a ast::Call), + AstBox(Box), + IrRef(&'a ir::Call), + IrBox(Box), +} + +impl Call<'_> { + /// Returns a new AST call with the given span. This is often used by commands that need an + /// empty call to pass to a command. It's not easily possible to add anything to this. + pub fn new(span: Span) -> Self { + // this is using the boxed variant, which isn't so efficient... but this is only temporary + // anyway. + Call { + head: span, + decl_id: 0, + inner: CallImpl::AstBox(Box::new(ast::Call::new(span))), + } + } + + /// Convert the `Call` from any lifetime into `'static`, by cloning the data within onto the + /// heap. + pub fn to_owned(&self) -> Call<'static> { + Call { + head: self.head, + decl_id: self.decl_id, + inner: self.inner.to_owned(), + } + } + + /// Assert that the call is `ast::Call`, and fail with an error if it isn't. + /// + /// Provided as a stop-gap for commands that can't work with `ir::Call`, or just haven't been + /// implemented yet. Eventually these issues should be resolved and then this can be removed. + pub fn assert_ast_call(&self) -> Result<&ast::Call, ShellError> { + match &self.inner { + CallImpl::AstRef(call) => Ok(call), + CallImpl::AstBox(call) => Ok(call), + _ => Err(ShellError::NushellFailedSpanned { + msg: "Can't be used in IR context".into(), + label: "this command is not yet supported by IR evaluation".into(), + span: self.head, + }), + } + } + + /// FIXME: implementation asserts `ast::Call` and proxies to that + pub fn has_flag_const( + &self, + working_set: &StateWorkingSet, + flag_name: &str, + ) -> Result { + self.assert_ast_call()? + .has_flag_const(working_set, flag_name) + } + + /// FIXME: implementation asserts `ast::Call` and proxies to that + pub fn get_flag_const( + &self, + working_set: &StateWorkingSet, + name: &str, + ) -> Result, ShellError> { + self.assert_ast_call()?.get_flag_const(working_set, name) + } + + /// FIXME: implementation asserts `ast::Call` and proxies to that + pub fn req_const( + &self, + working_set: &StateWorkingSet, + pos: usize, + ) -> Result { + self.assert_ast_call()?.req_const(working_set, pos) + } + + /// FIXME: implementation asserts `ast::Call` and proxies to that + pub fn rest_const( + &self, + working_set: &StateWorkingSet, + starting_pos: usize, + ) -> Result, ShellError> { + self.assert_ast_call()? + .rest_const(working_set, starting_pos) + } + + /// Returns a span covering the call's arguments. + pub fn arguments_span(&self) -> Span { + match &self.inner { + CallImpl::AstRef(call) => call.arguments_span(), + CallImpl::AstBox(call) => call.arguments_span(), + CallImpl::IrRef(call) => call.arguments_span(), + CallImpl::IrBox(call) => call.arguments_span(), + } + } + + /// Returns a span covering the whole call. + pub fn span(&self) -> Span { + match &self.inner { + CallImpl::AstRef(call) => call.span(), + CallImpl::AstBox(call) => call.span(), + CallImpl::IrRef(call) => call.span(), + CallImpl::IrBox(call) => call.span(), + } + } + + /// Get a parser info argument by name. + pub fn get_parser_info<'a>(&'a self, stack: &'a Stack, name: &str) -> Option<&'a Expression> { + match &self.inner { + CallImpl::AstRef(call) => call.get_parser_info(name), + CallImpl::AstBox(call) => call.get_parser_info(name), + CallImpl::IrRef(call) => call.get_parser_info(stack, name), + CallImpl::IrBox(call) => call.get_parser_info(stack, name), + } + } + + /// Evaluator-agnostic implementation of `rest_iter_flattened()`. Evaluates or gets all of the + /// positional and spread arguments, flattens spreads, and then returns one list of values. + pub fn rest_iter_flattened( + &self, + engine_state: &EngineState, + stack: &mut Stack, + eval_expression: fn( + &EngineState, + &mut Stack, + &ast::Expression, + ) -> Result, + starting_pos: usize, + ) -> Result, ShellError> { + fn by_ast( + call: &ast::Call, + engine_state: &EngineState, + stack: &mut Stack, + eval_expression: fn( + &EngineState, + &mut Stack, + &ast::Expression, + ) -> Result, + starting_pos: usize, + ) -> Result, ShellError> { + call.rest_iter_flattened(starting_pos, |expr| { + eval_expression(engine_state, stack, expr) + }) + } + + fn by_ir( + call: &ir::Call, + stack: &Stack, + starting_pos: usize, + ) -> Result, ShellError> { + call.rest_iter_flattened(stack, starting_pos) + } + + match &self.inner { + CallImpl::AstRef(call) => { + by_ast(call, engine_state, stack, eval_expression, starting_pos) + } + CallImpl::AstBox(call) => { + by_ast(call, engine_state, stack, eval_expression, starting_pos) + } + CallImpl::IrRef(call) => by_ir(call, stack, starting_pos), + CallImpl::IrBox(call) => by_ir(call, stack, starting_pos), + } + } + + /// Get the original AST expression for a positional argument. Does not usually work for IR + /// unless the decl specified `requires_ast_for_arguments()` + pub fn positional_nth<'a>(&'a self, stack: &'a Stack, index: usize) -> Option<&'a Expression> { + match &self.inner { + CallImpl::AstRef(call) => call.positional_nth(index), + CallImpl::AstBox(call) => call.positional_nth(index), + CallImpl::IrRef(call) => call.positional_ast(stack, index).map(|arc| arc.as_ref()), + CallImpl::IrBox(call) => call.positional_ast(stack, index).map(|arc| arc.as_ref()), + } + } +} + +impl CallImpl<'_> { + pub fn to_owned(&self) -> CallImpl<'static> { + match self { + CallImpl::AstRef(call) => CallImpl::AstBox(Box::new((*call).clone())), + CallImpl::AstBox(call) => CallImpl::AstBox(call.clone()), + CallImpl::IrRef(call) => CallImpl::IrBox(Box::new((*call).clone())), + CallImpl::IrBox(call) => CallImpl::IrBox(call.clone()), + } + } +} + +impl<'a> From<&'a ast::Call> for Call<'a> { + fn from(call: &'a ast::Call) -> Self { + Call { + head: call.head, + decl_id: call.decl_id, + inner: CallImpl::AstRef(call), + } + } +} + +impl<'a> From<&'a ir::Call> for Call<'a> { + fn from(call: &'a ir::Call) -> Self { + Call { + head: call.head, + decl_id: call.decl_id, + inner: CallImpl::IrRef(call), + } + } +} diff --git a/crates/nu-protocol/src/engine/command.rs b/crates/nu-protocol/src/engine/command.rs index 043d2a66c7..48cdc4440d 100644 --- a/crates/nu-protocol/src/engine/command.rs +++ b/crates/nu-protocol/src/engine/command.rs @@ -1,5 +1,5 @@ use super::{EngineState, Stack, StateWorkingSet}; -use crate::{ast::Call, Alias, BlockId, Example, OutDest, PipelineData, ShellError, Signature}; +use crate::{engine::Call, Alias, BlockId, Example, OutDest, PipelineData, ShellError, Signature}; use std::fmt::Display; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -124,6 +124,12 @@ pub trait Command: Send + Sync + CommandClone { fn pipe_redirection(&self) -> (Option, Option) { (None, None) } + + /// Return true if the AST nodes for the arguments are required for IR evaluation. This is + /// currently inefficient so is not generally done. + fn requires_ast_for_arguments(&self) -> bool { + false + } } pub trait CommandClone { diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 3cc6f66572..e46cdd6fac 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -9,10 +9,11 @@ use crate::{ }, eval_const::create_nu_constant, BlockId, Category, Config, DeclId, FileId, GetSpan, HistoryConfig, Module, ModuleId, OverlayId, - ShellError, Signature, Span, SpanId, Type, Value, VarId, VirtualPathId, + ShellError, Signals, Signature, Span, SpanId, Type, Value, VarId, VirtualPathId, }; use fancy_regex::Regex; use lru::LruCache; +use nu_path::AbsolutePathBuf; use std::{ collections::HashMap, num::NonZeroUsize, @@ -87,6 +88,7 @@ pub struct EngineState { pub scope: ScopeFrame, pub ctrlc: Option>, pub ctrlc_handlers: Option, + signals: Signals, pub env_vars: Arc, pub previous_env_vars: Arc>, pub config: Arc, @@ -148,6 +150,7 @@ impl EngineState { ), ctrlc: None, ctrlc_handlers: None, + signals: Signals::empty(), env_vars: Arc::new( [(DEFAULT_OVERLAY_NAME.to_string(), HashMap::new())] .into_iter() @@ -180,6 +183,18 @@ impl EngineState { } } + pub fn signals(&self) -> &Signals { + &self.signals + } + + pub fn reset_signals(&mut self) { + self.signals.reset() + } + + pub fn set_signals(&mut self, signals: Signals) { + self.signals = signals; + } + /// Merges a `StateDelta` onto the current state. These deltas come from a system, like the parser, that /// creates a new set of definitions and visible symbols in the current scope. We make this transactional /// as there are times when we want to run the parser and immediately throw away the results (namely: @@ -296,28 +311,11 @@ impl EngineState { stack: &mut Stack, cwd: impl AsRef, ) -> Result<(), ShellError> { - let mut config_updated = false; - for mut scope in stack.env_vars.drain(..) { - for (overlay_name, mut env) in scope.drain() { + for (overlay_name, mut env) in Arc::make_mut(&mut scope).drain() { if let Some(env_vars) = Arc::make_mut(&mut self.env_vars).get_mut(&overlay_name) { // Updating existing overlay - for (k, v) in env.drain() { - if k == "config" { - // Don't insert the record as the "config" env var as-is. - // Instead, mutate a clone of it with into_config(), and put THAT in env_vars. - let mut new_record = v.clone(); - let (config, error) = new_record.parse_as_config(&self.config); - self.config = Arc::new(config); - config_updated = true; - env_vars.insert(k, new_record); - if let Some(e) = error { - return Err(e); - } - } else { - env_vars.insert(k, v); - } - } + env_vars.extend(env.drain()); } else { // Pushing a new overlay Arc::make_mut(&mut self.env_vars).insert(overlay_name, env); @@ -328,7 +326,10 @@ impl EngineState { // TODO: better error std::env::set_current_dir(cwd)?; - if config_updated { + if let Some(config) = stack.config.take() { + // If config was updated in the stack, replace it. + self.config = config; + // Make plugin GC config changes take effect immediately. #[cfg(feature = "plugin")] self.update_plugin_gc_configs(&self.config.plugin_gc); @@ -733,24 +734,30 @@ impl EngineState { &[0u8; 0] } - pub fn get_config(&self) -> &Config { + /// Get the global config from the engine state. + /// + /// Use [`Stack::get_config()`] instead whenever the `Stack` is available, as it takes into + /// account local changes to `$env.config`. + pub fn get_config(&self) -> &Arc { &self.config } - pub fn set_config(&mut self, conf: Config) { + pub fn set_config(&mut self, conf: impl Into>) { + let conf = conf.into(); + #[cfg(feature = "plugin")] if conf.plugin_gc != self.config.plugin_gc { // Make plugin GC config changes take effect immediately. self.update_plugin_gc_configs(&conf.plugin_gc); } - self.config = Arc::new(conf); + self.config = conf; } /// Fetch the configuration for a plugin /// - /// The `plugin` must match the registered name of a plugin. For `register nu_plugin_example` - /// the plugin name to use will be `"example"` + /// The `plugin` must match the registered name of a plugin. For `plugin add + /// nu_plugin_example` the plugin name to use will be `"example"` pub fn get_plugin_config(&self, plugin: &str) -> Option<&Value> { self.config.plugins.get(plugin) } @@ -918,58 +925,44 @@ impl EngineState { /// /// If `stack` is supplied, also considers modifications to the working /// directory on the stack that have yet to be merged into the engine state. - pub fn cwd(&self, stack: Option<&Stack>) -> Result { + pub fn cwd(&self, stack: Option<&Stack>) -> Result { // Helper function to create a simple generic error. - fn error(msg: &str, cwd: impl AsRef) -> Result { - Err(ShellError::GenericError { + fn error(msg: &str, cwd: impl AsRef) -> ShellError { + ShellError::GenericError { error: msg.into(), msg: format!("$env.PWD = {}", cwd.as_ref().display()), span: None, help: Some("Use `cd` to reset $env.PWD into a good state".into()), inner: vec![], - }) - } - - // Helper function to check if a path is a root path. - fn is_root(path: &Path) -> bool { - path.parent().is_none() - } - - // Helper function to check if a path has trailing slashes. - fn has_trailing_slash(path: &Path) -> bool { - nu_path::components(path).last() - == Some(std::path::Component::Normal(std::ffi::OsStr::new(""))) + } } // Retrieve $env.PWD from the stack or the engine state. let pwd = if let Some(stack) = stack { stack.get_env_var(self, "PWD") } else { - self.get_env_var("PWD").map(ToOwned::to_owned) + self.get_env_var("PWD").cloned() }; - if let Some(pwd) = pwd { - if let Value::String { val, .. } = pwd { - let path = PathBuf::from(val); + let pwd = pwd.ok_or_else(|| error("$env.PWD not found", ""))?; - // Technically, a root path counts as "having trailing slashes", but - // for the purpose of PWD, a root path is acceptable. - if !is_root(&path) && has_trailing_slash(&path) { - error("$env.PWD contains trailing slashes", path) - } else if !path.is_absolute() { - error("$env.PWD is not an absolute path", path) - } else if !path.exists() { - error("$env.PWD points to a non-existent directory", path) - } else if !path.is_dir() { - error("$env.PWD points to a non-directory", path) - } else { - Ok(path) - } + if let Value::String { val, .. } = pwd { + let path = AbsolutePathBuf::try_from(val) + .map_err(|path| error("$env.PWD is not an absolute path", path))?; + + // Technically, a root path counts as "having trailing slashes", but + // for the purpose of PWD, a root path is acceptable. + if path.parent().is_some() && nu_path::has_trailing_slash(path.as_ref()) { + Err(error("$env.PWD contains trailing slashes", &path)) + } else if !path.exists() { + Err(error("$env.PWD points to a non-existent directory", &path)) + } else if !path.is_dir() { + Err(error("$env.PWD points to a non-directory", &path)) } else { - error("$env.PWD is not a string", format!("{pwd:?}")) + Ok(path) } } else { - error("$env.PWD not found", "") + Err(error("$env.PWD is not a string", format!("{pwd:?}"))) } } @@ -1146,7 +1139,7 @@ mod engine_state_tests { let mut plugins = HashMap::new(); plugins.insert("example".into(), Value::string("value", Span::test_data())); - let mut config = engine_state.get_config().clone(); + let mut config = Config::clone(engine_state.get_config()); config.plugins = plugins; engine_state.set_config(config); diff --git a/crates/nu-protocol/src/engine/error_handler.rs b/crates/nu-protocol/src/engine/error_handler.rs new file mode 100644 index 0000000000..076678be20 --- /dev/null +++ b/crates/nu-protocol/src/engine/error_handler.rs @@ -0,0 +1,55 @@ +use crate::RegId; + +/// Describes an error handler stored during IR evaluation. +#[derive(Debug, Clone, Copy)] +pub struct ErrorHandler { + /// Instruction index within the block that will handle the error + pub handler_index: usize, + /// Register to put the error information into, when an error occurs + pub error_register: Option, +} + +/// Keeps track of error handlers pushed during evaluation of an IR block. +#[derive(Debug, Clone)] +pub struct ErrorHandlerStack { + handlers: Vec, +} + +impl ErrorHandlerStack { + pub const fn new() -> ErrorHandlerStack { + ErrorHandlerStack { handlers: vec![] } + } + + /// Get the current base of the stack, which establishes a frame. + pub fn get_base(&self) -> usize { + self.handlers.len() + } + + /// Push a new error handler onto the stack. + pub fn push(&mut self, handler: ErrorHandler) { + self.handlers.push(handler); + } + + /// Try to pop an error handler from the stack. Won't go below `base`, to avoid retrieving a + /// handler belonging to a parent frame. + pub fn pop(&mut self, base: usize) -> Option { + if self.handlers.len() > base { + self.handlers.pop() + } else { + None + } + } + + /// Reset the stack to the state it was in at the beginning of the frame, in preparation to + /// return control to the parent frame. + pub fn leave_frame(&mut self, base: usize) { + if self.handlers.len() >= base { + self.handlers.truncate(base); + } else { + panic!( + "ErrorHandlerStack bug: tried to leave frame at {base}, but current base is {}", + self.get_base() + ) + } + } +} diff --git a/crates/nu-protocol/src/engine/mod.rs b/crates/nu-protocol/src/engine/mod.rs index 61915950a1..24e4b15094 100644 --- a/crates/nu-protocol/src/engine/mod.rs +++ b/crates/nu-protocol/src/engine/mod.rs @@ -1,8 +1,12 @@ +//! Representation of the engine state and many of the details that implement the scoping +mod argument; mod cached_file; +mod call; mod call_info; mod capture_block; mod command; mod engine_state; +mod error_handler; mod overlay; mod pattern_match; mod sequence; @@ -15,10 +19,13 @@ mod variable; pub use cached_file::CachedFile; +pub use argument::*; +pub use call::*; pub use call_info::*; pub use capture_block::*; pub use command::*; pub use engine_state::*; +pub use error_handler::*; pub use overlay::*; pub use pattern_match::*; pub use sequence::*; diff --git a/crates/nu-protocol/src/engine/stack.rs b/crates/nu-protocol/src/engine/stack.rs index 19726db9c0..fbfb586762 100644 --- a/crates/nu-protocol/src/engine/stack.rs +++ b/crates/nu-protocol/src/engine/stack.rs @@ -1,9 +1,9 @@ use crate::{ engine::{ - EngineState, Redirection, StackCallArgGuard, StackCaptureGuard, StackIoGuard, StackOutDest, - DEFAULT_OVERLAY_NAME, + ArgumentStack, EngineState, ErrorHandlerStack, Redirection, StackCallArgGuard, + StackCaptureGuard, StackIoGuard, StackOutDest, DEFAULT_OVERLAY_NAME, }, - OutDest, ShellError, Span, Value, VarId, ENV_VARIABLE_ID, NU_VARIABLE_ID, + Config, OutDest, ShellError, Span, Value, VarId, ENV_VARIABLE_ID, NU_VARIABLE_ID, }; use std::{ collections::{HashMap, HashSet}, @@ -36,15 +36,23 @@ pub struct Stack { /// Variables pub vars: Vec<(VarId, Value)>, /// Environment variables arranged as a stack to be able to recover values from parent scopes - pub env_vars: Vec, + pub env_vars: Vec>, /// Tells which environment variables from engine state are hidden, per overlay. - pub env_hidden: HashMap>, + pub env_hidden: Arc>>, /// List of active overlays pub active_overlays: Vec, + /// Argument stack for IR evaluation + pub arguments: ArgumentStack, + /// Error handler stack for IR evaluation + pub error_handlers: ErrorHandlerStack, + /// Set true to always use IR mode + pub use_ir: bool, pub recursion_count: u64, pub parent_stack: Option>, /// Variables that have been deleted (this is used to hide values from parent stack lookups) pub parent_deletions: Vec, + /// Locally updated config. Use [`.get_config()`] to access correctly. + pub config: Option>, pub(crate) out_dest: StackOutDest, } @@ -66,11 +74,15 @@ impl Stack { Self { vars: Vec::new(), env_vars: Vec::new(), - env_hidden: HashMap::new(), + env_hidden: Arc::new(HashMap::new()), active_overlays: vec![DEFAULT_OVERLAY_NAME.to_string()], + arguments: ArgumentStack::new(), + error_handlers: ErrorHandlerStack::new(), + use_ir: false, recursion_count: 0, parent_stack: None, parent_deletions: vec![], + config: None, out_dest: StackOutDest::new(), } } @@ -85,9 +97,13 @@ impl Stack { env_vars: parent.env_vars.clone(), env_hidden: parent.env_hidden.clone(), active_overlays: parent.active_overlays.clone(), + arguments: ArgumentStack::new(), + error_handlers: ErrorHandlerStack::new(), + use_ir: parent.use_ir, recursion_count: parent.recursion_count, vars: vec![], parent_deletions: vec![], + config: parent.config.clone(), out_dest: parent.out_dest.clone(), parent_stack: Some(parent), } @@ -114,13 +130,14 @@ impl Stack { unique_stack.env_vars = child.env_vars; unique_stack.env_hidden = child.env_hidden; unique_stack.active_overlays = child.active_overlays; + unique_stack.config = child.config; unique_stack } pub fn with_env( &mut self, - env_vars: &[EnvVars], - env_hidden: &HashMap>, + env_vars: &[Arc], + env_hidden: &Arc>>, ) { // Do not clone the environment if it hasn't changed if self.env_vars.iter().any(|scope| !scope.is_empty()) { @@ -180,6 +197,36 @@ impl Stack { } } + /// Get the local config if set, otherwise the config from the engine state. + /// + /// This is the canonical way to get [`Config`] when [`Stack`] is available. + pub fn get_config(&self, engine_state: &EngineState) -> Arc { + self.config + .clone() + .unwrap_or_else(|| engine_state.config.clone()) + } + + /// Update the local config with the config stored in the `config` environment variable. Run + /// this after assigning to `$env.config`. + /// + /// The config will be updated with successfully parsed values even if an error occurs. + pub fn update_config(&mut self, engine_state: &EngineState) -> Result<(), ShellError> { + if let Some(mut config) = self.get_env_var(engine_state, "config") { + let existing_config = self.get_config(engine_state); + let (new_config, error) = config.parse_as_config(&existing_config); + self.config = Some(new_config.into()); + // The config value is modified by the update, so we should add it again + self.add_env_var("config".into(), config); + match error { + None => Ok(()), + Some(err) => Err(err), + } + } else { + self.config = None; + Ok(()) + } + } + pub fn add_var(&mut self, var_id: VarId, value: Value) { //self.vars.insert(var_id, value); for (id, val) in &mut self.vars { @@ -207,23 +254,24 @@ impl Stack { pub fn add_env_var(&mut self, var: String, value: Value) { if let Some(last_overlay) = self.active_overlays.last() { - if let Some(env_hidden) = self.env_hidden.get_mut(last_overlay) { + if let Some(env_hidden) = Arc::make_mut(&mut self.env_hidden).get_mut(last_overlay) { // if the env var was hidden, let's activate it again env_hidden.remove(&var); } if let Some(scope) = self.env_vars.last_mut() { + let scope = Arc::make_mut(scope); if let Some(env_vars) = scope.get_mut(last_overlay) { env_vars.insert(var, value); } else { scope.insert(last_overlay.into(), [(var, value)].into_iter().collect()); } } else { - self.env_vars.push( + self.env_vars.push(Arc::new( [(last_overlay.into(), [(var, value)].into_iter().collect())] .into_iter() .collect(), - ); + )); } } else { // TODO: Remove panic @@ -245,24 +293,27 @@ impl Stack { } pub fn captures_to_stack_preserve_out_dest(&self, captures: Vec<(VarId, Value)>) -> Stack { - // FIXME: this is probably slow let mut env_vars = self.env_vars.clone(); - env_vars.push(HashMap::new()); + env_vars.push(Arc::new(HashMap::new())); Stack { vars: captures, env_vars, env_hidden: self.env_hidden.clone(), active_overlays: self.active_overlays.clone(), + arguments: ArgumentStack::new(), + error_handlers: ErrorHandlerStack::new(), + use_ir: self.use_ir, recursion_count: self.recursion_count, parent_stack: None, parent_deletions: vec![], + config: self.config.clone(), out_dest: self.out_dest.clone(), } } pub fn gather_captures(&self, engine_state: &EngineState, captures: &[VarId]) -> Stack { - let mut vars = vec![]; + let mut vars = Vec::with_capacity(captures.len()); let fake_span = Span::new(0, 0); @@ -277,16 +328,20 @@ impl Stack { } let mut env_vars = self.env_vars.clone(); - env_vars.push(HashMap::new()); + env_vars.push(Arc::new(HashMap::new())); Stack { vars, env_vars, env_hidden: self.env_hidden.clone(), active_overlays: self.active_overlays.clone(), + arguments: ArgumentStack::new(), + error_handlers: ErrorHandlerStack::new(), + use_ir: self.use_ir, recursion_count: self.recursion_count, parent_stack: None, parent_deletions: vec![], + config: self.config.clone(), out_dest: self.out_dest.clone(), } } @@ -444,6 +499,7 @@ impl Stack { pub fn remove_env_var(&mut self, engine_state: &EngineState, name: &str) -> bool { for scope in self.env_vars.iter_mut().rev() { + let scope = Arc::make_mut(scope); for active_overlay in self.active_overlays.iter().rev() { if let Some(env_vars) = scope.get_mut(active_overlay) { if env_vars.remove(name).is_some() { @@ -456,10 +512,11 @@ impl Stack { for active_overlay in self.active_overlays.iter().rev() { if let Some(env_vars) = engine_state.env_vars.get(active_overlay) { if env_vars.get(name).is_some() { - if let Some(env_hidden) = self.env_hidden.get_mut(active_overlay) { - env_hidden.insert(name.into()); + let env_hidden = Arc::make_mut(&mut self.env_hidden); + if let Some(env_hidden_in_overlay) = env_hidden.get_mut(active_overlay) { + env_hidden_in_overlay.insert(name.into()); } else { - self.env_hidden + env_hidden .insert(active_overlay.into(), [name.into()].into_iter().collect()); } diff --git a/crates/nu-protocol/src/engine/state_working_set.rs b/crates/nu-protocol/src/engine/state_working_set.rs index af950b8321..822bf35e3a 100644 --- a/crates/nu-protocol/src/engine/state_working_set.rs +++ b/crates/nu-protocol/src/engine/state_working_set.rs @@ -4,8 +4,8 @@ use crate::{ usage::build_usage, CachedFile, Command, CommandType, EngineState, OverlayFrame, StateDelta, Variable, VirtualPath, Visibility, }, - BlockId, Category, Config, DeclId, FileId, GetSpan, Module, ModuleId, ParseError, ParseWarning, - Span, SpanId, Type, Value, VarId, VirtualPathId, + BlockId, Category, CompileError, Config, DeclId, FileId, GetSpan, Module, ModuleId, ParseError, + ParseWarning, Span, SpanId, Type, Value, VarId, VirtualPathId, }; use core::panic; use std::{ @@ -25,12 +25,12 @@ use crate::{PluginIdentity, PluginRegistryItem, RegisteredPlugin}; pub struct StateWorkingSet<'a> { pub permanent_state: &'a EngineState, pub delta: StateDelta, - pub external_commands: Vec>, pub files: FileStack, /// Whether or not predeclarations are searched when looking up a command (used with aliases) pub search_predecls: bool, pub parse_errors: Vec, pub parse_warnings: Vec, + pub compile_errors: Vec, } impl<'a> StateWorkingSet<'a> { @@ -45,11 +45,11 @@ impl<'a> StateWorkingSet<'a> { Self { delta: StateDelta::new(permanent_state), permanent_state, - external_commands: vec![], files, search_predecls: true, parse_errors: vec![], parse_warnings: vec![], + compile_errors: vec![], } } @@ -260,6 +260,12 @@ impl<'a> StateWorkingSet<'a> { } pub fn add_block(&mut self, block: Arc) -> BlockId { + log::trace!( + "block id={} added, has IR = {:?}", + self.num_blocks(), + block.ir_block.is_some() + ); + self.delta.blocks.push(block); self.num_blocks() - 1 @@ -619,9 +625,9 @@ impl<'a> StateWorkingSet<'a> { /// Returns a reference to the config stored at permanent state /// - /// At runtime, you most likely want to call nu_engine::env::get_config because this method - /// does not capture environment updates during runtime. - pub fn get_config(&self) -> &Config { + /// At runtime, you most likely want to call [`Stack::get_config()`][super::Stack::get_config()] + /// because this method does not capture environment updates during runtime. + pub fn get_config(&self) -> &Arc { &self.permanent_state.config } diff --git a/crates/nu-protocol/src/errors/cli_error.rs b/crates/nu-protocol/src/errors/cli_error.rs index 003564f933..9ce6829b7b 100644 --- a/crates/nu-protocol/src/errors/cli_error.rs +++ b/crates/nu-protocol/src/errors/cli_error.rs @@ -1,3 +1,6 @@ +//! This module manages the step of turning error types into printed error messages +//! +//! Relies on the `miette` crate for pretty layout use crate::{ engine::{EngineState, StateWorkingSet}, ErrorStyle, @@ -44,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(); @@ -107,4 +130,8 @@ impl<'src> miette::Diagnostic for CliError<'src> { fn related<'a>(&'a self) -> Option + 'a>> { self.0.related() } + + fn diagnostic_source(&self) -> Option<&dyn miette::Diagnostic> { + self.0.diagnostic_source() + } } diff --git a/crates/nu-protocol/src/errors/compile_error.rs b/crates/nu-protocol/src/errors/compile_error.rs new file mode 100644 index 0000000000..cc805a73ed --- /dev/null +++ b/crates/nu-protocol/src/errors/compile_error.rs @@ -0,0 +1,238 @@ +use crate::{RegId, Span}; +use miette::Diagnostic; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// An internal compiler error, generally means a Nushell bug rather than an issue with user error +/// since parsing and typechecking has already passed. +#[derive(Debug, Clone, Error, Diagnostic, PartialEq, Serialize, Deserialize)] +pub enum CompileError { + #[error("Register overflow.")] + #[diagnostic(code(nu::compile::register_overflow))] + RegisterOverflow { + #[label("the code being compiled is probably too large")] + block_span: Option, + }, + + #[error("Register {reg_id} was uninitialized when used, possibly reused.")] + #[diagnostic( + code(nu::compile::register_uninitialized), + help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new\nfrom: {caller}"), + )] + RegisterUninitialized { reg_id: RegId, caller: String }, + + #[error("Register {reg_id} was uninitialized when used, possibly reused.")] + #[diagnostic( + code(nu::compile::register_uninitialized), + help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new\nfrom: {caller}"), + )] + RegisterUninitializedWhilePushingInstruction { + reg_id: RegId, + caller: String, + instruction: String, + #[label("while adding this instruction: {instruction}")] + span: Span, + }, + + #[error("Block contains too much string data: maximum 4 GiB exceeded.")] + #[diagnostic( + code(nu::compile::data_overflow), + help("try loading the string data from a file instead") + )] + DataOverflow { + #[label("while compiling this block")] + block_span: Option, + }, + + #[error("Block contains too many files.")] + #[diagnostic( + code(nu::compile::register_overflow), + help("try using fewer file redirections") + )] + FileOverflow { + #[label("while compiling this block")] + block_span: Option, + }, + + #[error("Invalid redirect mode: File should not be specified by commands.")] + #[diagnostic( + code(nu::compile::invalid_redirect_mode), + help("this is a command bug. Please report it at https://github.com/nushell/nushell/issues/new") + )] + InvalidRedirectMode { + #[label("while compiling this expression")] + span: Span, + }, + + #[error("Encountered garbage, likely due to parse error.")] + #[diagnostic(code(nu::compile::garbage))] + Garbage { + #[label("garbage found here")] + span: Span, + }, + + #[error("Unsupported operator expression.")] + #[diagnostic(code(nu::compile::unsupported_operator_expression))] + UnsupportedOperatorExpression { + #[label("this expression is in operator position but is not an operator")] + span: Span, + }, + + #[error("Attempted access of $env by integer path.")] + #[diagnostic(code(nu::compile::access_env_by_int))] + AccessEnvByInt { + #[label("$env keys should be strings")] + span: Span, + }, + + #[error("Encountered invalid `{keyword}` keyword call.")] + #[diagnostic(code(nu::compile::invalid_keyword_call))] + InvalidKeywordCall { + keyword: String, + #[label("this call is not properly formed")] + span: Span, + }, + + #[error("Attempted to set branch target of non-branch instruction.")] + #[diagnostic( + code(nu::compile::set_branch_target_of_non_branch_instruction), + help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new"), + )] + SetBranchTargetOfNonBranchInstruction { + instruction: String, + #[label("tried to modify: {instruction}")] + span: Span, + }, + + /// You're trying to run an unsupported external command. + /// + /// ## Resolution + /// + /// Make sure there's an appropriate `run-external` declaration for this external command. + #[error("External calls are not supported.")] + #[diagnostic( + code(nu::compile::run_external_not_found), + help("`run-external` was not found in scope") + )] + RunExternalNotFound { + #[label("can't be run in this context")] + span: Span, + }, + + /// Invalid assignment left-hand side + /// + /// ## Resolution + /// + /// Assignment requires that you assign to a variable or variable cell path. + #[error("Assignment operations require a variable.")] + #[diagnostic( + code(nu::compile::assignment_requires_variable), + help("try assigning to a variable or a cell path of a variable") + )] + AssignmentRequiresVar { + #[label("needs to be a variable")] + span: Span, + }, + + /// Invalid assignment left-hand side + /// + /// ## Resolution + /// + /// Assignment requires that you assign to a mutable variable or cell path. + #[error("Assignment to an immutable variable.")] + #[diagnostic( + code(nu::compile::assignment_requires_mutable_variable), + help("declare the variable with `mut`, or shadow it again with `let`") + )] + AssignmentRequiresMutableVar { + #[label("needs to be a mutable variable")] + span: Span, + }, + + /// This environment variable cannot be set manually. + /// + /// ## Resolution + /// + /// This environment variable is set automatically by Nushell and cannot not be set manually. + #[error("{envvar_name} cannot be set manually.")] + #[diagnostic( + code(nu::compile::automatic_env_var_set_manually), + help( + r#"The environment variable '{envvar_name}' is set automatically by Nushell and cannot be set manually."# + ) + )] + AutomaticEnvVarSetManually { + envvar_name: String, + #[label("cannot set '{envvar_name}' manually")] + span: Span, + }, + + /// It is not possible to replace the entire environment at once + /// + /// ## Resolution + /// + /// Setting the entire environment is not allowed. Change environment variables individually + /// instead. + #[error("Cannot replace environment.")] + #[diagnostic( + code(nu::compile::cannot_replace_env), + help("Assigning a value to '$env' is not allowed.") + )] + CannotReplaceEnv { + #[label("setting '$env' not allowed")] + span: Span, + }, + + #[error("Unexpected expression.")] + #[diagnostic(code(nu::compile::unexpected_expression))] + UnexpectedExpression { + expr_name: String, + #[label("{expr_name} is not allowed in this context")] + span: Span, + }, + + #[error("Missing required declaration: `{decl_name}`")] + #[diagnostic(code(nu::compile::missing_required_declaration))] + MissingRequiredDeclaration { + decl_name: String, + #[label("`{decl_name}` must be in scope to compile this expression")] + span: Span, + }, + + #[error("Invalid literal")] + #[diagnostic(code(nu::compile::invalid_literal))] + InvalidLiteral { + msg: String, + #[label("{msg}")] + span: Span, + }, + + #[error("{msg}")] + #[diagnostic(code(nu::compile::not_in_a_loop))] + NotInALoop { + msg: String, + #[label("can't be used outside of a loop")] + span: Option, + }, + + #[error("Incoherent loop state: the loop that ended was not the one we were expecting.")] + #[diagnostic( + code(nu::compile::incoherent_loop_state), + help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new"), + )] + IncoherentLoopState { + #[label("while compiling this block")] + block_span: Option, + }, + + #[error("Undefined label `{label_id}`.")] + #[diagnostic( + code(nu::compile::undefined_label), + help("this is a compiler bug. Please report it at https://github.com/nushell/nushell/issues/new"), + )] + UndefinedLabel { + label_id: usize, + #[label("label was used while compiling this code")] + span: Option, + }, +} diff --git a/crates/nu-protocol/src/errors/mod.rs b/crates/nu-protocol/src/errors/mod.rs index 23006ab684..59369cd54f 100644 --- a/crates/nu-protocol/src/errors/mod.rs +++ b/crates/nu-protocol/src/errors/mod.rs @@ -1,10 +1,14 @@ pub mod cli_error; +mod compile_error; mod labeled_error; 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}; pub use parse_warning::ParseWarning; diff --git a/crates/nu-protocol/src/errors/parse_error.rs b/crates/nu-protocol/src/errors/parse_error.rs index 4d59821f7d..0e20f0dcba 100644 --- a/crates/nu-protocol/src/errors/parse_error.rs +++ b/crates/nu-protocol/src/errors/parse_error.rs @@ -61,7 +61,11 @@ pub enum ParseError { #[error("Command output doesn't match {0}.")] #[diagnostic(code(nu::parser::output_type_mismatch))] - OutputMismatch(Type, #[label("command doesn't output {0}")] Span), + OutputMismatch( + Type, + Type, + #[label("expected {0}, but command outputs {1}")] Span, + ), #[error("Type mismatch during operation.")] #[diagnostic(code(nu::parser::type_mismatch))] @@ -554,7 +558,7 @@ impl ParseError { ParseError::TypeMismatch(_, _, s) => *s, ParseError::TypeMismatchHelp(_, _, s, _) => *s, ParseError::InputMismatch(_, s) => *s, - ParseError::OutputMismatch(_, s) => *s, + ParseError::OutputMismatch(_, _, s) => *s, ParseError::MissingRequiredFlag(_, s) => *s, ParseError::IncompleteMathExpression(s) => *s, ParseError::UnknownState(_, s) => *s, diff --git a/crates/nu-protocol/src/errors/parse_warning.rs b/crates/nu-protocol/src/errors/parse_warning.rs index a30bc731d8..5f34deb20c 100644 --- a/crates/nu-protocol/src/errors/parse_warning.rs +++ b/crates/nu-protocol/src/errors/parse_warning.rs @@ -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, }, diff --git a/crates/nu-protocol/src/errors/shell_error.rs b/crates/nu-protocol/src/errors/shell_error.rs index 30752d5c9e..ab01ccfa54 100644 --- a/crates/nu-protocol/src/errors/shell_error.rs +++ b/crates/nu-protocol/src/errors/shell_error.rs @@ -1187,6 +1187,13 @@ pub enum ShellError { span: Option, }, + /// Operation interrupted + #[error("Operation interrupted")] + Interrupted { + #[label("This operation was interrupted")] + span: Span, + }, + /// Operation interrupted by user #[error("Operation interrupted by user")] InterruptedByUser { @@ -1369,6 +1376,23 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"# help("Set XDG_CONFIG_HOME to an absolute path, or set it to an empty string to ignore it") )] InvalidXdgConfig { xdg: String, default: String }, + + /// An unexpected error occurred during IR evaluation. + /// + /// ## Resolution + /// + /// This is most likely a correctness issue with the IR compiler or evaluator. Please file a + /// bug with the minimum code needed to reproduce the issue, if possible. + #[error("IR evaluation error: {msg}")] + #[diagnostic( + code(nu::shell::ir_eval_error), + help("this is a bug, please report it at https://github.com/nushell/nushell/issues/new along with the code you were running if able") + )] + IrEvalError { + msg: String, + #[label = "while running this code"] + span: Option, + }, } // TODO: Implement as From trait diff --git a/crates/nu-protocol/src/eval_base.rs b/crates/nu-protocol/src/eval_base.rs index 7e1e2b0a7c..f49d235482 100644 --- a/crates/nu-protocol/src/eval_base.rs +++ b/crates/nu-protocol/src/eval_base.rs @@ -1,3 +1,4 @@ +//! Foundational [`Eval`] trait allowing dispatch between const-eval and regular evaluation use crate::{ ast::{ eval_operator, Assignment, Bits, Boolean, Call, Comparison, Expr, Expression, @@ -6,7 +7,7 @@ use crate::{ debugger::DebugContext, Config, GetSpan, Range, Record, ShellError, Span, Value, VarId, ENV_VARIABLE_ID, }; -use std::{borrow::Cow, collections::HashMap}; +use std::{collections::HashMap, sync::Arc}; /// To share implementations for regular eval and const eval pub trait Eval { @@ -315,7 +316,7 @@ pub trait Eval { } } - fn get_config<'a>(state: Self::State<'a>, mut_state: &mut Self::MutState) -> Cow<'a, Config>; + fn get_config(state: Self::State<'_>, mut_state: &mut Self::MutState) -> Arc; fn eval_filepath( state: Self::State<'_>, diff --git a/crates/nu-protocol/src/eval_const.rs b/crates/nu-protocol/src/eval_const.rs index 87913e4ee3..cba2392338 100644 --- a/crates/nu-protocol/src/eval_const.rs +++ b/crates/nu-protocol/src/eval_const.rs @@ -1,3 +1,7 @@ +//! Implementation of const-evaluation +//! +//! This enables you to assign `const`-constants and execute parse-time code dependent on this. +//! e.g. `source $my_const` use crate::{ ast::{Assignment, Block, Call, Expr, Expression, ExternalArgument}, debugger::{DebugContext, WithoutDebug}, @@ -7,8 +11,8 @@ use crate::{ }; use nu_system::os_info::{get_kernel_version, get_os_arch, get_os_family, get_os_name}; use std::{ - borrow::Cow, path::{Path, PathBuf}, + sync::Arc, }; /// Create a Value for `$nu`. @@ -307,7 +311,7 @@ fn eval_const_call( return Err(ShellError::NotAConstHelp { span: call.head }); } - decl.run_const(working_set, call, input) + decl.run_const(working_set, &call.into(), input) } pub fn eval_const_subexpression( @@ -360,8 +364,8 @@ impl Eval for EvalConst { type MutState = (); - fn get_config<'a>(state: Self::State<'a>, _: &mut ()) -> Cow<'a, Config> { - Cow::Borrowed(state.get_config()) + fn get_config(state: Self::State<'_>, _: &mut ()) -> Arc { + state.get_config().clone() } fn eval_filepath( diff --git a/crates/nu-protocol/src/id.rs b/crates/nu-protocol/src/id.rs index 73c4f52e70..829ee8f36d 100644 --- a/crates/nu-protocol/src/id.rs +++ b/crates/nu-protocol/src/id.rs @@ -7,5 +7,19 @@ pub type ModuleId = usize; pub type OverlayId = usize; pub type FileId = usize; pub type VirtualPathId = usize; + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] pub struct SpanId(pub usize); // more robust ID style used in the new parser + +/// An ID for an [IR](crate::ir) register. `%n` is a common shorthand for `RegId(n)`. +/// +/// Note: `%0` is allocated with the block input at the beginning of a compiled block. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +#[repr(transparent)] +pub struct RegId(pub u32); + +impl std::fmt::Display for RegId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "%{}", self.0) + } +} diff --git a/crates/nu-protocol/src/ir/call.rs b/crates/nu-protocol/src/ir/call.rs new file mode 100644 index 0000000000..3d16f82eb6 --- /dev/null +++ b/crates/nu-protocol/src/ir/call.rs @@ -0,0 +1,351 @@ +use std::sync::Arc; + +use crate::{ + ast::Expression, + engine::{self, Argument, Stack}, + DeclId, ShellError, Span, Spanned, Value, +}; + +use super::DataSlice; + +/// Contains the information for a call being made to a declared command. +#[derive(Debug, Clone)] +pub struct Call { + /// The declaration ID of the command to be invoked. + pub decl_id: DeclId, + /// The span encompassing the command name, before the arguments. + pub head: Span, + /// The span encompassing the command name and all arguments. + pub span: Span, + /// The base index of the arguments for this call within the + /// [argument stack](crate::engine::ArgumentStack). + pub args_base: usize, + /// The number of [`Argument`]s for the call. Note that this just counts the number of + /// `Argument` entries on the stack, and has nothing to do with the actual number of positional + /// or spread arguments. + pub args_len: usize, +} + +impl Call { + /// Build a new call with arguments. + pub fn build(decl_id: DeclId, head: Span) -> CallBuilder { + CallBuilder { + inner: Call { + decl_id, + head, + span: head, + args_base: 0, + args_len: 0, + }, + } + } + + /// Get the arguments for this call from the arguments stack. + pub fn arguments<'a>(&self, stack: &'a Stack) -> &'a [Argument] { + stack.arguments.get_args(self.args_base, self.args_len) + } + + /// The span encompassing the arguments + /// + /// If there are no arguments the span covers where the first argument would exist + /// + /// 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(); + Span::new(past.start, self.span.end) + } + + /// The number of named arguments, with or without values. + pub fn named_len(&self, stack: &Stack) -> usize { + self.arguments(stack) + .iter() + .filter(|arg| matches!(arg, Argument::Named { .. } | Argument::Flag { .. })) + .count() + } + + /// Iterate through named arguments, with or without values. + pub fn named_iter<'a>( + &'a self, + stack: &'a Stack, + ) -> impl Iterator, Option<&'a Value>)> + 'a { + self.arguments(stack).iter().filter_map( + |arg: &Argument| -> Option<(Spanned<&str>, Option<&Value>)> { + match arg { + Argument::Flag { + data, name, span, .. + } => Some(( + Spanned { + item: std::str::from_utf8(&data[*name]).expect("invalid arg name"), + span: *span, + }, + None, + )), + Argument::Named { + data, + name, + span, + val, + .. + } => Some(( + Spanned { + item: std::str::from_utf8(&data[*name]).expect("invalid arg name"), + span: *span, + }, + Some(val), + )), + _ => None, + } + }, + ) + } + + /// Get a named argument's value by name. Returns [`None`] for named arguments with no value as + /// well. + pub fn get_named_arg<'a>(&self, stack: &'a Stack, flag_name: &str) -> Option<&'a Value> { + // Optimized to avoid str::from_utf8() + self.arguments(stack) + .iter() + .find_map(|arg: &Argument| -> Option> { + match arg { + Argument::Flag { data, name, .. } if &data[*name] == flag_name.as_bytes() => { + Some(None) + } + Argument::Named { + data, name, val, .. + } if &data[*name] == flag_name.as_bytes() => Some(Some(val)), + _ => None, + } + }) + .flatten() + } + + /// The number of positional arguments, excluding spread arguments. + pub fn positional_len(&self, stack: &Stack) -> usize { + self.arguments(stack) + .iter() + .filter(|arg| matches!(arg, Argument::Positional { .. })) + .count() + } + + /// Iterate through positional arguments. Does not include spread arguments. + pub fn positional_iter<'a>(&self, stack: &'a Stack) -> impl Iterator { + self.arguments(stack).iter().filter_map(|arg| match arg { + Argument::Positional { val, .. } => Some(val), + _ => None, + }) + } + + /// Get a positional argument by index. Does not include spread arguments. + pub fn positional_nth<'a>(&self, stack: &'a Stack, index: usize) -> Option<&'a Value> { + self.positional_iter(stack).nth(index) + } + + /// Get the AST node for a positional argument by index. Not usually available unless the decl + /// required it. + pub fn positional_ast<'a>( + &self, + stack: &'a Stack, + index: usize, + ) -> Option<&'a Arc> { + self.arguments(stack) + .iter() + .filter_map(|arg| match arg { + Argument::Positional { ast, .. } => Some(ast), + _ => None, + }) + .nth(index) + .and_then(|option| option.as_ref()) + } + + /// Returns every argument to the rest parameter, as well as whether each argument + /// is spread or a normal positional argument (true for spread, false for normal) + pub fn rest_iter<'a>( + &self, + stack: &'a Stack, + start: usize, + ) -> impl Iterator + 'a { + self.arguments(stack) + .iter() + .filter_map(|arg| match arg { + Argument::Positional { val, .. } => Some((val, false)), + Argument::Spread { vals, .. } => Some((vals, true)), + _ => None, + }) + .skip(start) + } + + /// Returns all of the positional arguments including and after `start`, with spread arguments + /// flattened into a single `Vec`. + pub fn rest_iter_flattened( + &self, + stack: &Stack, + start: usize, + ) -> Result, ShellError> { + let mut acc = vec![]; + for (rest_val, spread) in self.rest_iter(stack, start) { + if spread { + match rest_val { + Value::List { vals, .. } => acc.extend(vals.iter().cloned()), + Value::Error { error, .. } => return Err(ShellError::clone(error)), + _ => { + return Err(ShellError::CannotSpreadAsList { + span: rest_val.span(), + }) + } + } + } else { + acc.push(rest_val.clone()); + } + } + Ok(acc) + } + + /// Get a parser info argument by name. + pub fn get_parser_info<'a>(&self, stack: &'a Stack, name: &str) -> Option<&'a Expression> { + self.arguments(stack) + .iter() + .find_map(|argument| match argument { + Argument::ParserInfo { + data, + name: name_slice, + info: expr, + } if &data[*name_slice] == name.as_bytes() => Some(expr.as_ref()), + _ => None, + }) + } + + /// Returns a span encompassing the entire call. + pub fn span(&self) -> Span { + self.span + } + + /// Resets the [`Stack`] to its state before the call was made. + pub fn leave(&self, stack: &mut Stack) { + stack.arguments.leave_frame(self.args_base); + } +} + +/// Utility struct for building a [`Call`] with arguments on the [`Stack`]. +pub struct CallBuilder { + inner: Call, +} + +impl CallBuilder { + /// Add an argument to the [`Stack`] and reference it from the [`Call`]. + pub fn add_argument(&mut self, stack: &mut Stack, argument: Argument) -> &mut Self { + if self.inner.args_len == 0 { + self.inner.args_base = stack.arguments.get_base(); + } + self.inner.args_len += 1; + if let Some(span) = argument.span() { + self.inner.span = self.inner.span.append(span); + } + stack.arguments.push(argument); + self + } + + /// Add a positional argument to the [`Stack`] and reference it from the [`Call`]. + pub fn add_positional(&mut self, stack: &mut Stack, span: Span, val: Value) -> &mut Self { + self.add_argument( + stack, + Argument::Positional { + span, + val, + ast: None, + }, + ) + } + + /// Add a spread argument to the [`Stack`] and reference it from the [`Call`]. + pub fn add_spread(&mut self, stack: &mut Stack, span: Span, vals: Value) -> &mut Self { + self.add_argument( + stack, + Argument::Spread { + span, + vals, + ast: None, + }, + ) + } + + /// Add a flag (no-value named) argument to the [`Stack`] and reference it from the [`Call`]. + pub fn add_flag( + &mut self, + stack: &mut Stack, + name: impl AsRef, + short: impl AsRef, + span: Span, + ) -> &mut Self { + let (data, name, short) = data_from_name_and_short(name.as_ref(), short.as_ref()); + self.add_argument( + stack, + Argument::Flag { + data, + name, + short, + span, + }, + ) + } + + /// Add a named argument to the [`Stack`] and reference it from the [`Call`]. + pub fn add_named( + &mut self, + stack: &mut Stack, + name: impl AsRef, + short: impl AsRef, + span: Span, + val: Value, + ) -> &mut Self { + let (data, name, short) = data_from_name_and_short(name.as_ref(), short.as_ref()); + self.add_argument( + stack, + Argument::Named { + data, + name, + short, + span, + val, + ast: None, + }, + ) + } + + /// Produce the finished [`Call`] from the builder. + /// + /// The call should be entered / run before any other calls are constructed, because the + /// argument stack will be reset when they exit. + pub fn finish(&self) -> Call { + self.inner.clone() + } + + /// Run a closure with the [`Call`] as an [`engine::Call`] reference, and then clean up the + /// arguments that were added to the [`Stack`] after. + /// + /// For convenience. Calls [`Call::leave`] after the closure ends. + pub fn with( + self, + stack: &mut Stack, + f: impl FnOnce(&mut Stack, &engine::Call<'_>) -> T, + ) -> T { + let call = engine::Call::from(&self.inner); + let result = f(stack, &call); + self.inner.leave(stack); + result + } +} + +fn data_from_name_and_short(name: &str, short: &str) -> (Arc<[u8]>, DataSlice, DataSlice) { + let data: Vec = name.bytes().chain(short.bytes()).collect(); + let data: Arc<[u8]> = data.into(); + let name = DataSlice { + start: 0, + len: name.len().try_into().expect("flag name too big"), + }; + let short = DataSlice { + start: name.start.checked_add(name.len).expect("flag name too big"), + len: short.len().try_into().expect("flag short name too big"), + }; + (data, name, short) +} diff --git a/crates/nu-protocol/src/ir/display.rs b/crates/nu-protocol/src/ir/display.rs new file mode 100644 index 0000000000..c28323cca4 --- /dev/null +++ b/crates/nu-protocol/src/ir/display.rs @@ -0,0 +1,452 @@ +use std::fmt; + +use crate::{ast::Pattern, engine::EngineState, DeclId, VarId}; + +use super::{DataSlice, Instruction, IrBlock, Literal, RedirectMode}; + +pub struct FmtIrBlock<'a> { + pub(super) engine_state: &'a EngineState, + pub(super) ir_block: &'a IrBlock, +} + +impl<'a> fmt::Display for FmtIrBlock<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let plural = |count| if count == 1 { "" } else { "s" }; + writeln!( + f, + "# {} register{}, {} instruction{}, {} byte{} of data", + self.ir_block.register_count, + plural(self.ir_block.register_count as usize), + self.ir_block.instructions.len(), + plural(self.ir_block.instructions.len()), + self.ir_block.data.len(), + plural(self.ir_block.data.len()), + )?; + if self.ir_block.file_count > 0 { + writeln!( + f, + "# {} file{} used for redirection", + self.ir_block.file_count, + plural(self.ir_block.file_count as usize) + )?; + } + for (index, instruction) in self.ir_block.instructions.iter().enumerate() { + let formatted = format!( + "{:-4}: {}", + index, + FmtInstruction { + engine_state: self.engine_state, + instruction, + data: &self.ir_block.data, + } + ); + let comment = &self.ir_block.comments[index]; + if comment.is_empty() { + writeln!(f, "{formatted}")?; + } else { + writeln!(f, "{formatted:40} # {comment}")?; + } + } + Ok(()) + } +} + +pub struct FmtInstruction<'a> { + pub(super) engine_state: &'a EngineState, + pub(super) instruction: &'a Instruction, + pub(super) data: &'a [u8], +} + +impl<'a> fmt::Display for FmtInstruction<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + const WIDTH: usize = 22; + + match self.instruction { + Instruction::Unreachable => { + write!(f, "{:WIDTH$}", "unreachable") + } + Instruction::LoadLiteral { dst, lit } => { + let lit = FmtLiteral { + literal: lit, + data: self.data, + }; + write!(f, "{:WIDTH$} {dst}, {lit}", "load-literal") + } + Instruction::LoadValue { dst, val } => { + let val = val.to_debug_string(); + write!(f, "{:WIDTH$} {dst}, {val}", "load-value") + } + Instruction::Move { dst, src } => { + write!(f, "{:WIDTH$} {dst}, {src}", "move") + } + Instruction::Clone { dst, src } => { + write!(f, "{:WIDTH$} {dst}, {src}", "clone") + } + Instruction::Collect { src_dst } => { + write!(f, "{:WIDTH$} {src_dst}", "collect") + } + Instruction::Span { src_dst } => { + write!(f, "{:WIDTH$} {src_dst}", "span") + } + Instruction::Drop { src } => { + write!(f, "{:WIDTH$} {src}", "drop") + } + Instruction::Drain { src } => { + write!(f, "{:WIDTH$} {src}", "drain") + } + Instruction::LoadVariable { dst, var_id } => { + let var = FmtVar::new(self.engine_state, *var_id); + write!(f, "{:WIDTH$} {dst}, {var}", "load-variable") + } + Instruction::StoreVariable { var_id, src } => { + let var = FmtVar::new(self.engine_state, *var_id); + write!(f, "{:WIDTH$} {var}, {src}", "store-variable") + } + Instruction::LoadEnv { dst, key } => { + let key = FmtData(self.data, *key); + write!(f, "{:WIDTH$} {dst}, {key}", "load-env") + } + Instruction::LoadEnvOpt { dst, key } => { + let key = FmtData(self.data, *key); + write!(f, "{:WIDTH$} {dst}, {key}", "load-env-opt") + } + Instruction::StoreEnv { key, src } => { + let key = FmtData(self.data, *key); + write!(f, "{:WIDTH$} {key}, {src}", "store-env") + } + Instruction::PushPositional { src } => { + write!(f, "{:WIDTH$} {src}", "push-positional") + } + Instruction::AppendRest { src } => { + write!(f, "{:WIDTH$} {src}", "append-rest") + } + Instruction::PushFlag { name } => { + let name = FmtData(self.data, *name); + write!(f, "{:WIDTH$} {name}", "push-flag") + } + Instruction::PushShortFlag { short } => { + let short = FmtData(self.data, *short); + write!(f, "{:WIDTH$} {short}", "push-short-flag") + } + Instruction::PushNamed { name, src } => { + let name = FmtData(self.data, *name); + write!(f, "{:WIDTH$} {name}, {src}", "push-named") + } + Instruction::PushShortNamed { short, src } => { + let short = FmtData(self.data, *short); + write!(f, "{:WIDTH$} {short}, {src}", "push-short-named") + } + Instruction::PushParserInfo { name, info } => { + let name = FmtData(self.data, *name); + write!(f, "{:WIDTH$} {name}, {info:?}", "push-parser-info") + } + Instruction::RedirectOut { mode } => { + write!(f, "{:WIDTH$} {mode}", "redirect-out") + } + Instruction::RedirectErr { mode } => { + write!(f, "{:WIDTH$} {mode}", "redirect-err") + } + Instruction::CheckErrRedirected { src } => { + write!(f, "{:WIDTH$} {src}", "check-err-redirected") + } + Instruction::OpenFile { + file_num, + path, + append, + } => { + write!( + f, + "{:WIDTH$} file({file_num}), {path}, append = {append:?}", + "open-file" + ) + } + Instruction::WriteFile { file_num, src } => { + write!(f, "{:WIDTH$} file({file_num}), {src}", "write-file") + } + Instruction::CloseFile { file_num } => { + write!(f, "{:WIDTH$} file({file_num})", "close-file") + } + Instruction::Call { decl_id, src_dst } => { + let decl = FmtDecl::new(self.engine_state, *decl_id); + write!(f, "{:WIDTH$} {decl}, {src_dst}", "call") + } + Instruction::StringAppend { src_dst, val } => { + write!(f, "{:WIDTH$} {src_dst}, {val}", "string-append") + } + Instruction::GlobFrom { src_dst, no_expand } => { + let no_expand = if *no_expand { "no-expand" } else { "expand" }; + write!(f, "{:WIDTH$} {src_dst}, {no_expand}", "glob-from",) + } + Instruction::ListPush { src_dst, item } => { + write!(f, "{:WIDTH$} {src_dst}, {item}", "list-push") + } + Instruction::ListSpread { src_dst, items } => { + write!(f, "{:WIDTH$} {src_dst}, {items}", "list-spread") + } + Instruction::RecordInsert { src_dst, key, val } => { + write!(f, "{:WIDTH$} {src_dst}, {key}, {val}", "record-insert") + } + Instruction::RecordSpread { src_dst, items } => { + write!(f, "{:WIDTH$} {src_dst}, {items}", "record-spread") + } + Instruction::Not { src_dst } => { + write!(f, "{:WIDTH$} {src_dst}", "not") + } + Instruction::BinaryOp { lhs_dst, op, rhs } => { + write!(f, "{:WIDTH$} {lhs_dst}, {op:?}, {rhs}", "binary-op") + } + Instruction::FollowCellPath { src_dst, path } => { + write!(f, "{:WIDTH$} {src_dst}, {path}", "follow-cell-path") + } + Instruction::CloneCellPath { dst, src, path } => { + write!(f, "{:WIDTH$} {dst}, {src}, {path}", "clone-cell-path") + } + Instruction::UpsertCellPath { + src_dst, + path, + new_value, + } => { + write!( + f, + "{:WIDTH$} {src_dst}, {path}, {new_value}", + "upsert-cell-path" + ) + } + Instruction::Jump { index } => { + write!(f, "{:WIDTH$} {index}", "jump") + } + Instruction::BranchIf { cond, index } => { + write!(f, "{:WIDTH$} {cond}, {index}", "branch-if") + } + Instruction::BranchIfEmpty { src, index } => { + write!(f, "{:WIDTH$} {src}, {index}", "branch-if-empty") + } + Instruction::Match { + pattern, + src, + index, + } => { + let pattern = FmtPattern { + engine_state: self.engine_state, + pattern, + }; + write!(f, "{:WIDTH$} ({pattern}), {src}, {index}", "match") + } + Instruction::CheckMatchGuard { src } => { + write!(f, "{:WIDTH$} {src}", "check-match-guard") + } + Instruction::Iterate { + dst, + stream, + end_index, + } => { + write!(f, "{:WIDTH$} {dst}, {stream}, end {end_index}", "iterate") + } + Instruction::OnError { index } => { + write!(f, "{:WIDTH$} {index}", "on-error") + } + Instruction::OnErrorInto { index, dst } => { + write!(f, "{:WIDTH$} {index}, {dst}", "on-error-into") + } + Instruction::PopErrorHandler => { + write!(f, "{:WIDTH$}", "pop-error-handler") + } + Instruction::CheckExternalFailed { dst, src } => { + write!(f, "{:WIDTH$} {dst}, {src}", "check-external-failed") + } + Instruction::ReturnEarly { src } => { + write!(f, "{:WIDTH$} {src}", "return-early") + } + Instruction::Return { src } => { + write!(f, "{:WIDTH$} {src}", "return") + } + } + } +} + +struct FmtDecl<'a>(DeclId, &'a str); + +impl<'a> FmtDecl<'a> { + fn new(engine_state: &'a EngineState, decl_id: DeclId) -> Self { + FmtDecl(decl_id, engine_state.get_decl(decl_id).name()) + } +} + +impl fmt::Display for FmtDecl<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "decl {} {:?}", self.0, self.1) + } +} + +struct FmtVar<'a>(DeclId, Option<&'a str>); + +impl<'a> FmtVar<'a> { + fn new(engine_state: &'a EngineState, var_id: VarId) -> Self { + // Search for the name of the variable + let name: Option<&str> = engine_state + .active_overlays(&[]) + .flat_map(|overlay| overlay.vars.iter()) + .find(|(_, v)| **v == var_id) + .map(|(k, _)| std::str::from_utf8(k).unwrap_or("")); + FmtVar(var_id, name) + } +} + +impl fmt::Display for FmtVar<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(name) = self.1 { + write!(f, "var {} {:?}", self.0, name) + } else { + write!(f, "var {}", self.0) + } + } +} + +impl fmt::Display for RedirectMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RedirectMode::Pipe => write!(f, "pipe"), + RedirectMode::Capture => write!(f, "capture"), + RedirectMode::Null => write!(f, "null"), + RedirectMode::Inherit => write!(f, "inherit"), + RedirectMode::File { file_num } => write!(f, "file({file_num})"), + RedirectMode::Caller => write!(f, "caller"), + } + } +} + +struct FmtData<'a>(&'a [u8], DataSlice); + +impl<'a> fmt::Display for FmtData<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Ok(s) = std::str::from_utf8(&self.0[self.1]) { + // Write as string + write!(f, "{s:?}") + } else { + // Write as byte array + write!(f, "0x{:x?}", self.0) + } + } +} + +struct FmtLiteral<'a> { + literal: &'a Literal, + data: &'a [u8], +} + +impl<'a> fmt::Display for FmtLiteral<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.literal { + Literal::Bool(b) => write!(f, "bool({b:?})"), + Literal::Int(i) => write!(f, "int({i:?})"), + Literal::Float(fl) => write!(f, "float({fl:?})"), + Literal::Filesize(q) => write!(f, "filesize({q}b)"), + Literal::Duration(q) => write!(f, "duration({q}ns)"), + Literal::Binary(b) => write!(f, "binary({})", FmtData(self.data, *b)), + Literal::Block(id) => write!(f, "block({id})"), + Literal::Closure(id) => write!(f, "closure({id})"), + Literal::RowCondition(id) => write!(f, "row_condition({id})"), + Literal::Range { + start, + step, + end, + inclusion, + } => write!(f, "range({start}, {step}, {end}, {inclusion:?})"), + Literal::List { capacity } => write!(f, "list(capacity = {capacity})"), + Literal::Record { capacity } => write!(f, "record(capacity = {capacity})"), + Literal::Filepath { val, no_expand } => write!( + f, + "filepath({}, no_expand = {no_expand:?})", + FmtData(self.data, *val) + ), + Literal::Directory { val, no_expand } => write!( + f, + "directory({}, no_expand = {no_expand:?})", + FmtData(self.data, *val) + ), + Literal::GlobPattern { val, no_expand } => write!( + f, + "glob-pattern({}, no_expand = {no_expand:?})", + FmtData(self.data, *val) + ), + Literal::String(s) => write!(f, "string({})", FmtData(self.data, *s)), + Literal::RawString(rs) => write!(f, "raw-string({})", FmtData(self.data, *rs)), + Literal::CellPath(p) => write!(f, "cell-path({p})"), + Literal::Date(dt) => write!(f, "date({dt})"), + Literal::Nothing => write!(f, "nothing"), + } + } +} + +struct FmtPattern<'a> { + engine_state: &'a EngineState, + pattern: &'a Pattern, +} + +impl<'a> fmt::Display for FmtPattern<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.pattern { + Pattern::Record(bindings) => { + f.write_str("{")?; + for (name, pattern) in bindings { + write!( + f, + "{}: {}", + name, + FmtPattern { + engine_state: self.engine_state, + pattern: &pattern.pattern, + } + )?; + } + f.write_str("}") + } + Pattern::List(bindings) => { + f.write_str("[")?; + for pattern in bindings { + write!( + f, + "{}", + FmtPattern { + engine_state: self.engine_state, + pattern: &pattern.pattern + } + )?; + } + f.write_str("]") + } + Pattern::Value(expr) => { + let string = + String::from_utf8_lossy(self.engine_state.get_span_contents(expr.span)); + f.write_str(&string) + } + Pattern::Variable(var_id) => { + let variable = FmtVar::new(self.engine_state, *var_id); + write!(f, "{}", variable) + } + Pattern::Or(patterns) => { + for (index, pattern) in patterns.iter().enumerate() { + if index > 0 { + f.write_str(" | ")?; + } + write!( + f, + "{}", + FmtPattern { + engine_state: self.engine_state, + pattern: &pattern.pattern + } + )?; + } + Ok(()) + } + Pattern::Rest(var_id) => { + let variable = FmtVar::new(self.engine_state, *var_id); + write!(f, "..{}", variable) + } + Pattern::IgnoreRest => f.write_str(".."), + Pattern::IgnoreValue => f.write_str("_"), + Pattern::Garbage => f.write_str(""), + } + } +} diff --git a/crates/nu-protocol/src/ir/mod.rs b/crates/nu-protocol/src/ir/mod.rs new file mode 100644 index 0000000000..aff5f42c8c --- /dev/null +++ b/crates/nu-protocol/src/ir/mod.rs @@ -0,0 +1,476 @@ +use std::{fmt, sync::Arc}; + +use crate::{ + ast::{CellPath, Expression, Operator, Pattern, RangeInclusion}, + engine::EngineState, + BlockId, DeclId, RegId, Span, Value, VarId, +}; + +use chrono::{DateTime, FixedOffset}; +use serde::{Deserialize, Serialize}; + +mod call; +mod display; + +pub use call::*; +pub use display::{FmtInstruction, FmtIrBlock}; + +#[derive(Clone, Serialize, Deserialize)] +pub struct IrBlock { + pub instructions: Vec, + pub spans: Vec, + #[serde(with = "serde_arc_u8_array")] + pub data: Arc<[u8]>, + pub ast: Vec>, + /// Additional information that can be added to help with debugging + pub comments: Vec>, + pub register_count: u32, + pub file_count: u32, +} + +impl fmt::Debug for IrBlock { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // the ast field is too verbose and doesn't add much + f.debug_struct("IrBlock") + .field("instructions", &self.instructions) + .field("spans", &self.spans) + .field("data", &self.data) + .field("comments", &self.comments) + .field("register_count", &self.register_count) + .field("file_count", &self.file_count) + .finish_non_exhaustive() + } +} + +impl IrBlock { + /// Returns a value that can be formatted with [`Display`](std::fmt::Display) to show a detailed + /// listing of the instructions contained within this [`IrBlock`]. + pub fn display<'a>(&'a self, engine_state: &'a EngineState) -> FmtIrBlock<'a> { + FmtIrBlock { + engine_state, + ir_block: self, + } + } +} + +/// A slice into the `data` array of a block. This is a compact and cache-friendly way to store +/// string data that a block uses. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct DataSlice { + pub start: u32, + pub len: u32, +} + +impl DataSlice { + /// A data slice that contains no data. This slice is always valid. + pub const fn empty() -> DataSlice { + DataSlice { start: 0, len: 0 } + } +} + +impl std::ops::Index for [u8] { + type Output = [u8]; + + fn index(&self, index: DataSlice) -> &Self::Output { + &self[index.start as usize..(index.start as usize + index.len as usize)] + } +} + +/// A possible reference into the abstract syntax tree for an instruction. This is not present for +/// most instructions and is just added when needed. +#[derive(Debug, Clone)] +pub struct IrAstRef(pub Arc); + +impl Serialize for IrAstRef { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.as_ref().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for IrAstRef { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Expression::deserialize(deserializer).map(|expr| IrAstRef(Arc::new(expr))) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Instruction { + /// Unreachable code path (error) + Unreachable, + /// Load a literal value into the `dst` register + LoadLiteral { dst: RegId, lit: Literal }, + /// Load a clone of a boxed value into the `dst` register (e.g. from const evaluation) + LoadValue { dst: RegId, val: Box }, + /// Move a register. Value is taken from `src` (used by this instruction). + Move { dst: RegId, src: RegId }, + /// Copy a register (must be a collected value). Value is still in `src` after this instruction. + Clone { dst: RegId, src: RegId }, + /// Collect a stream in a register to a value + Collect { src_dst: RegId }, + /// Change the span of the contents of a register to the span of this instruction. + Span { src_dst: RegId }, + /// Drop the value/stream in a register, without draining + Drop { src: RegId }, + /// Drain the value/stream in a register and discard (e.g. semicolon). + /// + /// If passed a stream from an external command, sets $env.LAST_EXIT_CODE to the resulting exit + /// code, and invokes any available error handler with Empty, or if not available, returns an + /// exit-code-only stream, leaving the block. + Drain { src: RegId }, + /// Load the value of a variable into the `dst` register + LoadVariable { dst: RegId, var_id: VarId }, + /// Store the value of a variable from the `src` register + StoreVariable { var_id: VarId, src: RegId }, + /// 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 + /// doesn't exist + LoadEnvOpt { dst: RegId, key: DataSlice }, + /// Store the value of an environment variable from the `src` register + StoreEnv { key: DataSlice, src: RegId }, + /// Add a positional arg to the next (internal) call. + PushPositional { src: RegId }, + /// Add a list of args to the next (internal) call (spread/rest). + AppendRest { src: RegId }, + /// Add a named arg with no value to the next (internal) call. + PushFlag { name: DataSlice }, + /// Add a short named arg with no value to the next (internal) call. + PushShortFlag { short: DataSlice }, + /// Add a named arg with a value to the next (internal) call. + PushNamed { name: DataSlice, src: RegId }, + /// Add a short named arg with a value to the next (internal) call. + PushShortNamed { short: DataSlice, src: RegId }, + /// Add parser info to the next (internal) call. + PushParserInfo { + name: DataSlice, + info: Box, + }, + /// Set the redirection for stdout for the next call (only). + /// + /// The register for a file redirection is not consumed. + RedirectOut { mode: RedirectMode }, + /// Set the redirection for stderr for the next call (only). + /// + /// The register for a file redirection is not consumed. + RedirectErr { mode: RedirectMode }, + /// Throw an error if stderr wasn't redirected in the given stream. `src` is preserved. + CheckErrRedirected { src: RegId }, + /// Open a file for redirection, pushing it onto the file stack. + OpenFile { + file_num: u32, + path: RegId, + append: bool, + }, + /// Write data from the register to a file. This is done to finish a file redirection, in case + /// an internal command or expression was evaluated rather than an external one. + WriteFile { file_num: u32, src: RegId }, + /// Pop a file used for redirection from the file stack. + CloseFile { file_num: u32 }, + /// Make a call. The input is taken from `src_dst`, and the output is placed in `src_dst`, + /// overwriting it. The argument stack is used implicitly and cleared when the call ends. + Call { decl_id: DeclId, src_dst: RegId }, + /// Append a value onto the end of a string. Uses `to_expanded_string(", ", ...)` on the value. + /// Used for string interpolation literals. Not the same thing as the `++` operator. + StringAppend { src_dst: RegId, val: RegId }, + /// Convert a string into a glob. Used for glob interpolation and setting glob variables. If the + /// value is already a glob, it won't be modified (`no_expand` will have no effect). + GlobFrom { src_dst: RegId, no_expand: bool }, + /// Push a value onto the end of a list. Used to construct list literals. + ListPush { src_dst: RegId, item: RegId }, + /// Spread a value onto the end of a list. Used to construct list literals. + ListSpread { src_dst: RegId, items: RegId }, + /// Insert a key-value pair into a record. Used to construct record literals. Raises an error if + /// the key already existed in the record. + RecordInsert { + src_dst: RegId, + key: RegId, + val: RegId, + }, + /// Spread a record onto a record. Used to construct record literals. Any existing value for the + /// key is overwritten. + RecordSpread { src_dst: RegId, items: RegId }, + /// Negate a boolean. + Not { src_dst: RegId }, + /// Do a binary operation on `lhs_dst` (left) and `rhs` (right) and write the result to + /// `lhs_dst`. + BinaryOp { + lhs_dst: RegId, + op: Operator, + rhs: RegId, + }, + /// Follow a cell path on the value in `src_dst`, storing the result back to `src_dst` + FollowCellPath { src_dst: RegId, path: RegId }, + /// Clone the value at a cell path in `src`, storing the result to `dst`. The original value + /// remains in `src`. Must be a collected value. + CloneCellPath { dst: RegId, src: RegId, path: RegId }, + /// Update/insert a cell path to `new_value` on the value in `src_dst`, storing the modified + /// value back to `src_dst` + UpsertCellPath { + src_dst: RegId, + path: RegId, + new_value: RegId, + }, + /// Jump to an offset in this block + Jump { index: usize }, + /// Branch to an offset in this block if the value of the `cond` register is a true boolean, + /// otherwise continue execution + BranchIf { cond: RegId, index: usize }, + /// Branch to an offset in this block if the value of the `src` register is Empty or Nothing, + /// otherwise continue execution. The original value in `src` is preserved. + BranchIfEmpty { src: RegId, index: usize }, + /// Match a pattern on `src`. If the pattern matches, branch to `index` after having set any + /// variables captured by the pattern. If the pattern doesn't match, continue execution. The + /// original value is preserved in `src` through this instruction. + Match { + pattern: Box, + src: RegId, + index: usize, + }, + /// Check that a match guard is a boolean, throwing + /// [`MatchGuardNotBool`](crate::ShellError::MatchGuardNotBool) if it isn't. Preserves `src`. + CheckMatchGuard { src: RegId }, + /// Iterate on register `stream`, putting the next value in `dst` if present, or jumping to + /// `end_index` if the iterator is finished + Iterate { + dst: RegId, + stream: RegId, + end_index: usize, + }, + /// Push an error handler, without capturing the error value + OnError { index: usize }, + /// Push an error handler, capturing the error value into `dst`. If the error handler is not + /// called, the register should be freed manually. + OnErrorInto { index: usize, dst: RegId }, + /// Pop an error handler. This is not necessary when control flow is directed to the error + /// handler due to an error. + PopErrorHandler, + /// Check if an external command failed. Boolean value into `dst`. `src` is preserved, but it + /// does require waiting for the command to exit. + CheckExternalFailed { dst: RegId, src: RegId }, + /// Return early from the block, raising a `ShellError::Return` instead. + /// + /// Collecting the value is unavoidable. + ReturnEarly { src: RegId }, + /// Return from the block with the value in the register + Return { src: RegId }, +} + +impl Instruction { + /// Returns a value that can be formatted with [`Display`](std::fmt::Display) to show a detailed + /// listing of the instruction. + pub fn display<'a>( + &'a self, + engine_state: &'a EngineState, + data: &'a [u8], + ) -> FmtInstruction<'a> { + FmtInstruction { + engine_state, + instruction: self, + data, + } + } + + /// Get the output register, for instructions that produce some kind of immediate result. + pub fn output_register(&self) -> Option { + match *self { + Instruction::Unreachable => None, + Instruction::LoadLiteral { dst, .. } => Some(dst), + Instruction::LoadValue { dst, .. } => Some(dst), + Instruction::Move { dst, .. } => Some(dst), + Instruction::Clone { dst, .. } => Some(dst), + Instruction::Collect { src_dst } => Some(src_dst), + Instruction::Span { src_dst } => Some(src_dst), + Instruction::Drop { .. } => None, + Instruction::Drain { .. } => None, + Instruction::LoadVariable { dst, .. } => Some(dst), + Instruction::StoreVariable { .. } => None, + Instruction::LoadEnv { dst, .. } => Some(dst), + Instruction::LoadEnvOpt { dst, .. } => Some(dst), + Instruction::StoreEnv { .. } => None, + Instruction::PushPositional { .. } => None, + Instruction::AppendRest { .. } => None, + Instruction::PushFlag { .. } => None, + Instruction::PushShortFlag { .. } => None, + Instruction::PushNamed { .. } => None, + Instruction::PushShortNamed { .. } => None, + Instruction::PushParserInfo { .. } => None, + Instruction::RedirectOut { .. } => None, + Instruction::RedirectErr { .. } => None, + Instruction::CheckErrRedirected { .. } => None, + Instruction::OpenFile { .. } => None, + Instruction::WriteFile { .. } => None, + Instruction::CloseFile { .. } => None, + Instruction::Call { src_dst, .. } => Some(src_dst), + Instruction::StringAppend { src_dst, .. } => Some(src_dst), + Instruction::GlobFrom { src_dst, .. } => Some(src_dst), + Instruction::ListPush { src_dst, .. } => Some(src_dst), + Instruction::ListSpread { src_dst, .. } => Some(src_dst), + Instruction::RecordInsert { src_dst, .. } => Some(src_dst), + Instruction::RecordSpread { src_dst, .. } => Some(src_dst), + Instruction::Not { src_dst } => Some(src_dst), + Instruction::BinaryOp { lhs_dst, .. } => Some(lhs_dst), + Instruction::FollowCellPath { src_dst, .. } => Some(src_dst), + Instruction::CloneCellPath { dst, .. } => Some(dst), + Instruction::UpsertCellPath { src_dst, .. } => Some(src_dst), + Instruction::Jump { .. } => None, + Instruction::BranchIf { .. } => None, + Instruction::BranchIfEmpty { .. } => None, + Instruction::Match { .. } => None, + Instruction::CheckMatchGuard { .. } => None, + Instruction::Iterate { dst, .. } => Some(dst), + Instruction::OnError { .. } => None, + Instruction::OnErrorInto { .. } => None, + Instruction::PopErrorHandler => None, + Instruction::CheckExternalFailed { dst, .. } => Some(dst), + Instruction::ReturnEarly { .. } => None, + Instruction::Return { .. } => None, + } + } + + /// Returns the branch target index of the instruction if this is a branching instruction. + pub fn branch_target(&self) -> Option { + match self { + Instruction::Jump { index } => Some(*index), + Instruction::BranchIf { cond: _, index } => Some(*index), + Instruction::BranchIfEmpty { src: _, index } => Some(*index), + Instruction::Match { + pattern: _, + src: _, + index, + } => Some(*index), + + Instruction::Iterate { + dst: _, + stream: _, + end_index, + } => Some(*end_index), + Instruction::OnError { index } => Some(*index), + Instruction::OnErrorInto { index, dst: _ } => Some(*index), + _ => None, + } + } + + /// Sets the branch target of the instruction if this is a branching instruction. + /// + /// Returns `Err(target_index)` if it isn't a branching instruction. + pub fn set_branch_target(&mut self, target_index: usize) -> Result<(), usize> { + match self { + Instruction::Jump { index } => *index = target_index, + Instruction::BranchIf { cond: _, index } => *index = target_index, + Instruction::BranchIfEmpty { src: _, index } => *index = target_index, + Instruction::Match { + pattern: _, + src: _, + index, + } => *index = target_index, + + Instruction::Iterate { + dst: _, + stream: _, + end_index, + } => *end_index = target_index, + Instruction::OnError { index } => *index = target_index, + Instruction::OnErrorInto { index, dst: _ } => *index = target_index, + _ => return Err(target_index), + } + Ok(()) + } +} + +// This is to document/enforce the size of `Instruction` in bytes. +// We should try to avoid increasing the size of `Instruction`, +// and PRs that do so will have to change the number below so that it's noted in review. +const _: () = assert!(std::mem::size_of::() <= 24); + +/// A literal value that can be embedded in an instruction. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Literal { + Bool(bool), + Int(i64), + Float(f64), + Filesize(i64), + Duration(i64), + Binary(DataSlice), + Block(BlockId), + Closure(BlockId), + RowCondition(BlockId), + Range { + start: RegId, + step: RegId, + end: RegId, + inclusion: RangeInclusion, + }, + List { + capacity: usize, + }, + Record { + capacity: usize, + }, + Filepath { + val: DataSlice, + no_expand: bool, + }, + Directory { + val: DataSlice, + no_expand: bool, + }, + GlobPattern { + val: DataSlice, + no_expand: bool, + }, + String(DataSlice), + RawString(DataSlice), + CellPath(Box), + Date(Box>), + Nothing, +} + +/// A redirection mode for the next call. See [`OutDest`](crate::OutDest). +/// +/// This is generated by: +/// +/// 1. Explicit redirection in a [`PipelineElement`](crate::ast::PipelineElement), or +/// 2. The [`pipe_redirection()`](crate::engine::Command::pipe_redirection) of the command being +/// piped into. +/// +/// Not setting it uses the default, determined by [`Stack`](crate::engine::Stack). +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub enum RedirectMode { + Pipe, + Capture, + Null, + Inherit, + /// Use the given numbered file. + File { + file_num: u32, + }, + /// Use the redirection mode requested by the caller, for a pre-return call. + Caller, +} + +/// Just a hack to allow `Arc<[u8]>` to be serialized and deserialized +mod serde_arc_u8_array { + use serde::{Deserialize, Serialize}; + use std::sync::Arc; + + pub fn serialize(data: &Arc<[u8]>, ser: S) -> Result + where + S: serde::Serializer, + { + data.as_ref().serialize(ser) + } + + pub fn deserialize<'de, D>(de: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let data: Vec = Deserialize::deserialize(de)?; + Ok(data.into()) + } +} diff --git a/crates/nu-protocol/src/lib.rs b/crates/nu-protocol/src/lib.rs index 9c176953d5..e143a19819 100644 --- a/crates/nu-protocol/src/lib.rs +++ b/crates/nu-protocol/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod alias; pub mod ast; pub mod config; @@ -9,6 +10,7 @@ pub mod eval_base; pub mod eval_const; mod example; mod id; +pub mod ir; mod lev_distance; mod module; pub mod parser_path; diff --git a/crates/nu-protocol/src/pipeline/byte_stream.rs b/crates/nu-protocol/src/pipeline/byte_stream.rs index babd195a9e..bd384e88c2 100644 --- a/crates/nu-protocol/src/pipeline/byte_stream.rs +++ b/crates/nu-protocol/src/pipeline/byte_stream.rs @@ -1,9 +1,9 @@ -use serde::{Deserialize, Serialize}; - +//! Module managing the streaming of raw bytes between pipeline elements use crate::{ process::{ChildPipe, ChildProcess, ExitStatus}, - ErrSpan, IntoSpanned, OutDest, PipelineData, ShellError, Span, Type, Value, + ErrSpan, IntoSpanned, OutDest, PipelineData, ShellError, Signals, Span, Type, Value, }; +use serde::{Deserialize, Serialize}; #[cfg(unix)] use std::os::fd::OwnedFd; #[cfg(windows)] @@ -13,10 +13,6 @@ use std::{ fs::File, io::{self, BufRead, BufReader, Cursor, ErrorKind, Read, Write}, process::Stdio, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, thread, }; @@ -163,14 +159,14 @@ impl From for Type { /// Try not to use this method if possible. Rather, please use [`reader`](ByteStream::reader) /// (or [`lines`](ByteStream::lines) if it matches the situation). /// -/// Additionally, there are few methods to collect a [`Bytestream`] into memory: +/// Additionally, there are few methods to collect a [`ByteStream`] into memory: /// - [`into_bytes`](ByteStream::into_bytes): collects all bytes into a [`Vec`]. /// - [`into_string`](ByteStream::into_string): collects all bytes into a [`String`], erroring if utf-8 decoding failed. /// - [`into_value`](ByteStream::into_value): collects all bytes into a value typed appropriately /// for the [type](.type_()) of this stream. If the type is [`Unknown`](ByteStreamType::Unknown), /// it will produce a string value if the data is valid UTF-8, or a binary value otherwise. /// -/// There are also a few other methods to consume all the data of a [`Bytestream`]: +/// There are also a few other methods to consume all the data of a [`ByteStream`]: /// - [`drain`](ByteStream::drain): consumes all bytes and outputs nothing. /// - [`write_to`](ByteStream::write_to): writes all bytes to the given [`Write`] destination. /// - [`print`](ByteStream::print): a convenience wrapper around [`write_to`](ByteStream::write_to). @@ -182,7 +178,7 @@ impl From for Type { pub struct ByteStream { stream: ByteStreamSource, span: Span, - ctrlc: Option>, + signals: Signals, type_: ByteStreamType, known_size: Option, } @@ -192,13 +188,13 @@ impl ByteStream { pub fn new( stream: ByteStreamSource, span: Span, - interrupt: Option>, + signals: Signals, type_: ByteStreamType, ) -> Self { Self { stream, span, - ctrlc: interrupt, + signals, type_, known_size: None, } @@ -208,33 +204,33 @@ impl ByteStream { pub fn read( reader: impl Read + Send + 'static, span: Span, - interrupt: Option>, + signals: Signals, type_: ByteStreamType, ) -> Self { Self::new( ByteStreamSource::Read(Box::new(reader)), span, - interrupt, + signals, type_, ) } /// Create a [`ByteStream`] from a string. The type of the stream is always `String`. - pub fn read_string(string: String, span: Span, interrupt: Option>) -> Self { + pub fn read_string(string: String, span: Span, signals: Signals) -> Self { let len = string.len(); ByteStream::read( Cursor::new(string.into_bytes()), span, - interrupt, + signals, ByteStreamType::String, ) .with_known_size(Some(len as u64)) } /// Create a [`ByteStream`] from a byte vector. The type of the stream is always `Binary`. - pub fn read_binary(bytes: Vec, span: Span, interrupt: Option>) -> Self { + pub fn read_binary(bytes: Vec, span: Span, signals: Signals) -> Self { let len = bytes.len(); - ByteStream::read(Cursor::new(bytes), span, interrupt, ByteStreamType::Binary) + ByteStream::read(Cursor::new(bytes), span, signals, ByteStreamType::Binary) .with_known_size(Some(len as u64)) } @@ -242,11 +238,11 @@ impl ByteStream { /// /// The type is implicitly `Unknown`, as it's not typically known whether files will /// return text or binary. - pub fn file(file: File, span: Span, interrupt: Option>) -> Self { + pub fn file(file: File, span: Span, signals: Signals) -> Self { Self::new( ByteStreamSource::File(file), span, - interrupt, + signals, ByteStreamType::Unknown, ) } @@ -259,7 +255,7 @@ impl ByteStream { Self::new( ByteStreamSource::Child(Box::new(child)), span, - None, + Signals::empty(), ByteStreamType::Unknown, ) } @@ -271,14 +267,19 @@ impl ByteStream { pub fn stdin(span: Span) -> Result { let stdin = os_pipe::dup_stdin().err_span(span)?; let source = ByteStreamSource::File(convert_file(stdin)); - Ok(Self::new(source, span, None, ByteStreamType::Unknown)) + Ok(Self::new( + source, + span, + Signals::empty(), + ByteStreamType::Unknown, + )) } /// Create a [`ByteStream`] from a generator function that writes data to the given buffer /// when called, and returns `Ok(false)` on end of stream. pub fn from_fn( span: Span, - interrupt: Option>, + signals: Signals, type_: ByteStreamType, generator: impl FnMut(&mut Vec) -> Result + Send + 'static, ) -> Self { @@ -288,7 +289,7 @@ impl ByteStream { generator, }, span, - interrupt, + signals, type_, ) } @@ -301,12 +302,7 @@ impl ByteStream { /// Create a new [`ByteStream`] from an [`Iterator`] of bytes slices. /// /// The returned [`ByteStream`] will have a [`ByteStreamSource`] of `Read`. - pub fn from_iter( - iter: I, - span: Span, - interrupt: Option>, - type_: ByteStreamType, - ) -> Self + pub fn from_iter(iter: I, span: Span, signals: Signals, type_: ByteStreamType) -> Self where I: IntoIterator, I::IntoIter: Send + 'static, @@ -314,7 +310,7 @@ impl ByteStream { { let iter = iter.into_iter(); let cursor = Some(Cursor::new(I::Item::default())); - Self::read(ReadIterator { iter, cursor }, span, interrupt, type_) + Self::read(ReadIterator { iter, cursor }, span, signals, type_) } /// Create a new [`ByteStream`] from an [`Iterator`] of [`Result`] bytes slices. @@ -323,7 +319,7 @@ impl ByteStream { pub fn from_result_iter( iter: I, span: Span, - interrupt: Option>, + signals: Signals, type_: ByteStreamType, ) -> Self where @@ -333,7 +329,7 @@ impl ByteStream { { let iter = iter.into_iter(); let cursor = Some(Cursor::new(T::default())); - Self::read(ReadResultIterator { iter, cursor }, span, interrupt, type_) + Self::read(ReadResultIterator { iter, cursor }, span, signals, type_) } /// Set the known size, in number of bytes, of the [`ByteStream`]. @@ -357,6 +353,12 @@ impl ByteStream { self.span } + /// Changes the [`Span`] associated with the [`ByteStream`]. + pub fn with_span(mut self, span: Span) -> Self { + self.span = span; + self + } + /// Returns the [`ByteStreamType`] associated with the [`ByteStream`]. pub fn type_(&self) -> ByteStreamType { self.type_ @@ -378,7 +380,7 @@ impl ByteStream { Some(Reader { reader: BufReader::new(reader), span: self.span, - ctrlc: self.ctrlc, + signals: self.signals, }) } @@ -394,7 +396,7 @@ impl ByteStream { Some(Lines { reader: BufReader::new(reader), span: self.span, - ctrlc: self.ctrlc, + signals: self.signals, }) } @@ -415,7 +417,7 @@ impl ByteStream { /// then the stream is considered empty and `None` will be returned. pub fn chunks(self) -> Option { let reader = self.stream.reader()?; - Some(Chunks::new(reader, self.span, self.ctrlc, self.type_)) + Some(Chunks::new(reader, self.span, self.signals, self.type_)) } /// Convert the [`ByteStream`] into its inner [`ByteStreamSource`]. @@ -552,7 +554,7 @@ impl ByteStream { pub fn drain(self) -> Result, ShellError> { match self.stream { ByteStreamSource::Read(read) => { - copy_with_interrupt(read, io::sink(), self.span, self.ctrlc.as_deref())?; + copy_with_signals(read, io::sink(), self.span, &self.signals)?; Ok(None) } ByteStreamSource::File(_) => Ok(None), @@ -578,14 +580,14 @@ impl ByteStream { /// then the [`ExitStatus`] of the [`ChildProcess`] is returned. pub fn write_to(self, dest: impl Write) -> Result, ShellError> { let span = self.span; - let ctrlc = self.ctrlc.as_deref(); + let signals = &self.signals; match self.stream { ByteStreamSource::Read(read) => { - copy_with_interrupt(read, dest, span, ctrlc)?; + copy_with_signals(read, dest, span, signals)?; Ok(None) } ByteStreamSource::File(file) => { - copy_with_interrupt(file, dest, span, ctrlc)?; + copy_with_signals(file, dest, span, signals)?; Ok(None) } ByteStreamSource::Child(mut child) => { @@ -597,10 +599,10 @@ impl ByteStream { if let Some(stdout) = child.stdout.take() { match stdout { ChildPipe::Pipe(pipe) => { - copy_with_interrupt(pipe, dest, span, ctrlc)?; + copy_with_signals(pipe, dest, span, signals)?; } ChildPipe::Tee(tee) => { - copy_with_interrupt(tee, dest, span, ctrlc)?; + copy_with_signals(tee, dest, span, signals)?; } } } @@ -615,21 +617,21 @@ impl ByteStream { stderr: &OutDest, ) -> Result, ShellError> { let span = self.span; - let ctrlc = self.ctrlc.as_deref(); + let signals = &self.signals; match self.stream { ByteStreamSource::Read(read) => { - write_to_out_dest(read, stdout, true, span, ctrlc)?; + write_to_out_dest(read, stdout, true, span, signals)?; Ok(None) } ByteStreamSource::File(file) => { match stdout { OutDest::Pipe | OutDest::Capture | OutDest::Null => {} OutDest::Inherit => { - copy_with_interrupt(file, io::stdout(), span, ctrlc)?; + copy_with_signals(file, io::stdout(), span, signals)?; } OutDest::File(f) => { - copy_with_interrupt(file, f.as_ref(), span, ctrlc)?; + copy_with_signals(file, f.as_ref(), span, signals)?; } } Ok(None) @@ -643,20 +645,20 @@ impl ByteStream { .name("stderr writer".into()) .spawn_scoped(s, || match err { ChildPipe::Pipe(pipe) => { - write_to_out_dest(pipe, stderr, false, span, ctrlc) + write_to_out_dest(pipe, stderr, false, span, signals) } ChildPipe::Tee(tee) => { - write_to_out_dest(tee, stderr, false, span, ctrlc) + write_to_out_dest(tee, stderr, false, span, signals) } }) .err_span(span); match out { ChildPipe::Pipe(pipe) => { - write_to_out_dest(pipe, stdout, true, span, ctrlc) + write_to_out_dest(pipe, stdout, true, span, signals) } ChildPipe::Tee(tee) => { - write_to_out_dest(tee, stdout, true, span, ctrlc) + write_to_out_dest(tee, stdout, true, span, signals) } }?; @@ -672,11 +674,11 @@ impl ByteStream { } (Some(out), None) => { // single output stream, we can consume directly - write_to_out_dest(out, stdout, true, span, ctrlc)?; + write_to_out_dest(out, stdout, true, span, signals)?; } (None, Some(err)) => { // single output stream, we can consume directly - write_to_out_dest(err, stderr, false, span, ctrlc)?; + write_to_out_dest(err, stderr, false, span, signals)?; } (None, None) => {} } @@ -749,7 +751,7 @@ where pub struct Reader { reader: BufReader, span: Span, - ctrlc: Option>, + signals: Signals, } impl Reader { @@ -760,14 +762,8 @@ impl Reader { impl Read for Reader { fn read(&mut self, buf: &mut [u8]) -> io::Result { - if nu_utils::ctrl_c::was_pressed(&self.ctrlc) { - Err(ShellError::InterruptedByUser { - span: Some(self.span), - } - .into()) - } else { - self.reader.read(buf) - } + self.signals.check(self.span)?; + self.reader.read(buf) } } @@ -784,7 +780,7 @@ impl BufRead for Reader { pub struct Lines { reader: BufReader, span: Span, - ctrlc: Option>, + signals: Signals, } impl Lines { @@ -797,7 +793,7 @@ impl Iterator for Lines { type Item = Result; fn next(&mut self) -> Option { - if nu_utils::ctrl_c::was_pressed(&self.ctrlc) { + if self.signals.interrupted() { None } else { let mut buf = Vec::new(); @@ -826,23 +822,18 @@ pub struct Chunks { pos: u64, error: bool, span: Span, - ctrlc: Option>, + signals: Signals, type_: ByteStreamType, } impl Chunks { - fn new( - reader: SourceReader, - span: Span, - ctrlc: Option>, - type_: ByteStreamType, - ) -> Self { + fn new(reader: SourceReader, span: Span, signals: Signals, type_: ByteStreamType) -> Self { Self { reader: BufReader::new(reader), pos: 0, error: false, span, - ctrlc, + signals, type_, } } @@ -922,7 +913,7 @@ impl Iterator for Chunks { type Item = Result; fn next(&mut self) -> Option { - if self.error || nu_utils::ctrl_c::was_pressed(&self.ctrlc) { + if self.error || self.signals.interrupted() { None } else { match self.type_ { @@ -988,14 +979,14 @@ fn write_to_out_dest( stream: &OutDest, stdout: bool, span: Span, - ctrlc: Option<&AtomicBool>, + signals: &Signals, ) -> Result<(), ShellError> { match stream { OutDest::Pipe | OutDest::Capture => return Ok(()), - OutDest::Null => copy_with_interrupt(read, io::sink(), span, ctrlc), - OutDest::Inherit if stdout => copy_with_interrupt(read, io::stdout(), span, ctrlc), - OutDest::Inherit => copy_with_interrupt(read, io::stderr(), span, ctrlc), - OutDest::File(file) => copy_with_interrupt(read, file.as_ref(), span, ctrlc), + OutDest::Null => copy_with_signals(read, io::sink(), span, signals), + OutDest::Inherit if stdout => copy_with_signals(read, io::stdout(), span, signals), + OutDest::Inherit => copy_with_signals(read, io::stderr(), span, signals), + OutDest::File(file) => copy_with_signals(read, file.as_ref(), span, signals), }?; Ok(()) } @@ -1012,28 +1003,13 @@ pub(crate) fn convert_file>(file: impl Into) - const DEFAULT_BUF_SIZE: usize = 8192; -pub fn copy_with_interrupt( +pub fn copy_with_signals( mut reader: impl Read, mut writer: impl Write, span: Span, - interrupt: Option<&AtomicBool>, + signals: &Signals, ) -> Result { - if let Some(interrupt) = interrupt { - // #[cfg(any(target_os = "linux", target_os = "android"))] - // { - // return crate::sys::kernel_copy::copy_spec(reader, writer); - // } - match generic_copy(&mut reader, &mut writer, span, interrupt) { - Ok(len) => { - writer.flush().err_span(span)?; - Ok(len) - } - Err(err) => { - let _ = writer.flush(); - Err(err) - } - } - } else { + if signals.is_empty() { match io::copy(&mut reader, &mut writer) { Ok(n) => { writer.flush().err_span(span)?; @@ -1044,6 +1020,21 @@ pub fn copy_with_interrupt( Err(err.into_spanned(span).into()) } } + } else { + // #[cfg(any(target_os = "linux", target_os = "android"))] + // { + // return crate::sys::kernel_copy::copy_spec(reader, writer); + // } + match generic_copy(&mut reader, &mut writer, span, signals) { + Ok(len) => { + writer.flush().err_span(span)?; + Ok(len) + } + Err(err) => { + let _ = writer.flush(); + Err(err) + } + } } } @@ -1052,14 +1043,12 @@ fn generic_copy( mut reader: impl Read, mut writer: impl Write, span: Span, - interrupt: &AtomicBool, + signals: &Signals, ) -> Result { let buf = &mut [0; DEFAULT_BUF_SIZE]; let mut len = 0; loop { - if interrupt.load(Ordering::Relaxed) { - return Err(ShellError::InterruptedByUser { span: Some(span) }); - } + signals.check(span)?; let n = match reader.read(buf) { Ok(0) => break, Ok(n) => n, @@ -1134,7 +1123,7 @@ mod tests { Chunks::new( SourceReader::Read(Box::new(reader)), Span::test_data(), - None, + Signals::empty(), type_, ) } diff --git a/crates/nu-protocol/src/pipeline/list_stream.rs b/crates/nu-protocol/src/pipeline/list_stream.rs index 117c264219..55ae4bfee0 100644 --- a/crates/nu-protocol/src/pipeline/list_stream.rs +++ b/crates/nu-protocol/src/pipeline/list_stream.rs @@ -1,8 +1,9 @@ -use crate::{Config, PipelineData, ShellError, Span, Value}; -use std::{ - fmt::Debug, - sync::{atomic::AtomicBool, Arc}, -}; +//! Module managing the streaming of individual [`Value`]s as a [`ListStream`] between pipeline +//! elements +//! +//! For more general infos regarding our pipelining model refer to [`PipelineData`] +use crate::{Config, PipelineData, ShellError, Signals, Span, Value}; +use std::fmt::Debug; pub type ValueIterator = Box + Send + 'static>; @@ -21,10 +22,10 @@ impl ListStream { pub fn new( iter: impl Iterator + Send + 'static, span: Span, - interrupt: Option>, + signals: Signals, ) -> Self { Self { - stream: Box::new(Interrupt::new(iter, interrupt)), + stream: Box::new(InterruptIter::new(iter, signals)), span, } } @@ -34,11 +35,22 @@ impl ListStream { self.span } + /// Changes the [`Span`] associated with this [`ListStream`]. + pub fn with_span(mut self, span: Span) -> Self { + self.span = span; + self + } + /// Convert a [`ListStream`] into its inner [`Value`] `Iterator`. pub fn into_inner(self) -> ValueIterator { self.stream } + /// Take a single value from the inner `Iterator`, modifying the stream. + pub fn next_value(&mut self) -> Option { + self.stream.next() + } + /// Converts each value in a [`ListStream`] into a string and then joins the strings together /// using the given separator. pub fn into_string(self, separator: &str, config: &Config) -> String { @@ -69,10 +81,10 @@ impl ListStream { /// E.g., `take`, `filter`, `step_by`, and more. /// /// ``` - /// use nu_protocol::{ListStream, Span, Value}; + /// use nu_protocol::{ListStream, Signals, Span, Value}; /// /// let span = Span::unknown(); - /// let stream = ListStream::new(std::iter::repeat(Value::int(0, span)), span, None); + /// let stream = ListStream::new(std::iter::repeat(Value::int(0, span)), span, Signals::empty()); /// let new_stream = stream.modify(|iter| iter.take(100)); /// ``` pub fn modify(self, f: impl FnOnce(ValueIterator) -> I) -> Self @@ -128,22 +140,22 @@ impl Iterator for IntoIter { } } -struct Interrupt { +struct InterruptIter { iter: I, - interrupt: Option>, + signals: Signals, } -impl Interrupt { - fn new(iter: I, interrupt: Option>) -> Self { - Self { iter, interrupt } +impl InterruptIter { + fn new(iter: I, signals: Signals) -> Self { + Self { iter, signals } } } -impl Iterator for Interrupt { +impl Iterator for InterruptIter { type Item = ::Item; fn next(&mut self) -> Option { - if nu_utils::ctrl_c::was_pressed(&self.interrupt) { + if self.signals.interrupted() { None } else { self.iter.next() diff --git a/crates/nu-protocol/src/pipeline/mod.rs b/crates/nu-protocol/src/pipeline/mod.rs index a018a084ed..525806425d 100644 --- a/crates/nu-protocol/src/pipeline/mod.rs +++ b/crates/nu-protocol/src/pipeline/mod.rs @@ -3,9 +3,11 @@ pub mod list_stream; mod metadata; mod out_dest; mod pipeline_data; +mod signals; pub use byte_stream::*; pub use list_stream::*; pub use metadata::*; pub use out_dest::*; pub use pipeline_data::*; +pub use signals::*; diff --git a/crates/nu-protocol/src/pipeline/pipeline_data.rs b/crates/nu-protocol/src/pipeline/pipeline_data.rs index 03fbc64d21..0d1c0b3092 100644 --- a/crates/nu-protocol/src/pipeline/pipeline_data.rs +++ b/crates/nu-protocol/src/pipeline/pipeline_data.rs @@ -3,13 +3,10 @@ use crate::{ engine::{EngineState, Stack}, process::{ChildPipe, ChildProcess, ExitStatus}, ByteStream, ByteStreamType, Config, ErrSpan, ListStream, OutDest, PipelineMetadata, Range, - ShellError, Span, Type, Value, + ShellError, Signals, Span, Type, Value, }; use nu_utils::{stderr_write_all_and_flush, stdout_write_all_and_flush}; -use std::{ - io::{Cursor, Read, Write}, - sync::{atomic::AtomicBool, Arc}, -}; +use std::io::{Cursor, Read, Write}; const LINE_ENDING_PATTERN: &[char] = &['\r', '\n']; @@ -99,6 +96,24 @@ impl PipelineData { } } + /// Change the span of the [`PipelineData`]. + /// + /// Returns `Value(Nothing)` with the given span if it was [`PipelineData::Empty`]. + pub fn with_span(self, span: Span) -> Self { + match self { + PipelineData::Empty => PipelineData::Value(Value::nothing(span), None), + PipelineData::Value(value, metadata) => { + PipelineData::Value(value.with_span(span), metadata) + } + PipelineData::ListStream(stream, metadata) => { + PipelineData::ListStream(stream.with_span(span), metadata) + } + PipelineData::ByteStream(stream, metadata) => { + PipelineData::ByteStream(stream.with_span(span), metadata) + } + } + } + /// Get a type that is representative of the `PipelineData`. /// /// The type returned here makes no effort to collect a stream, so it may be a different type @@ -132,7 +147,8 @@ impl PipelineData { /// without consuming input and without writing anything. /// /// For the other [`OutDest`]s, the given `PipelineData` will be completely consumed - /// and `PipelineData::Empty` will be returned. + /// and `PipelineData::Empty` will be returned, unless the data is from an external stream, + /// in which case an external stream containing only that exit code will be returned. pub fn write_to_out_dests( self, engine_state: &EngineState, @@ -140,7 +156,11 @@ impl PipelineData { ) -> Result { match (self, stack.stdout()) { (PipelineData::ByteStream(stream, ..), stdout) => { - stream.write_to_out_dests(stdout, stack.stderr())?; + if let Some(exit_status) = stream.write_to_out_dests(stdout, stack.stderr())? { + return Ok(PipelineData::new_external_stream_with_only_exit_code( + exit_status.code(), + )); + } } (data, OutDest::Pipe | OutDest::Capture) => return Ok(data), (PipelineData::Empty, ..) => {} @@ -196,19 +216,23 @@ impl PipelineData { let val_span = value.span(); match value { Value::List { vals, .. } => PipelineIteratorInner::ListStream( - ListStream::new(vals.into_iter(), val_span, None).into_iter(), + ListStream::new(vals.into_iter(), val_span, Signals::empty()).into_iter(), ), Value::Binary { val, .. } => PipelineIteratorInner::ListStream( ListStream::new( val.into_iter().map(move |x| Value::int(x as i64, val_span)), val_span, - None, + Signals::empty(), ) .into_iter(), ), Value::Range { val, .. } => PipelineIteratorInner::ListStream( - ListStream::new(val.into_range_iter(val_span, None), val_span, None) - .into_iter(), + ListStream::new( + val.into_range_iter(val_span, Signals::empty()), + val_span, + Signals::empty(), + ) + .into_iter(), ), // Propagate errors by explicitly matching them before the final case. Value::Error { error, .. } => return Err(*error), @@ -301,11 +325,7 @@ impl PipelineData { } /// Simplified mapper to help with simple values also. For full iterator support use `.into_iter()` instead - pub fn map( - self, - mut f: F, - ctrlc: Option>, - ) -> Result + pub fn map(self, mut f: F, signals: &Signals) -> Result where Self: Sized, F: FnMut(Value) -> Value + 'static + Send, @@ -314,13 +334,14 @@ impl PipelineData { PipelineData::Value(value, metadata) => { let span = value.span(); let pipeline = match value { - Value::List { vals, .. } => { - vals.into_iter().map(f).into_pipeline_data(span, ctrlc) - } - Value::Range { val, .. } => val - .into_range_iter(span, ctrlc.clone()) + Value::List { vals, .. } => vals + .into_iter() .map(f) - .into_pipeline_data(span, ctrlc), + .into_pipeline_data(span, signals.clone()), + Value::Range { val, .. } => val + .into_range_iter(span, Signals::empty()) + .map(f) + .into_pipeline_data(span, signals.clone()), value => match f(value) { Value::Error { error, .. } => return Err(*error), v => v.into_pipeline_data(), @@ -339,11 +360,7 @@ impl PipelineData { } /// Simplified flatmapper. For full iterator support use `.into_iter()` instead - pub fn flat_map( - self, - mut f: F, - ctrlc: Option>, - ) -> Result + pub fn flat_map(self, mut f: F, signals: &Signals) -> Result where Self: Sized, U: IntoIterator + 'static, @@ -355,14 +372,17 @@ impl PipelineData { PipelineData::Value(value, metadata) => { let span = value.span(); let pipeline = match value { - Value::List { vals, .. } => { - vals.into_iter().flat_map(f).into_pipeline_data(span, ctrlc) - } - Value::Range { val, .. } => val - .into_range_iter(span, ctrlc.clone()) + Value::List { vals, .. } => vals + .into_iter() .flat_map(f) - .into_pipeline_data(span, ctrlc), - value => f(value).into_iter().into_pipeline_data(span, ctrlc), + .into_pipeline_data(span, signals.clone()), + Value::Range { val, .. } => val + .into_range_iter(span, Signals::empty()) + .flat_map(f) + .into_pipeline_data(span, signals.clone()), + value => f(value) + .into_iter() + .into_pipeline_data(span, signals.clone()), }; Ok(pipeline.set_metadata(metadata)) } @@ -380,18 +400,16 @@ impl PipelineData { } Err(err) => f(Value::binary(err.into_bytes(), span)), }; - Ok(iter - .into_iter() - .into_pipeline_data_with_metadata(span, ctrlc, metadata)) + Ok(iter.into_iter().into_pipeline_data_with_metadata( + span, + signals.clone(), + metadata, + )) } } } - pub fn filter( - self, - mut f: F, - ctrlc: Option>, - ) -> Result + pub fn filter(self, mut f: F, signals: &Signals) -> Result where Self: Sized, F: FnMut(&Value) -> bool + 'static + Send, @@ -401,13 +419,14 @@ impl PipelineData { PipelineData::Value(value, metadata) => { let span = value.span(); let pipeline = match value { - Value::List { vals, .. } => { - vals.into_iter().filter(f).into_pipeline_data(span, ctrlc) - } - Value::Range { val, .. } => val - .into_range_iter(span, ctrlc.clone()) + Value::List { vals, .. } => vals + .into_iter() .filter(f) - .into_pipeline_data(span, ctrlc), + .into_pipeline_data(span, signals.clone()), + Value::Range { val, .. } => val + .into_range_iter(span, Signals::empty()) + .filter(f) + .into_pipeline_data(span, signals.clone()), value => { if f(&value) { value.into_pipeline_data() @@ -453,7 +472,10 @@ impl PipelineData { /// Currently this will consume an external command to completion. pub fn check_external_failed(self) -> Result<(Self, bool), ShellError> { if let PipelineData::ByteStream(stream, metadata) = self { + // Preserve stream attributes let span = stream.span(); + let type_ = stream.type_(); + let known_size = stream.known_size(); match stream.into_child() { Ok(mut child) => { // Only check children without stdout. This means that nothing @@ -485,10 +507,12 @@ impl PipelineData { child.stderr = Some(ChildPipe::Tee(Box::new(Cursor::new(stderr)))); } child.set_exit_code(code); - let stream = ByteStream::child(child, span); + let stream = ByteStream::child(child, span).with_type(type_); Ok((PipelineData::ByteStream(stream, metadata), code != 0)) } else { - let stream = ByteStream::child(child, span); + let stream = ByteStream::child(child, span) + .with_type(type_) + .with_known_size(known_size); Ok((PipelineData::ByteStream(stream, metadata), false)) } } @@ -533,7 +557,8 @@ impl PipelineData { } } } - let range_values: Vec = val.into_range_iter(span, None).collect(); + let range_values: Vec = + val.into_range_iter(span, Signals::empty()).collect(); Ok(PipelineData::Value(Value::list(range_values, span), None)) } x => Ok(PipelineData::Value(x, metadata)), @@ -568,7 +593,7 @@ impl PipelineData { self.write_all_and_flush(engine_state, no_newline, to_stderr) } else { let call = Call::new(Span::new(0, 0)); - let table = command.run(engine_state, stack, &call, self)?; + let table = command.run(engine_state, stack, &(&call).into(), self)?; table.write_all_and_flush(engine_state, no_newline, to_stderr) } } else { @@ -610,6 +635,34 @@ impl PipelineData { Ok(None) } } + + pub fn unsupported_input_error( + self, + expected_type: impl Into, + 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 { @@ -633,10 +686,15 @@ impl IntoIterator for PipelineData { let span = value.span(); match value { Value::List { vals, .. } => PipelineIteratorInner::ListStream( - ListStream::new(vals.into_iter(), span, None).into_iter(), + ListStream::new(vals.into_iter(), span, Signals::empty()).into_iter(), ), Value::Range { val, .. } => PipelineIteratorInner::ListStream( - ListStream::new(val.into_range_iter(span, None), span, None).into_iter(), + ListStream::new( + val.into_range_iter(span, Signals::empty()), + span, + Signals::empty(), + ) + .into_iter(), ), x => PipelineIteratorInner::Value(x), } @@ -698,11 +756,11 @@ where } pub trait IntoInterruptiblePipelineData { - fn into_pipeline_data(self, span: Span, ctrlc: Option>) -> PipelineData; + fn into_pipeline_data(self, span: Span, signals: Signals) -> PipelineData; fn into_pipeline_data_with_metadata( self, span: Span, - ctrlc: Option>, + signals: Signals, metadata: impl Into>, ) -> PipelineData; } @@ -713,18 +771,18 @@ where I::IntoIter: Send + 'static, ::Item: Into, { - fn into_pipeline_data(self, span: Span, ctrlc: Option>) -> PipelineData { - ListStream::new(self.into_iter().map(Into::into), span, ctrlc).into() + fn into_pipeline_data(self, span: Span, signals: Signals) -> PipelineData { + ListStream::new(self.into_iter().map(Into::into), span, signals).into() } fn into_pipeline_data_with_metadata( self, span: Span, - ctrlc: Option>, + signals: Signals, metadata: impl Into>, ) -> PipelineData { PipelineData::ListStream( - ListStream::new(self.into_iter().map(Into::into), span, ctrlc), + ListStream::new(self.into_iter().map(Into::into), span, signals), metadata.into(), ) } diff --git a/crates/nu-protocol/src/pipeline/signals.rs b/crates/nu-protocol/src/pipeline/signals.rs new file mode 100644 index 0000000000..06ce583c82 --- /dev/null +++ b/crates/nu-protocol/src/pipeline/signals.rs @@ -0,0 +1,76 @@ +use crate::{ShellError, Span}; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +/// Used to check for signals to suspend or terminate the execution of Nushell code. +/// +/// For now, this struct only supports interruption (ctrl+c or SIGINT). +#[derive(Debug, Clone)] +pub struct Signals { + signals: Option>, +} + +impl Signals { + /// A [`Signals`] that is not hooked up to any event/signals source. + /// + /// So, this [`Signals`] will never be interrupted. + pub const EMPTY: Self = Signals { signals: None }; + + /// Create a new [`Signals`] with `ctrlc` as the interrupt source. + /// + /// Once `ctrlc` is set to `true`, [`check`](Self::check) will error + /// and [`interrupted`](Self::interrupted) will return `true`. + pub fn new(ctrlc: Arc) -> Self { + Self { + signals: Some(ctrlc), + } + } + + /// Create a [`Signals`] that is not hooked up to any event/signals source. + /// + /// So, the returned [`Signals`] will never be interrupted. + /// + /// This should only be used in test code, or if the stream/iterator being created + /// already has an underlying [`Signals`]. + pub const fn empty() -> Self { + Self::EMPTY + } + + /// Returns an `Err` if an interrupt has been triggered. + /// + /// Otherwise, returns `Ok`. + #[inline] + pub fn check(&self, span: Span) -> Result<(), ShellError> { + #[inline] + #[cold] + fn interrupt_error(span: Span) -> Result<(), ShellError> { + Err(ShellError::Interrupted { span }) + } + + if self.interrupted() { + interrupt_error(span) + } else { + Ok(()) + } + } + + /// Returns whether an interrupt has been triggered. + #[inline] + pub fn interrupted(&self) -> bool { + self.signals + .as_deref() + .is_some_and(|b| b.load(Ordering::Relaxed)) + } + + pub(crate) fn is_empty(&self) -> bool { + self.signals.is_none() + } + + pub(crate) fn reset(&self) { + if let Some(signals) = &self.signals { + signals.store(false, Ordering::Relaxed); + } + } +} diff --git a/crates/nu-protocol/src/plugin/registered.rs b/crates/nu-protocol/src/plugin/registered.rs index cbf78478e0..8749c24af9 100644 --- a/crates/nu-protocol/src/plugin/registered.rs +++ b/crates/nu-protocol/src/plugin/registered.rs @@ -26,7 +26,7 @@ pub trait RegisteredPlugin: Send + Sync { fn stop(&self) -> Result<(), ShellError>; /// Stop the plugin and reset any state so that we don't make any assumptions about the plugin - /// next time it launches. This is used on `register`. + /// next time it launches. This is used on `plugin add`. fn reset(&self) -> Result<(), ShellError>; /// Cast the pointer to an [`Any`] so that its concrete type can be retrieved. diff --git a/crates/nu-protocol/src/process/mod.rs b/crates/nu-protocol/src/process/mod.rs index 2fcf65f56e..6e10fdd620 100644 --- a/crates/nu-protocol/src/process/mod.rs +++ b/crates/nu-protocol/src/process/mod.rs @@ -1,3 +1,4 @@ +//! Handling of external subprocesses mod child; mod exit_status; diff --git a/crates/nu-protocol/src/signature.rs b/crates/nu-protocol/src/signature.rs index 70e94b35f1..f8b83063d7 100644 --- a/crates/nu-protocol/src/signature.rs +++ b/crates/nu-protocol/src/signature.rs @@ -1,11 +1,11 @@ use crate::{ - ast::Call, - engine::{Command, CommandType, EngineState, Stack}, + engine::{Call, Command, CommandType, EngineState, Stack}, BlockId, PipelineData, ShellError, SyntaxShape, Type, Value, VarId, }; use serde::{Deserialize, Serialize}; use std::fmt::Write; +/// The signature definition of a named flag that either accepts a value or acts as a toggle flag #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Flag { pub long: String, @@ -19,6 +19,7 @@ pub struct Flag { pub default_value: Option, } +/// The signature definition for a positional argument #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct PositionalArg { pub name: String, @@ -30,6 +31,7 @@ pub struct PositionalArg { pub default_value: Option, } +/// Command categories #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum Category { Bits, @@ -103,6 +105,7 @@ impl std::fmt::Display for Category { } } +/// Signature information of a [`Command`] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Signature { pub name: String, @@ -486,15 +489,14 @@ impl Signature { (name, s) } - pub fn get_positional(&self, position: usize) -> Option { + pub fn get_positional(&self, position: usize) -> Option<&PositionalArg> { if position < self.required_positional.len() { - self.required_positional.get(position).cloned() + self.required_positional.get(position) } else if position < (self.required_positional.len() + self.optional_positional.len()) { self.optional_positional .get(position - self.required_positional.len()) - .cloned() } else { - self.rest_positional.clone() + self.rest_positional.as_ref() } } diff --git a/crates/nu-protocol/src/span.rs b/crates/nu-protocol/src/span.rs index 0d280eaa9d..cb6d96421c 100644 --- a/crates/nu-protocol/src/span.rs +++ b/crates/nu-protocol/src/span.rs @@ -1,3 +1,4 @@ +//! [`Span`] to point to sections of source code and the [`Spanned`] wrapper type use crate::SpanId; use miette::SourceSpan; use serde::{Deserialize, Serialize}; @@ -53,6 +54,22 @@ impl Spanned { } } +impl Spanned> { + /// Move the `Result` to the outside, resulting in a spanned `Ok` or unspanned `Err`. + pub fn transpose(self) -> Result, E> { + match self { + Spanned { + item: Ok(item), + span, + } => Ok(Spanned { item, span }), + Spanned { + item: Err(err), + span: _, + } => Err(err), + } + } +} + /// Helper trait to create [`Spanned`] more ergonomically. pub trait IntoSpanned: Sized { /// Wrap items together with a span into [`Spanned`]. diff --git a/crates/nu-protocol/src/ty.rs b/crates/nu-protocol/src/ty.rs index d5ea8c1554..7c004d81de 100644 --- a/crates/nu-protocol/src/ty.rs +++ b/crates/nu-protocol/src/ty.rs @@ -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()) } diff --git a/crates/nu-protocol/src/value/mod.rs b/crates/nu-protocol/src/value/mod.rs index df030ad3e9..fc618d6341 100644 --- a/crates/nu-protocol/src/value/mod.rs +++ b/crates/nu-protocol/src/value/mod.rs @@ -23,7 +23,7 @@ use crate::{ ast::{Bits, Boolean, CellPath, Comparison, Math, Operator, PathMember}, did_you_mean, engine::{Closure, EngineState}, - Config, ShellError, Span, Type, + Config, ShellError, Signals, Span, Type, }; use chrono::{DateTime, Datelike, FixedOffset, Locale, TimeZone}; use chrono_humanize::HumanTime; @@ -1017,8 +1017,9 @@ impl Value { } } Value::Range { ref val, .. } => { - if let Some(item) = - val.into_range_iter(current.span(), None).nth(*count) + if let Some(item) = val + .into_range_iter(current.span(), Signals::empty()) + .nth(*count) { current = item; } else if *optional { diff --git a/crates/nu-protocol/src/value/range.rs b/crates/nu-protocol/src/value/range.rs index 5b1fa32ec7..6ec7ce16ad 100644 --- a/crates/nu-protocol/src/value/range.rs +++ b/crates/nu-protocol/src/value/range.rs @@ -1,25 +1,13 @@ //! A Range is an iterator over integers or floats. +use crate::{ast::RangeInclusion, ShellError, Signals, Span, Value}; use serde::{Deserialize, Serialize}; -use std::{ - cmp::Ordering, - fmt::Display, - sync::{atomic::AtomicBool, Arc}, -}; - -use crate::{ast::RangeInclusion, ShellError, Span, Value}; +use std::{cmp::Ordering, fmt::Display}; mod int_range { - use std::{ - cmp::Ordering, - fmt::Display, - ops::Bound, - sync::{atomic::AtomicBool, Arc}, - }; - + use crate::{ast::RangeInclusion, ShellError, Signals, Span, Value}; use serde::{Deserialize, Serialize}; - - use crate::{ast::RangeInclusion, ShellError, Span, Value}; + use std::{cmp::Ordering, fmt::Display, ops::Bound}; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct IntRange { @@ -123,12 +111,12 @@ mod int_range { } } - pub fn into_range_iter(self, ctrlc: Option>) -> Iter { + pub fn into_range_iter(self, signals: Signals) -> Iter { Iter { current: Some(self.start), step: self.step, end: self.end, - ctrlc, + signals, } } } @@ -202,7 +190,7 @@ mod int_range { current: Option, step: i64, end: Bound, - ctrlc: Option>, + signals: Signals, } impl Iterator for Iter { @@ -218,7 +206,7 @@ mod int_range { (_, Bound::Unbounded) => true, // will stop once integer overflows }; - if not_end && !nu_utils::ctrl_c::was_pressed(&self.ctrlc) { + if not_end && !self.signals.interrupted() { self.current = current.checked_add(self.step); Some(current) } else { @@ -233,16 +221,9 @@ mod int_range { } mod float_range { - use std::{ - cmp::Ordering, - fmt::Display, - ops::Bound, - sync::{atomic::AtomicBool, Arc}, - }; - + use crate::{ast::RangeInclusion, IntRange, Range, ShellError, Signals, Span, Value}; use serde::{Deserialize, Serialize}; - - use crate::{ast::RangeInclusion, IntRange, Range, ShellError, Span, Value}; + use std::{cmp::Ordering, fmt::Display, ops::Bound}; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] pub struct FloatRange { @@ -365,13 +346,13 @@ mod float_range { } } - pub fn into_range_iter(self, ctrlc: Option>) -> Iter { + pub fn into_range_iter(self, signals: Signals) -> Iter { Iter { start: self.start, step: self.step, end: self.end, iter: Some(0), - ctrlc, + signals, } } } @@ -477,7 +458,7 @@ mod float_range { step: f64, end: Bound, iter: Option, - ctrlc: Option>, + signals: Signals, } impl Iterator for Iter { @@ -495,7 +476,7 @@ mod float_range { (_, Bound::Unbounded) => current.is_finite(), }; - if not_end && !nu_utils::ctrl_c::was_pressed(&self.ctrlc) { + if not_end && !self.signals.interrupted() { self.iter = iter.checked_add(1); Some(current) } else { @@ -549,10 +530,10 @@ impl Range { } } - pub fn into_range_iter(self, span: Span, ctrlc: Option>) -> Iter { + pub fn into_range_iter(self, span: Span, signals: Signals) -> Iter { match self { - Range::IntRange(range) => Iter::IntIter(range.into_range_iter(ctrlc), span), - Range::FloatRange(range) => Iter::FloatIter(range.into_range_iter(ctrlc), span), + Range::IntRange(range) => Iter::IntIter(range.into_range_iter(signals), span), + Range::FloatRange(range) => Iter::FloatIter(range.into_range_iter(signals), span), } } } diff --git a/crates/nu-protocol/src/value/record.rs b/crates/nu-protocol/src/value/record.rs index 8b61e61f7f..f4e963737f 100644 --- a/crates/nu-protocol/src/value/record.rs +++ b/crates/nu-protocol/src/value/record.rs @@ -1,3 +1,4 @@ +//! Our insertion ordered map-type [`Record`] use std::{iter::FusedIterator, ops::RangeBounds}; use crate::{ShellError, Span, Value}; diff --git a/crates/nu-protocol/tests/test_signature.rs b/crates/nu-protocol/tests/test_signature.rs index e22029bab9..8faf772c38 100644 --- a/crates/nu-protocol/tests/test_signature.rs +++ b/crates/nu-protocol/tests/test_signature.rs @@ -39,7 +39,7 @@ fn test_signature_chained() { assert_eq!( signature.get_positional(0), - Some(PositionalArg { + Some(&PositionalArg { name: "required".to_string(), desc: "required description".to_string(), shape: SyntaxShape::String, @@ -49,7 +49,7 @@ fn test_signature_chained() { ); assert_eq!( signature.get_positional(1), - Some(PositionalArg { + Some(&PositionalArg { name: "optional".to_string(), desc: "optional description".to_string(), shape: SyntaxShape::String, @@ -59,7 +59,7 @@ fn test_signature_chained() { ); assert_eq!( signature.get_positional(2), - Some(PositionalArg { + Some(&PositionalArg { name: "rest".to_string(), desc: "rest description".to_string(), shape: SyntaxShape::String, diff --git a/crates/nu-std/README.md b/crates/nu-std/README.md index d6e4d6e9f6..23bd684a50 100644 --- a/crates/nu-std/README.md +++ b/crates/nu-std/README.md @@ -7,7 +7,7 @@ The standard library is a pure-`nushell` collection of custom commands which provide interactive utilities and building blocks for users writing casual scripts or complex applications. To see what's here: -``` +```text > use std > scope commands | select name usage | where name =~ "std " #┬───────────name────────────┬──────────────────────usage────────────────────── diff --git a/crates/nu-std/src/lib.rs b/crates/nu-std/src/lib.rs index a20e3fc5da..ff6910e4ba 100644 --- a/crates/nu-std/src/lib.rs +++ b/crates/nu-std/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] use log::trace; use nu_engine::eval_block; use nu_parser::parse; diff --git a/crates/nu-std/tests/test_help.nu b/crates/nu-std/tests/test_help.nu index 1608950886..b82e5072ef 100644 --- a/crates/nu-std/tests/test_help.nu +++ b/crates/nu-std/tests/test_help.nu @@ -7,3 +7,8 @@ def show_help_on_commands [] { assert ("item not found" not-in $help_result) } +#[test] +def show_help_on_error_make [] { + let help_result = (help error make) + assert ("Error: nu::shell::eval_block_with_input" not-in $help_result) +} diff --git a/crates/nu-system/README.md b/crates/nu-system/README.md new file mode 100644 index 0000000000..d847bf4e0f --- /dev/null +++ b/crates/nu-system/README.md @@ -0,0 +1,7 @@ +Operating system specific bindings used by Nushell. + +Currently primarily wrappers around processes and ways to gather process info from the system + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-system/src/lib.rs b/crates/nu-system/src/lib.rs index 2fc97d2e7e..3633d62990 100644 --- a/crates/nu-system/src/lib.rs +++ b/crates/nu-system/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod foreground; #[cfg(target_os = "freebsd")] diff --git a/crates/nu-table/README.md b/crates/nu-table/README.md new file mode 100644 index 0000000000..945d515e62 --- /dev/null +++ b/crates/nu-table/README.md @@ -0,0 +1,7 @@ +The layout logic for Nushell's table viewer. + +See also the separate `table` command implementation + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-table/src/lib.rs b/crates/nu-table/src/lib.rs index 66cc2a3a82..182585f193 100644 --- a/crates/nu-table/src/lib.rs +++ b/crates/nu-table/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] mod table; mod table_theme; mod types; diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index bdaf7fb6c3..bc23e48248 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -12,13 +12,17 @@ use tabled::{ config::{AlignmentHorizontal, ColoredConfig, Entity, EntityMap, Position}, dimension::CompleteDimensionVecRecords, records::{ - vec_records::{CellInfo, VecRecords}, + vec_records::{Cell, CellInfo, VecRecords}, ExactRecords, PeekableRecords, Records, Resizable, }, }, settings::{ - formatting::AlignmentStrategy, object::Segment, peaker::Peaker, themes::ColumnNames, Color, - Modify, Padding, Settings, TableOption, Width, + formatting::AlignmentStrategy, + object::{Columns, Segment}, + peaker::Peaker, + themes::ColumnNames, + width::Truncate, + Color, Modify, Padding, Settings, TableOption, Width, }, Table, }; @@ -216,7 +220,7 @@ fn build_table( } let pad = indent.0 + indent.1; - let widths = maybe_truncate_columns(&mut data, &cfg.theme, termwidth, pad); + let widths = maybe_truncate_columns(&mut data, &cfg, termwidth, pad); if widths.is_empty() { return None; } @@ -251,7 +255,8 @@ fn draw_table( align_table(&mut table, alignments, with_index, with_header, with_footer); colorize_table(&mut table, styles, with_index, with_header, with_footer); - let width_ctrl = TableWidthCtrl::new(widths, cfg, termwidth); + let pad = indent.0 + indent.1; + let width_ctrl = TableWidthCtrl::new(widths, cfg, termwidth, pad); if with_header && border_header { set_border_head(&mut table, with_footer, width_ctrl); @@ -269,20 +274,51 @@ fn set_indent(table: &mut Table, left: usize, right: usize) { fn set_border_head(table: &mut Table, with_footer: bool, wctrl: TableWidthCtrl) { if with_footer { let count_rows = table.count_rows(); + let last_row_index = count_rows - 1; + + // note: funnily last and row must be equal at this point but we do not rely on it just in case. + + let mut first_row = GetRow(0, Vec::new()); + let mut head_settings = GetRowSettings(0, AlignmentHorizontal::Left, None); + let mut last_row = GetRow(last_row_index, Vec::new()); + + table.with(&mut first_row); + table.with(&mut head_settings); + table.with(&mut last_row); + table.with( Settings::default() .with(wctrl) .with(StripColorFromRow(0)) .with(StripColorFromRow(count_rows - 1)) - .with(HeaderMove((0, 0), true)) - .with(HeaderMove((count_rows - 1 - 1, count_rows - 1), false)), + .with(MoveRowNext::new(0, 0)) + .with(MoveRowPrev::new(last_row_index - 1, last_row_index)) + .with(SetLineHeaders::new( + 0, + first_row.1, + head_settings.1, + head_settings.2.clone(), + )) + .with(SetLineHeaders::new( + last_row_index - 1, + last_row.1, + head_settings.1, + head_settings.2, + )), ); } else { + let mut row = GetRow(0, Vec::new()); + let mut row_opts = GetRowSettings(0, AlignmentHorizontal::Left, None); + + table.with(&mut row); + table.with(&mut row_opts); + table.with( Settings::default() .with(wctrl) .with(StripColorFromRow(0)) - .with(HeaderMove((0, 0), true)), + .with(MoveRowNext::new(0, 0)) + .with(SetLineHeaders::new(0, row.1, row_opts.1, row_opts.2)), ); } } @@ -301,12 +337,18 @@ fn table_to_string(table: Table, termwidth: usize) -> Option { struct TableWidthCtrl { width: Vec, cfg: NuTableConfig, - max: usize, + width_max: usize, + pad: usize, } impl TableWidthCtrl { - fn new(width: Vec, cfg: NuTableConfig, max: usize) -> Self { - Self { width, cfg, max } + fn new(width: Vec, cfg: NuTableConfig, max: usize, pad: usize) -> Self { + Self { + width, + cfg, + width_max: max, + pad, + } } } @@ -319,24 +361,51 @@ impl TableOption, ColoredConfig> for ) { let total_width = get_total_width2(&self.width, cfg); - if total_width > self.max { - TableTrim { - max: self.max, - strategy: self.cfg.trim, - width: self.width, - } + if total_width > self.width_max { + let has_header = self.cfg.with_header && rec.count_rows() > 1; + let trim_as_head = has_header && self.cfg.header_on_border; + + TableTrim::new( + self.width, + self.width_max, + self.cfg.trim, + trim_as_head, + self.pad, + ) .change(rec, cfg, dim); - } else if self.cfg.expand && self.max > total_width { - Settings::new(SetDimensions(self.width), Width::increase(self.max)) + } else if self.cfg.expand && self.width_max > total_width { + Settings::new(SetDimensions(self.width), Width::increase(self.width_max)) .change(rec, cfg, dim) + } else { + SetDimensions(self.width).change(rec, cfg, dim); } } } struct TableTrim { width: Vec, + width_max: usize, strategy: TrimStrategy, - max: usize, + trim_as_head: bool, + pad: usize, +} + +impl TableTrim { + fn new( + width: Vec, + width_max: usize, + strategy: TrimStrategy, + trim_as_head: bool, + pad: usize, + ) -> Self { + Self { + width, + strategy, + pad, + width_max, + trim_as_head, + } + } } impl TableOption, ColoredConfig> for TableTrim { @@ -346,9 +415,16 @@ impl TableOption, ColoredConfig> for cfg: &mut ColoredConfig, dims: &mut CompleteDimensionVecRecords<'_>, ) { + // we already must have been estimated that it's safe to do. + // and all dims will be suffitient + if self.trim_as_head { + trim_as_header(recs, cfg, dims, self); + return; + } + match self.strategy { TrimStrategy::Wrap { try_to_keep_words } => { - let mut wrap = Width::wrap(self.max).priority::(); + let mut wrap = Width::wrap(self.width_max).priority::(); if try_to_keep_words { wrap = wrap.keep_words(); } @@ -356,7 +432,7 @@ impl TableOption, ColoredConfig> for Settings::new(SetDimensions(self.width), wrap).change(recs, cfg, dims); } TrimStrategy::Truncate { suffix } => { - let mut truncate = Width::truncate(self.max).priority::(); + let mut truncate = Width::truncate(self.width_max).priority::(); if let Some(suffix) = suffix { truncate = truncate.suffix(suffix).suffix_try_color(true); } @@ -367,6 +443,67 @@ impl TableOption, ColoredConfig> for } } +fn trim_as_header( + recs: &mut VecRecords>, + 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::>(); + 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, @@ -460,19 +597,25 @@ fn load_theme( fn maybe_truncate_columns( data: &mut NuRecords, - theme: &TableTheme, + cfg: &NuTableConfig, termwidth: usize, pad: usize, ) -> Vec { const TERMWIDTH_THRESHOLD: usize = 120; - let truncate = if termwidth > TERMWIDTH_THRESHOLD { + let preserve_content = termwidth > TERMWIDTH_THRESHOLD; + let has_header = cfg.with_header && data.count_rows() > 1; + let is_header_on_border = has_header && cfg.header_on_border; + + let truncate = if is_header_on_border { + truncate_columns_by_head + } else if preserve_content { truncate_columns_by_columns } else { truncate_columns_by_content }; - truncate(data, theme, pad, termwidth) + truncate(data, &cfg.theme, pad, termwidth) } // VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE. @@ -627,6 +770,83 @@ fn truncate_columns_by_columns( widths } +// VERSION where we are showing AS LITTLE COLUMNS AS POSSIBLE but WITH AS MUCH CONTENT AS POSSIBLE. +fn truncate_columns_by_head( + data: &mut NuRecords, + theme: &TableTheme, + pad: usize, + termwidth: usize, +) -> Vec { + const TRAILING_COLUMN_WIDTH: usize = 5; + + let config = get_config(theme, false, None); + let mut widths = build_width(&*data, pad); + let total_width = get_total_width2(&widths, &config); + if total_width <= termwidth { + return widths; + } + + if data.is_empty() { + return widths; + } + + let head = &data[0]; + + let borders = config.get_borders(); + let has_vertical = borders.has_vertical(); + + let mut width = borders.has_left() as usize + borders.has_right() as usize; + 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 + pad; + + if i > 0 { + width += has_vertical as usize; + } + + if width >= termwidth { + width -= column_header_width + (i > 0 && has_vertical) as usize + pad; + break; + } + + truncate_pos += 1; + } + + // we don't need any truncation then (is it possible?) + if truncate_pos == head.len() { + return widths; + } + + if truncate_pos == 0 { + return vec![]; + } + + truncate_columns(data, truncate_pos); + widths.truncate(truncate_pos); + + // Append columns with a trailing column + + let min_width = width; + + let diff = termwidth - min_width; + let can_trailing_column_be_pushed = diff > TRAILING_COLUMN_WIDTH + has_vertical as usize; + + if !can_trailing_column_be_pushed { + if data.count_columns() == 1 { + return vec![]; + } + + truncate_columns(data, data.count_columns() - 1); + widths.pop(); + } + + push_empty_column(data); + widths.push(3 + pad); + + widths +} + /// The same as [`tabled::peaker::PriorityMax`] but prioritizes left columns first in case of equal width. #[derive(Debug, Default, Clone)] pub struct PriorityMax; @@ -714,9 +934,8 @@ impl TableOption, ColoredConfig> for SetDi } // it assumes no spans is used. +// todo: Could be replaced by Dimension impl usage fn build_width(records: &NuRecords, pad: usize) -> Vec { - use tabled::grid::records::vec_records::Cell; - let count_columns = records.count_columns(); let mut widths = vec![0; count_columns]; for columns in records.iter_rows() { @@ -729,50 +948,156 @@ fn build_width(records: &NuRecords, pad: usize) -> Vec { widths } -struct HeaderMove((usize, usize), bool); +struct GetRow(usize, Vec); -impl TableOption, ColoredConfig> for HeaderMove { +impl TableOption, ColoredConfig> for &mut GetRow { + fn change( + self, + recs: &mut NuRecords, + _: &mut ColoredConfig, + _: &mut CompleteDimensionVecRecords<'_>, + ) { + let row = self.0; + self.1 = recs[row].iter().map(|c| c.as_ref().to_owned()).collect(); + } +} + +struct GetRowSettings(usize, AlignmentHorizontal, Option); + +impl TableOption, ColoredConfig> + for &mut GetRowSettings +{ + fn change( + self, + _: &mut NuRecords, + cfg: &mut ColoredConfig, + _: &mut CompleteDimensionVecRecords<'_>, + ) { + let row = self.0; + self.1 = *cfg.get_alignment_horizontal(Entity::Row(row)); + self.2 = cfg + .get_colors() + .get_color((row, 0)) + .cloned() + .map(Color::from); + } +} + +struct SetLineHeaders { + line: usize, + columns: Vec, + alignment: AlignmentHorizontal, + color: Option, +} + +impl SetLineHeaders { + fn new( + line: usize, + columns: Vec, + alignment: AlignmentHorizontal, + color: Option, + ) -> Self { + Self { + line, + columns, + alignment, + color, + } + } +} + +impl TableOption, ColoredConfig> for SetLineHeaders { fn change( self, recs: &mut NuRecords, cfg: &mut ColoredConfig, dims: &mut CompleteDimensionVecRecords<'_>, ) { - let (row, line) = self.0; - if self.1 { - move_header_on_next(recs, cfg, dims, row, line); - } else { - move_header_on_prev(recs, cfg, dims, row, line); - } + let mut columns = self.columns; + match dims.get_widths() { + Some(widths) => { + columns = columns + .into_iter() + .zip(widths.iter().map(|w| w.checked_sub(2).unwrap_or(*w))) // exclude padding; which is generally 2 + .map(|(s, width)| Truncate::truncate_text(&s, width).into_owned()) + .collect(); + } + None => { + // we don't have widths cached; which means that NO width adjustments were done + // which means we are OK to leave columns as they are. + // + // but we actually always have to have widths at this point + } + }; + + set_column_names( + recs, + cfg, + dims, + columns, + self.line, + self.alignment, + self.color, + ) } } -fn move_header_on_next( - recs: &mut NuRecords, - cfg: &mut ColoredConfig, - dims: &mut CompleteDimensionVecRecords<'_>, +struct MoveRowNext { row: usize, line: usize, -) { +} + +impl MoveRowNext { + fn new(row: usize, line: usize) -> Self { + Self { row, line } + } +} + +struct MoveRowPrev { + row: usize, + line: usize, +} + +impl MoveRowPrev { + fn new(row: usize, line: usize) -> Self { + Self { row, line } + } +} + +impl TableOption, ColoredConfig> for MoveRowNext { + fn change( + self, + recs: &mut NuRecords, + cfg: &mut ColoredConfig, + _: &mut CompleteDimensionVecRecords<'_>, + ) { + row_shift_next(recs, cfg, self.row, self.line); + } +} + +impl TableOption, ColoredConfig> for MoveRowPrev { + fn change( + self, + recs: &mut NuRecords, + cfg: &mut ColoredConfig, + _: &mut CompleteDimensionVecRecords<'_>, + ) { + row_shift_prev(recs, cfg, self.row, self.line); + } +} + +fn row_shift_next(recs: &mut NuRecords, cfg: &mut ColoredConfig, row: usize, line: usize) { let count_rows = recs.count_rows(); let count_columns = recs.count_columns(); let has_line = cfg.has_horizontal(line, count_rows); let has_next_line = cfg.has_horizontal(line + 1, count_rows); - let align = *cfg.get_alignment_horizontal(Entity::Row(row)); - let color = cfg - .get_colors() - .get_color((row, 0)) - .cloned() - .map(Color::from); - if !has_line && !has_next_line { return; } if !has_line { - let head = remove_row(recs, row); + let _ = remove_row(recs, row); let count_rows = recs.count_rows(); - set_column_names(recs, cfg, dims, head, line, align, color); shift_alignments_down(cfg, row, count_rows, count_columns); shift_colors_down(cfg, row, count_rows, count_columns); shift_lines_up(cfg, count_rows, &[line + 1]); @@ -780,47 +1105,31 @@ fn move_header_on_next( return; } - let head = remove_row(recs, row); + let _ = remove_row(recs, row); let count_rows = recs.count_rows(); - set_column_names(recs, cfg, dims, head, line, align, color); shift_alignments_down(cfg, row, count_rows, count_columns); shift_colors_down(cfg, row, count_rows, count_columns); remove_lines(cfg, count_rows, &[line + 1]); shift_lines_up(cfg, count_rows, &[count_rows]); } -fn move_header_on_prev( - recs: &mut NuRecords, - cfg: &mut ColoredConfig, - dims: &mut CompleteDimensionVecRecords<'_>, - row: usize, - line: usize, -) { +fn row_shift_prev(recs: &mut NuRecords, cfg: &mut ColoredConfig, row: usize, line: usize) { let count_rows = recs.count_rows(); let count_columns = recs.count_columns(); let has_line = cfg.has_horizontal(line, count_rows); let has_prev_line = cfg.has_horizontal(line - 1, count_rows); - let align = *cfg.get_alignment_horizontal(Entity::Row(row)); - let color = cfg - .get_colors() - .get_color((row, 0)) - .cloned() - .map(Color::from); - if !has_line && !has_prev_line { return; } if !has_line { - let head = remove_row(recs, row); + let _ = remove_row(recs, row); // shift_lines_down(table, &[line - 1]); - set_column_names(recs, cfg, dims, head, line - 1, align, color); return; } - let head = remove_row(recs, row); + let _ = remove_row(recs, row); let count_rows = count_rows - 1; - set_column_names(recs, cfg, dims, head, line - 1, align, color); shift_alignments_down(cfg, row, count_rows, count_columns); shift_colors_down(cfg, row, count_rows, count_columns); remove_lines(cfg, count_rows, &[line - 1]); diff --git a/crates/nu-table/src/types/expanded.rs b/crates/nu-table/src/types/expanded.rs index 9472cc2846..668e5ea25c 100644 --- a/crates/nu-table/src/types/expanded.rs +++ b/crates/nu-table/src/types/expanded.rs @@ -102,9 +102,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { } for (row, item) in input.iter().enumerate() { - if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) { - return Ok(None); - } + cfg.opts.signals.check(cfg.opts.span)?; if let Value::Error { error, .. } = item { return Err(*error.clone()); @@ -143,9 +141,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { } for (row, item) in input.iter().enumerate() { - if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) { - return Ok(None); - } + cfg.opts.signals.check(cfg.opts.span)?; if let Value::Error { error, .. } = item { return Err(*error.clone()); @@ -230,9 +226,7 @@ fn expanded_table_list(input: &[Value], cfg: Cfg<'_>) -> TableResult { } for (row, item) in input.iter().enumerate() { - if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) { - return Ok(None); - } + cfg.opts.signals.check(cfg.opts.span)?; if let Value::Error { error, .. } = item { return Err(*error.clone()); @@ -353,9 +347,7 @@ fn expanded_table_kv(record: &Record, cfg: Cfg<'_>) -> StringResult { let mut data = Vec::with_capacity(record.len()); for (key, value) in record { - if nu_utils::ctrl_c::was_pressed(&cfg.opts.ctrlc) { - return Ok(None); - } + cfg.opts.signals.check(cfg.opts.span)?; let (value, is_expanded) = match expand_table_value(value, value_width, &cfg)? { Some(val) => val, diff --git a/crates/nu-table/src/types/general.rs b/crates/nu-table/src/types/general.rs index 4a6cd302e8..66048267d7 100644 --- a/crates/nu-table/src/types/general.rs +++ b/crates/nu-table/src/types/general.rs @@ -43,9 +43,7 @@ fn create_table(input: &[Value], opts: TableOpts<'_>) -> Result, fn kv_table(record: &Record, opts: TableOpts<'_>) -> StringResult { let mut data = vec![Vec::with_capacity(2); record.len()]; for ((column, value), row) in record.iter().zip(data.iter_mut()) { - if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) { - return Ok(None); - } + opts.signals.check(opts.span)?; let value = nu_value_to_string_colored(value, opts.config, opts.style_computer); @@ -123,9 +121,7 @@ fn to_table_with_header( } for (row, item) in input.iter().enumerate() { - if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) { - return Ok(None); - } + opts.signals.check(opts.span)?; if let Value::Error { error, .. } = item { return Err(*error.clone()); @@ -158,9 +154,7 @@ fn to_table_with_no_header( table.set_index_style(get_index_style(opts.style_computer)); for (row, item) in input.iter().enumerate() { - if nu_utils::ctrl_c::was_pressed(&opts.ctrlc) { - return Ok(None); - } + opts.signals.check(opts.span)?; if let Value::Error { error, .. } = item { return Err(*error.clone()); diff --git a/crates/nu-table/src/types/mod.rs b/crates/nu-table/src/types/mod.rs index 10289c3b81..163e4b0c81 100644 --- a/crates/nu-table/src/types/mod.rs +++ b/crates/nu-table/src/types/mod.rs @@ -8,8 +8,7 @@ pub use general::JustTable; use crate::{common::INDEX_COLUMN_NAME, NuTable}; use nu_color_config::StyleComputer; -use nu_protocol::{Config, Span, TableIndexMode, TableMode}; -use std::sync::{atomic::AtomicBool, Arc}; +use nu_protocol::{Config, Signals, Span, TableIndexMode, TableMode}; pub struct TableOutput { pub table: NuTable, @@ -29,7 +28,7 @@ impl TableOutput { #[derive(Debug, Clone)] pub struct TableOpts<'a> { - ctrlc: Option>, + signals: &'a Signals, config: &'a Config, style_computer: &'a StyleComputer<'a>, span: Span, @@ -45,7 +44,7 @@ impl<'a> TableOpts<'a> { pub fn new( config: &'a Config, style_computer: &'a StyleComputer<'a>, - ctrlc: Option>, + signals: &'a Signals, span: Span, width: usize, indent: (usize, usize), @@ -54,7 +53,7 @@ impl<'a> TableOpts<'a> { index_remove: bool, ) -> Self { Self { - ctrlc, + signals, config, style_computer, span, diff --git a/crates/nu-term-grid/README.md b/crates/nu-term-grid/README.md new file mode 100644 index 0000000000..5bdd32077d --- /dev/null +++ b/crates/nu-term-grid/README.md @@ -0,0 +1,5 @@ +Implementation of the layout engine for the `grid` command. + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-term-grid/src/lib.rs b/crates/nu-term-grid/src/lib.rs index 79b146593f..ca8ccd4e23 100644 --- a/crates/nu-term-grid/src/lib.rs +++ b/crates/nu-term-grid/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] pub mod grid; pub use grid::Grid; diff --git a/crates/nu-test-support/README.md b/crates/nu-test-support/README.md new file mode 100644 index 0000000000..f384b15067 --- /dev/null +++ b/crates/nu-test-support/README.md @@ -0,0 +1,7 @@ +This crate provides utilities for testing of Nushell + +Plugin authors should instead refer to `nu-plugin-test-support` + +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-test-support/src/fs.rs b/crates/nu-test-support/src/fs.rs index 00321e6590..230e5eafe3 100644 --- a/crates/nu-test-support/src/fs.rs +++ b/crates/nu-test-support/src/fs.rs @@ -1,137 +1,5 @@ -use std::fmt::Display; +use nu_path::{AbsolutePath, AbsolutePathBuf, Path}; use std::io::Read; -use std::ops::Div; -use std::path::{Path, PathBuf}; - -pub struct AbsoluteFile { - inner: PathBuf, -} - -impl AbsoluteFile { - pub fn new(path: impl AsRef) -> AbsoluteFile { - let path = path.as_ref(); - - if !path.is_absolute() { - panic!( - "AbsoluteFile::new must take an absolute path :: {}", - path.display() - ) - } else if path.is_dir() { - // At the moment, this is not an invariant, but rather a way to catch bugs - // in tests. - panic!( - "AbsoluteFile::new must not take a directory :: {}", - path.display() - ) - } else { - AbsoluteFile { - inner: path.to_path_buf(), - } - } - } - - pub fn dir(&self) -> AbsolutePath { - AbsolutePath::new(if let Some(parent) = self.inner.parent() { - parent - } else { - unreachable!("Internal error: could not get parent in dir") - }) - } -} - -impl From for PathBuf { - fn from(file: AbsoluteFile) -> Self { - file.inner - } -} - -pub struct AbsolutePath { - pub inner: PathBuf, -} - -impl AbsolutePath { - pub fn new(path: impl AsRef) -> AbsolutePath { - let path = path.as_ref(); - - if path.is_absolute() { - AbsolutePath { - inner: path.to_path_buf(), - } - } else { - panic!("AbsolutePath::new must take an absolute path") - } - } -} - -impl Div<&str> for &AbsolutePath { - type Output = AbsolutePath; - - fn div(self, rhs: &str) -> Self::Output { - let parts = rhs.split('/'); - let mut result = self.inner.clone(); - - for part in parts { - result = result.join(part); - } - - AbsolutePath::new(result) - } -} - -impl AsRef for AbsolutePath { - fn as_ref(&self) -> &Path { - self.inner.as_path() - } -} - -pub struct RelativePath { - inner: PathBuf, -} - -impl RelativePath { - pub fn new(path: impl Into) -> RelativePath { - let path = path.into(); - - if path.is_relative() { - RelativePath { inner: path } - } else { - panic!("RelativePath::new must take a relative path") - } - } -} - -impl> Div for &RelativePath { - type Output = RelativePath; - - fn div(self, rhs: T) -> Self::Output { - let parts = rhs.as_ref().split('/'); - let mut result = self.inner.clone(); - - for part in parts { - result = result.join(part); - } - - RelativePath::new(result) - } -} - -impl Display for AbsoluteFile { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.inner.display()) - } -} - -impl Display for AbsolutePath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.inner.display()) - } -} - -impl Display for RelativePath { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.inner.display()) - } -} pub enum Stub<'a> { FileWithContent(&'a str, &'a str), @@ -140,7 +8,7 @@ pub enum Stub<'a> { FileWithPermission(&'a str, bool), } -pub fn file_contents(full_path: impl AsRef) -> String { +pub fn file_contents(full_path: impl AsRef) -> String { let mut file = std::fs::File::open(full_path.as_ref()).expect("can not open file"); let mut contents = String::new(); file.read_to_string(&mut contents) @@ -148,7 +16,7 @@ pub fn file_contents(full_path: impl AsRef) -> String { contents } -pub fn file_contents_binary(full_path: impl AsRef) -> Vec { +pub fn file_contents_binary(full_path: impl AsRef) -> Vec { let mut file = std::fs::File::open(full_path.as_ref()).expect("can not open file"); let mut contents = Vec::new(); file.read_to_end(&mut contents).expect("can not read file"); @@ -167,56 +35,32 @@ pub fn line_ending() -> String { } } -pub fn delete_file_at(full_path: impl AsRef) { - let full_path = full_path.as_ref(); - - if full_path.exists() { - std::fs::remove_file(full_path).expect("can not delete file"); - } +pub fn files_exist_at(files: Vec>, path: impl AsRef) -> bool { + let path = path.as_ref(); + files.iter().all(|f| path.join(f.as_ref()).exists()) } -pub fn create_file_at(full_path: impl AsRef) -> Result<(), std::io::Error> { - let full_path = full_path.as_ref(); - - if full_path.parent().is_some() { - panic!("path exists"); - } - - std::fs::write(full_path, b"fake data") -} - -pub fn copy_file_to(source: &str, destination: &str) { - std::fs::copy(source, destination).expect("can not copy file"); -} - -pub fn files_exist_at(files: Vec>, path: impl AsRef) -> bool { - files.iter().all(|f| { - let mut loc = PathBuf::from(path.as_ref()); - loc.push(f); - loc.exists() - }) -} - -pub fn delete_directory_at(full_path: &str) { - std::fs::remove_dir_all(PathBuf::from(full_path)).expect("can not remove directory"); -} - -pub fn executable_path() -> PathBuf { +pub fn executable_path() -> AbsolutePathBuf { let mut path = binaries(); path.push("nu"); path } -pub fn installed_nu_path() -> PathBuf { +pub fn installed_nu_path() -> AbsolutePathBuf { let path = std::env::var_os(crate::NATIVE_PATH_ENV_VAR); - which::which_in("nu", path, ".").unwrap_or_else(|_| executable_path()) + if let Ok(path) = which::which_in("nu", path, ".") { + AbsolutePathBuf::try_from(path).expect("installed nushell path is absolute") + } else { + executable_path() + } } -pub fn root() -> PathBuf { +pub fn root() -> AbsolutePathBuf { let manifest_dir = if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { - PathBuf::from(manifest_dir) + AbsolutePathBuf::try_from(manifest_dir).expect("CARGO_MANIFEST_DIR is not an absolute path") } else { - PathBuf::from(env!("CARGO_MANIFEST_DIR")) + AbsolutePathBuf::try_from(env!("CARGO_MANIFEST_DIR")) + .expect("CARGO_MANIFEST_DIR is not an absolute path") }; let test_path = manifest_dir.join("Cargo.lock"); @@ -228,11 +72,11 @@ pub fn root() -> PathBuf { .expect("Couldn't find the debug binaries directory") .parent() .expect("Couldn't find the debug binaries directory") - .to_path_buf() + .into() } } -pub fn binaries() -> PathBuf { +pub fn binaries() -> AbsolutePathBuf { let build_target = std::env::var("CARGO_BUILD_TARGET").unwrap_or_default(); let profile = if let Ok(env_profile) = std::env::var("NUSHELL_CARGO_PROFILE") { @@ -244,27 +88,27 @@ pub fn binaries() -> PathBuf { }; std::env::var("CARGO_TARGET_DIR") - .map(PathBuf::from) - .unwrap_or_else(|_| root().join("target")) + .ok() + .and_then(|p| AbsolutePathBuf::try_from(p).ok()) + .unwrap_or_else(|| root().join("target")) .join(build_target) .join(profile) } -pub fn fixtures() -> PathBuf { - root().join("tests").join("fixtures") +pub fn fixtures() -> AbsolutePathBuf { + let mut path = root(); + path.push("tests"); + path.push("fixtures"); + path } -pub fn assets() -> PathBuf { - root().join("tests/assets") +pub fn assets() -> AbsolutePathBuf { + let mut path = root(); + path.push("tests"); + path.push("assets"); + path } -pub fn in_directory(str: impl AsRef) -> String { - let path = str.as_ref(); - let path = if path.is_relative() { - root().join(path) - } else { - path.to_path_buf() - }; - - path.display().to_string() +pub fn in_directory(path: impl AsRef) -> AbsolutePathBuf { + root().join(path) } diff --git a/crates/nu-test-support/src/lib.rs b/crates/nu-test-support/src/lib.rs index 5319830920..c6cadbcbd8 100644 --- a/crates/nu-test-support/src/lib.rs +++ b/crates/nu-test-support/src/lib.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("../README.md")] pub mod commands; pub mod fs; pub mod locale_override; diff --git a/crates/nu-test-support/src/macros.rs b/crates/nu-test-support/src/macros.rs index e83b4354da..6fd3a8c307 100644 --- a/crates/nu-test-support/src/macros.rs +++ b/crates/nu-test-support/src/macros.rs @@ -234,35 +234,29 @@ macro_rules! nu_with_plugins { } use crate::{Outcome, NATIVE_PATH_ENV_VAR}; -use std::ffi::OsStr; +use nu_path::{AbsolutePath, AbsolutePathBuf, Path}; use std::{ - path::Path, + ffi::OsStr, process::{Command, Stdio}, }; use tempfile::tempdir; #[derive(Default)] pub struct NuOpts { - pub cwd: Option, + pub cwd: Option, pub locale: Option, pub envs: Option>, pub collapse_output: Option, + pub use_ir: Option, } pub fn nu_run_test(opts: NuOpts, commands: impl AsRef, with_std: bool) -> Outcome { - let test_bins = crate::fs::binaries(); - - let cwd = std::env::current_dir().expect("Could not get current working directory."); - let test_bins = nu_path::canonicalize_with(&test_bins, cwd).unwrap_or_else(|e| { - panic!( - "Couldn't canonicalize dummy binaries path {}: {:?}", - test_bins.display(), - e - ) - }); + let test_bins = crate::fs::binaries() + .canonicalize() + .expect("Could not canonicalize dummy binaries path"); let mut paths = crate::shell_os_paths(); - paths.insert(0, test_bins); + paths.insert(0, test_bins.into()); let commands = commands.as_ref().lines().collect::>().join("; "); @@ -271,7 +265,7 @@ pub fn nu_run_test(opts: NuOpts, commands: impl AsRef, with_std: bool) -> O Err(_) => panic!("Couldn't join paths for PATH var."), }; - let target_cwd = opts.cwd.unwrap_or(".".to_string()); + let target_cwd = opts.cwd.unwrap_or_else(crate::fs::root); let locale = opts.locale.unwrap_or("en_US.UTF-8".to_string()); let executable_path = crate::fs::executable_path(); @@ -296,6 +290,15 @@ pub fn nu_run_test(opts: NuOpts, commands: impl AsRef, with_std: bool) -> O .stdout(Stdio::piped()) .stderr(Stdio::piped()); + // Explicitly set NU_USE_IR + if let Some(use_ir) = opts.use_ir { + if use_ir { + command.env("NU_USE_IR", "1"); + } else { + command.env_remove("NU_USE_IR"); + } + } + // Uncomment to debug the command being run: // println!("=== command\n{command:?}\n"); @@ -373,6 +376,7 @@ where if !executable_path.exists() { executable_path = crate::fs::installed_nu_path(); } + let process = match setup_command(&executable_path, &target_cwd) .envs(envs) .arg("--commands") @@ -439,7 +443,7 @@ fn collapse_output(out: &str) -> String { out.replace('\n', "") } -fn setup_command(executable_path: &Path, target_cwd: &str) -> Command { +fn setup_command(executable_path: &AbsolutePath, target_cwd: &AbsolutePath) -> Command { let mut command = Command::new(executable_path); command diff --git a/crates/nu-test-support/src/playground.rs b/crates/nu-test-support/src/playground.rs index 375f192983..4d33dcebe5 100644 --- a/crates/nu-test-support/src/playground.rs +++ b/crates/nu-test-support/src/playground.rs @@ -6,5 +6,5 @@ mod play; mod tests; pub use director::Director; -pub use nu_process::{Executable, NuProcess, NuResult, Outcome}; +pub use nu_process::{Executable, NuProcess, Outcome}; pub use play::{Dirs, EnvironmentVariable, Playground}; diff --git a/crates/nu-test-support/src/playground/director.rs b/crates/nu-test-support/src/playground/director.rs index 3d04155c25..a361342495 100644 --- a/crates/nu-test-support/src/playground/director.rs +++ b/crates/nu-test-support/src/playground/director.rs @@ -83,7 +83,7 @@ impl Director { } impl Executable for Director { - fn execute(&mut self) -> NuResult { + fn execute(&mut self) -> Result { use std::process::Stdio; match self.executable() { diff --git a/crates/nu-test-support/src/playground/nu_process.rs b/crates/nu-test-support/src/playground/nu_process.rs index 0031a173e8..2eeac703c6 100644 --- a/crates/nu-test-support/src/playground/nu_process.rs +++ b/crates/nu-test-support/src/playground/nu_process.rs @@ -1,12 +1,13 @@ use super::EnvironmentVariable; use crate::fs::{binaries as test_bins_path, executable_path}; -use std::ffi::{OsStr, OsString}; -use std::fmt; -use std::path::Path; -use std::process::{Command, ExitStatus}; +use std::{ + ffi::{OsStr, OsString}, + fmt, + process::{Command, ExitStatus}, +}; pub trait Executable { - fn execute(&mut self) -> NuResult; + fn execute(&mut self) -> Result; } #[derive(Clone, Debug)] @@ -24,8 +25,6 @@ impl Outcome { } } -pub type NuResult = Result; - #[derive(Debug)] pub struct NuError { pub desc: String, @@ -69,14 +68,10 @@ impl NuProcess { self } - pub fn get_cwd(&self) -> Option<&Path> { - self.cwd.as_ref().map(Path::new) - } - pub fn construct(&self) -> Command { let mut command = Command::new(executable_path()); - if let Some(cwd) = self.get_cwd() { + if let Some(cwd) = &self.cwd { command.current_dir(cwd); } diff --git a/crates/nu-test-support/src/playground/play.rs b/crates/nu-test-support/src/playground/play.rs index 902997200e..12295c2690 100644 --- a/crates/nu-test-support/src/playground/play.rs +++ b/crates/nu-test-support/src/playground/play.rs @@ -1,8 +1,9 @@ use super::Director; -use crate::fs; -use crate::fs::Stub; +use crate::fs::{self, Stub}; use nu_glob::glob; -use std::path::{Path, PathBuf}; +#[cfg(not(target_arch = "wasm32"))] +use nu_path::Path; +use nu_path::{AbsolutePath, AbsolutePathBuf}; use std::str; use tempfile::{tempdir, TempDir}; @@ -22,46 +23,46 @@ impl EnvironmentVariable { } pub struct Playground<'a> { - root: TempDir, + _root: TempDir, tests: String, - cwd: PathBuf, - config: Option, + cwd: AbsolutePathBuf, + config: Option, environment_vars: Vec, dirs: &'a Dirs, } -#[derive(Default, Clone)] +#[derive(Clone)] pub struct Dirs { - pub root: PathBuf, - pub test: PathBuf, - pub fixtures: PathBuf, + pub root: AbsolutePathBuf, + pub test: AbsolutePathBuf, + pub fixtures: AbsolutePathBuf, } impl Dirs { - pub fn formats(&self) -> PathBuf { + pub fn formats(&self) -> AbsolutePathBuf { self.fixtures.join("formats") } - pub fn root(&self) -> &Path { - self.root.as_path() + pub fn root(&self) -> &AbsolutePath { + &self.root } - pub fn test(&self) -> &Path { - self.test.as_path() + pub fn test(&self) -> &AbsolutePath { + &self.test } } impl<'a> Playground<'a> { - pub fn root(&self) -> &Path { - self.root.path() + pub fn root(&self) -> &AbsolutePath { + &self.dirs.root } - pub fn cwd(&self) -> &Path { + pub fn cwd(&self) -> &AbsolutePath { &self.cwd } pub fn back_to_playground(&mut self) -> &mut Self { - self.cwd = PathBuf::from(self.root()).join(self.tests.clone()); + self.cwd = self.root().join(&self.tests); self } @@ -70,68 +71,46 @@ impl<'a> Playground<'a> { } pub fn setup(topic: &str, block: impl FnOnce(Dirs, &mut Playground)) { - let root = tempdir().expect("Couldn't create a tempdir"); - let nuplay_dir = root.path().join(topic); + let temp = tempdir().expect("Could not create a tempdir"); - if PathBuf::from(&nuplay_dir).exists() { - std::fs::remove_dir_all(PathBuf::from(&nuplay_dir)).expect("can not remove directory"); + let root = AbsolutePathBuf::try_from(temp.path()) + .expect("Tempdir is not an absolute path") + .canonicalize() + .expect("Could not canonicalize tempdir"); + + let test = root.join(topic); + if test.exists() { + std::fs::remove_dir_all(&test).expect("Could not remove directory"); } + std::fs::create_dir(&test).expect("Could not create directory"); + let test = test + .canonicalize() + .expect("Could not canonicalize test path"); - std::fs::create_dir(PathBuf::from(&nuplay_dir)).expect("can not create directory"); - - let fixtures = fs::fixtures(); - let cwd = std::env::current_dir().expect("Could not get current working directory."); - let fixtures = nu_path::canonicalize_with(fixtures.clone(), cwd).unwrap_or_else(|e| { - panic!( - "Couldn't canonicalize fixtures path {}: {:?}", - fixtures.display(), - e - ) - }); - - let mut playground = Playground { - root, - tests: topic.to_string(), - cwd: nuplay_dir, - config: None, - environment_vars: Vec::default(), - dirs: &Dirs::default(), - }; - - let playground_root = playground.root.path(); - - let cwd = std::env::current_dir().expect("Could not get current working directory."); - let test = - nu_path::canonicalize_with(playground_root.join(topic), cwd).unwrap_or_else(|e| { - panic!( - "Couldn't canonicalize test path {}: {:?}", - playground_root.join(topic).display(), - e - ) - }); - - let cwd = std::env::current_dir().expect("Could not get current working directory."); - let root = nu_path::canonicalize_with(playground_root, cwd).unwrap_or_else(|e| { - panic!( - "Couldn't canonicalize tests root path {}: {:?}", - playground_root.display(), - e - ) - }); + let fixtures = fs::fixtures() + .canonicalize() + .expect("Could not canonicalize fixtures path"); let dirs = Dirs { - root, - test, - fixtures, + root: root.into(), + test: test.as_path().into(), + fixtures: fixtures.into(), }; - playground.dirs = &dirs; + let mut playground = Playground { + _root: temp, + tests: topic.to_string(), + cwd: test.into(), + config: None, + environment_vars: Vec::default(), + dirs: &dirs, + }; block(dirs.clone(), &mut playground); } - pub fn with_config(&mut self, source_file: impl AsRef) -> &mut Self { - self.config = Some(source_file.as_ref().to_path_buf()); + pub fn with_config(&mut self, source_file: AbsolutePathBuf) -> &mut Self { + self.config = Some(source_file); self } @@ -205,7 +184,6 @@ impl<'a> Playground<'a> { files .iter() .map(|f| { - let mut path = PathBuf::from(&self.cwd); let mut permission_set = false; let mut write_able = true; let (file_name, contents) = match *f { @@ -227,7 +205,7 @@ impl<'a> Playground<'a> { } }; - path.push(file_name); + let path = self.cwd.join(file_name); std::fs::write(&path, contents.as_bytes()).expect("can not create file"); if permission_set { @@ -252,7 +230,7 @@ impl<'a> Playground<'a> { self } - pub fn glob_vec(pattern: &str) -> Vec { + pub fn glob_vec(pattern: &str) -> Vec { let glob = glob(pattern); glob.expect("invalid pattern") diff --git a/crates/nu-test-support/src/playground/tests.rs b/crates/nu-test-support/src/playground/tests.rs index cc4daa597b..7c2be0237e 100644 --- a/crates/nu-test-support/src/playground/tests.rs +++ b/crates/nu-test-support/src/playground/tests.rs @@ -1,11 +1,4 @@ use crate::playground::Playground; -use std::path::{Path, PathBuf}; - -fn path(p: &Path) -> PathBuf { - let cwd = std::env::current_dir().expect("Could not get current working directory."); - nu_path::canonicalize_with(p, cwd) - .unwrap_or_else(|e| panic!("Couldn't canonicalize path {}: {:?}", p.display(), e)) -} #[test] fn current_working_directory_in_sandbox_directory_created() { @@ -13,7 +6,7 @@ fn current_working_directory_in_sandbox_directory_created() { let original_cwd = dirs.test(); nu.within("some_directory_within"); - assert_eq!(path(nu.cwd()), original_cwd.join("some_directory_within")); + assert_eq!(nu.cwd(), original_cwd.join("some_directory_within")); }) } @@ -25,6 +18,6 @@ fn current_working_directory_back_to_root_from_anywhere() { nu.within("some_directory_within"); nu.back_to_playground(); - assert_eq!(path(nu.cwd()), *original_cwd); + assert_eq!(nu.cwd(), original_cwd); }) } diff --git a/crates/nu-utils/README.md b/crates/nu-utils/README.md new file mode 100644 index 0000000000..de50a8ae93 --- /dev/null +++ b/crates/nu-utils/README.md @@ -0,0 +1,6 @@ +Collection of small utilities that are shared across Nushell crates. + +This crate should compile early in the crate graph and thus not depend on major dependencies or core-nushell crates itself. +## Internal Nushell crate + +This crate implements components of Nushell and is not designed to support plugin authors or other users directly. diff --git a/crates/nu-utils/src/ctrl_c.rs b/crates/nu-utils/src/ctrl_c.rs deleted file mode 100644 index 72719139ad..0000000000 --- a/crates/nu-utils/src/ctrl_c.rs +++ /dev/null @@ -1,13 +0,0 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; - -/// Returns true if Nu has received a SIGINT signal / ctrl+c event -pub fn was_pressed(ctrlc: &Option>) -> bool { - if let Some(ctrlc) = ctrlc { - ctrlc.load(Ordering::SeqCst) - } else { - false - } -} diff --git a/crates/nu-utils/src/lib.rs b/crates/nu-utils/src/lib.rs index f1a915290b..0c021f1dc9 100644 --- a/crates/nu-utils/src/lib.rs +++ b/crates/nu-utils/src/lib.rs @@ -1,5 +1,5 @@ +#![doc = include_str!("../README.md")] mod casing; -pub mod ctrl_c; mod deansi; pub mod emoji; pub mod filesystem; diff --git a/crates/nu_plugin_example/README.md b/crates/nu_plugin_example/README.md index d4bc6dbe29..98283e9958 100644 --- a/crates/nu_plugin_example/README.md +++ b/crates/nu_plugin_example/README.md @@ -7,10 +7,12 @@ in order to create a binary that can be registered into nushell declaration list This subcommand demonstrates sending configuration from the nushell `$env.config` to a plugin. -To register from after building `nushell` run: +To make use of the plugin after building `nushell` run: ```nushell -register target/debug/nu_plugin_example +plugin add target/debug/nu_plugin_example +# or then either restart your current nushell session or run: +plugin use target/debug/nu_plugin_example ``` The configuration for the plugin lives in `$env.config.plugins.example`: diff --git a/crates/nu_plugin_example/src/commands/collect_bytes.rs b/crates/nu_plugin_example/src/commands/collect_bytes.rs index 398a1de4b1..e716493785 100644 --- a/crates/nu_plugin_example/src/commands/collect_bytes.rs +++ b/crates/nu_plugin_example/src/commands/collect_bytes.rs @@ -1,7 +1,7 @@ use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use nu_protocol::{ - ByteStream, ByteStreamType, Category, Example, LabeledError, PipelineData, Signature, Type, - Value, + ByteStream, ByteStreamType, Category, Example, LabeledError, PipelineData, Signals, Signature, + Type, Value, }; use crate::ExamplePlugin; @@ -52,7 +52,7 @@ impl PluginCommand for CollectBytes { ByteStream::from_result_iter( input.into_iter().map(Value::coerce_into_binary), call.head, - None, + Signals::empty(), ByteStreamType::Unknown, ), None, diff --git a/crates/nu_plugin_example/src/commands/generate.rs b/crates/nu_plugin_example/src/commands/generate.rs index 9938518f4c..c039cc23d5 100644 --- a/crates/nu_plugin_example/src/commands/generate.rs +++ b/crates/nu_plugin_example/src/commands/generate.rs @@ -1,7 +1,7 @@ use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use nu_protocol::{ - Category, Example, IntoInterruptiblePipelineData, LabeledError, PipelineData, Signature, - SyntaxShape, Type, Value, + Category, Example, IntoInterruptiblePipelineData, LabeledError, PipelineData, Signals, + Signature, SyntaxShape, Type, Value, }; use crate::ExamplePlugin; @@ -85,7 +85,7 @@ impl PluginCommand for Generate { }) .map(|result| result.unwrap_or_else(|err| Value::error(err, head))) }) - .into_pipeline_data(head, None)) + .into_pipeline_data(head, Signals::empty())) } } diff --git a/crates/nu_plugin_example/src/commands/seq.rs b/crates/nu_plugin_example/src/commands/seq.rs index 5652b3ef9a..88254394c1 100644 --- a/crates/nu_plugin_example/src/commands/seq.rs +++ b/crates/nu_plugin_example/src/commands/seq.rs @@ -1,6 +1,7 @@ use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use nu_protocol::{ - Category, Example, LabeledError, ListStream, PipelineData, Signature, SyntaxShape, Type, Value, + Category, Example, LabeledError, ListStream, PipelineData, Signals, Signature, SyntaxShape, + Type, Value, }; use crate::ExamplePlugin; @@ -54,7 +55,7 @@ impl PluginCommand for Seq { let first: i64 = call.req(0)?; let last: i64 = call.req(1)?; let iter = (first..=last).map(move |number| Value::int(number, head)); - Ok(ListStream::new(iter, head, None).into()) + Ok(ListStream::new(iter, head, Signals::empty()).into()) } } diff --git a/crates/nu_plugin_formats/README.md b/crates/nu_plugin_formats/README.md index 2b5fc2d829..a7ee046f99 100644 --- a/crates/nu_plugin_formats/README.md +++ b/crates/nu_plugin_formats/README.md @@ -12,7 +12,7 @@ A nushell plugin to convert data to nushell tables. # Usage 1. compile the binary: `cargo build` -2. register plugin(assume it's compiled in ./target/debug/): +2. plugin add plugin(assume it's compiled in ./target/debug/): ``` -register ./target/debug/nu_plugin_formats +plugin add ./target/debug/nu_plugin_formats ``` diff --git a/crates/nu_plugin_gstat/README.md b/crates/nu_plugin_gstat/README.md index 88db33cbf8..7b9ab158ec 100644 --- a/crates/nu_plugin_gstat/README.md +++ b/crates/nu_plugin_gstat/README.md @@ -8,6 +8,6 @@ To install: > cargo install --path . ``` -To register (from inside Nushell): +To add the plugin (from inside Nushell): ``` -> register +> plugin add diff --git a/crates/nu_plugin_inc/README.md b/crates/nu_plugin_inc/README.md index e989aba0b9..d97443a189 100644 --- a/crates/nu_plugin_inc/README.md +++ b/crates/nu_plugin_inc/README.md @@ -8,6 +8,6 @@ To install: cargo install --path . ``` -To register (from inside Nushell): +To add the plugin (from inside Nushell): ``` -register +> plugin add diff --git a/crates/nu_plugin_polars/Cargo.toml b/crates/nu_plugin_polars/Cargo.toml index 9589c89525..99c88e47d6 100644 --- a/crates/nu_plugin_polars/Cargo.toml +++ b/crates/nu_plugin_polars/Cargo.toml @@ -33,13 +33,13 @@ serde = { version = "1.0", features = ["derive"] } sqlparser = { version = "0.47"} polars-io = { version = "0.41", features = ["avro"]} polars-arrow = { version = "0.41"} -polars-ops = { version = "0.41"} +polars-ops = { version = "0.41", features = ["pivot"]} polars-plan = { version = "0.41", features = ["regex"]} 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 = [ diff --git a/crates/nu_plugin_polars/src/dataframe/eager/mod.rs b/crates/nu_plugin_polars/src/dataframe/eager/mod.rs index 509c2b6dc4..6aa37d1ed1 100644 --- a/crates/nu_plugin_polars/src/dataframe/eager/mod.rs +++ b/crates/nu_plugin_polars/src/dataframe/eager/mod.rs @@ -10,6 +10,7 @@ mod first; mod get; mod last; mod open; +mod pivot; mod query_df; mod rename; mod sample; @@ -76,6 +77,7 @@ pub(crate) fn eager_commands() -> Vec &str { + "polars pivot" + } + + fn usage(&self) -> &str { + "Pivot a DataFrame from wide to long format." + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .required_named( + "on", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "column names for pivoting", + Some('o'), + ) + .required_named( + "index", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "column names for indexes", + Some('i'), + ) + .required_named( + "values", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "column names used as value columns", + Some('v'), + ) + .named( + "aggregate", + SyntaxShape::String, + "Aggregation to apply when pivoting. The following are supported: first, sum, min, max, mean, median, count, last", + Some('a'), + ) + .switch( + "sort", + "Sort columns", + Some('s'), + ) + .switch( + "streamable", + "Whether or not to use the polars streaming engine. Only valid for lazy dataframes", + Some('t'), + ) + .input_output_type( + Type::Custom("dataframe".into()), + Type::Custom("dataframe".into()), + ) + .category(Category::Custom("dataframe".into())) + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "[[name subject test_1 test_2]; [Cady maths 98 100] [Cady physics 99 100] [Karen maths 61 60] [Karen physics 58 60]] | polars into-df | polars pivot --on [subject] --index [name] --values [test_1]", + description: "Perform a pivot in order to show individuals test score by subject", + result: Some( + NuDataFrame::try_from_columns( + vec![ + Column::new( + "name".to_string(), + vec![Value::string("Cady", Span::test_data()), Value::string("Karen", Span::test_data())], + ), + Column::new( + "maths".to_string(), + vec![Value::int(98, Span::test_data()), Value::int(61, Span::test_data())], + ), + Column::new( + "physics".to_string(), + vec![Value::int(99, Span::test_data()), Value::int(58, Span::test_data())], + ), + ], + None, + ) + .expect("simple df for test should not fail") + .into_value(Span::unknown()) + ) + } + ] + } + + fn run( + &self, + plugin: &Self::Plugin, + engine: &EngineInterface, + call: &EvaluatedCall, + input: PipelineData, + ) -> Result { + match PolarsPluginObject::try_from_pipeline(plugin, input, call.head)? { + PolarsPluginObject::NuDataFrame(df) => command_eager(plugin, engine, call, df), + PolarsPluginObject::NuLazyFrame(lazy) => { + command_eager(plugin, engine, call, lazy.collect(call.head)?) + } + _ => Err(ShellError::GenericError { + error: "Must be a dataframe or lazy dataframe".into(), + msg: "".into(), + span: Some(call.head), + help: None, + inner: vec![], + }), + } + .map_err(LabeledError::from) + } +} + +fn command_eager( + plugin: &PolarsPlugin, + engine: &EngineInterface, + call: &EvaluatedCall, + df: NuDataFrame, +) -> Result { + let on_col: Vec = call.get_flag("on")?.expect("required value"); + let index_col: Vec = call.get_flag("index")?.expect("required value"); + let val_col: Vec = call.get_flag("values")?.expect("required value"); + + let (on_col_string, id_col_span) = convert_columns_string(on_col, call.head)?; + let (index_col_string, index_col_span) = convert_columns_string(index_col, call.head)?; + let (val_col_string, val_col_span) = convert_columns_string(val_col, call.head)?; + + check_column_datatypes(df.as_ref(), &on_col_string, id_col_span)?; + check_column_datatypes(df.as_ref(), &index_col_string, index_col_span)?; + check_column_datatypes(df.as_ref(), &val_col_string, val_col_span)?; + + let aggregate: Option = call + .get_flag::("aggregate")? + .map(pivot_agg_for_str) + .transpose()?; + + let sort = call.has_flag("sort")?; + + let polars_df = df.to_polars(); + // todo add other args + let pivoted = pivot( + &polars_df, + &on_col_string, + Some(&index_col_string), + Some(&val_col_string), + sort, + aggregate, + None, + ) + .map_err(|e| ShellError::GenericError { + error: format!("Pivot error: {e}"), + msg: "".into(), + span: Some(call.head), + help: None, + inner: vec![], + })?; + + let res = NuDataFrame::new(false, pivoted); + res.to_pipeline_data(plugin, engine, call.head) +} + +fn check_column_datatypes>( + df: &polars::prelude::DataFrame, + cols: &[T], + col_span: Span, +) -> Result<(), ShellError> { + if cols.is_empty() { + return Err(ShellError::GenericError { + error: "Merge error".into(), + msg: "empty column list".into(), + span: Some(col_span), + help: None, + inner: vec![], + }); + } + + // Checking if they are same type + if cols.len() > 1 { + for w in cols.windows(2) { + let l_series = df + .column(w[0].as_ref()) + .map_err(|e| ShellError::GenericError { + error: "Error selecting columns".into(), + msg: e.to_string(), + span: Some(col_span), + help: None, + inner: vec![], + })?; + + let r_series = df + .column(w[1].as_ref()) + .map_err(|e| ShellError::GenericError { + error: "Error selecting columns".into(), + msg: e.to_string(), + span: Some(col_span), + help: None, + inner: vec![], + })?; + + if l_series.dtype() != r_series.dtype() { + return Err(ShellError::GenericError { + error: "Merge error".into(), + msg: "found different column types in list".into(), + span: Some(col_span), + help: Some(format!( + "datatypes {} and {} are incompatible", + l_series.dtype(), + r_series.dtype() + )), + inner: vec![], + }); + } + } + } + + Ok(()) +} + +fn pivot_agg_for_str(agg: impl AsRef) -> Result { + match agg.as_ref() { + "first" => Ok(PivotAgg::First), + "sum" => Ok(PivotAgg::Sum), + "min" => Ok(PivotAgg::Min), + "max" => Ok(PivotAgg::Max), + "mean" => Ok(PivotAgg::Mean), + "median" => Ok(PivotAgg::Median), + "count" => Ok(PivotAgg::Count), + "last" => Ok(PivotAgg::Last), + s => Err(ShellError::GenericError { + error: format!("{s} is not a valid aggregation"), + msg: "".into(), + span: None, + help: Some( + "Use one of the following: first, sum, min, max, mean, median, count, last".into(), + ), + inner: vec![], + }), + } +} + +#[cfg(test)] +mod test { + use crate::test::test_polars_plugin_command; + + use super::*; + + #[test] + fn test_examples() -> Result<(), ShellError> { + test_polars_plugin_command(&PivotDF) + } +} diff --git a/crates/nu_plugin_polars/src/dataframe/eager/unpivot.rs b/crates/nu_plugin_polars/src/dataframe/eager/unpivot.rs index c535b54c0e..dafdc65ab4 100644 --- a/crates/nu_plugin_polars/src/dataframe/eager/unpivot.rs +++ b/crates/nu_plugin_polars/src/dataframe/eager/unpivot.rs @@ -30,16 +30,16 @@ impl PluginCommand for UnpivotDF { fn signature(&self) -> Signature { Signature::build(self.name()) .required_named( - "columns", + "index", SyntaxShape::Table(vec![]), "column names for unpivoting", - Some('c'), + Some('i'), ) .required_named( - "values", + "on", SyntaxShape::Table(vec![]), "column names used as value columns", - Some('v'), + Some('o'), ) .named( "variable-name", @@ -60,7 +60,7 @@ impl PluginCommand for UnpivotDF { .switch( "streamable", "Whether or not to use the polars streaming engine. Only valid for lazy dataframes", - Some('s'), + Some('t'), ) .category(Category::Custom("dataframe".into())) } @@ -70,7 +70,7 @@ impl PluginCommand for UnpivotDF { Example { description: "unpivot on an eager dataframe", example: - "[[a b c d]; [x 1 4 a] [y 2 5 b] [z 3 6 c]] | polars into-df | polars unpivot -c [b c] -v [a d]", + "[[a b c d]; [x 1 4 a] [y 2 5 b] [z 3 6 c]] | polars into-df | polars unpivot -i [b c] -o [a d]", result: Some( NuDataFrame::try_from_columns(vec![ Column::new( @@ -125,7 +125,7 @@ impl PluginCommand for UnpivotDF { Example { description: "unpivot on a lazy dataframe", example: - "[[a b c d]; [x 1 4 a] [y 2 5 b] [z 3 6 c]] | polars into-lazy | polars unpivot -c [b c] -v [a d] | polars collect", + "[[a b c d]; [x 1 4 a] [y 2 5 b] [z 3 6 c]] | polars into-lazy | polars unpivot -i [b c] -o [a d] | polars collect", result: Some( NuDataFrame::try_from_columns(vec![ Column::new( @@ -208,21 +208,31 @@ fn command_eager( call: &EvaluatedCall, df: NuDataFrame, ) -> Result { - let id_col: Vec = call.get_flag("columns")?.expect("required value"); - let val_col: Vec = call.get_flag("values")?.expect("required value"); + let index_col: Vec = call.get_flag("index")?.expect("required value"); + let on_col: Vec = call.get_flag("on")?.expect("required value"); let value_name: Option> = call.get_flag("value-name")?; let variable_name: Option> = call.get_flag("variable-name")?; - let (id_col_string, id_col_span) = convert_columns_string(id_col, call.head)?; - let (val_col_string, val_col_span) = convert_columns_string(val_col, call.head)?; + let (index_col_string, index_col_span) = convert_columns_string(index_col, call.head)?; + let (on_col_string, on_col_span) = convert_columns_string(on_col, call.head)?; - check_column_datatypes(df.as_ref(), &id_col_string, id_col_span)?; - check_column_datatypes(df.as_ref(), &val_col_string, val_col_span)?; + check_column_datatypes(df.as_ref(), &index_col_string, index_col_span)?; + check_column_datatypes(df.as_ref(), &on_col_string, on_col_span)?; - let mut res = df + let streamable = call.has_flag("streamable")?; + + let args = UnpivotArgs { + on: on_col_string.iter().map(Into::into).collect(), + index: index_col_string.iter().map(Into::into).collect(), + variable_name: variable_name.map(|s| s.item.into()), + value_name: value_name.map(|s| s.item.into()), + streamable, + }; + + let res = df .as_ref() - .unpivot(&val_col_string, &id_col_string) + .unpivot2(args) .map_err(|e| ShellError::GenericError { error: "Error calculating unpivot".into(), msg: e.to_string(), @@ -231,28 +241,6 @@ fn command_eager( inner: vec![], })?; - if let Some(name) = &variable_name { - res.rename("variable", &name.item) - .map_err(|e| ShellError::GenericError { - error: "Error renaming column".into(), - msg: e.to_string(), - span: Some(name.span), - help: None, - inner: vec![], - })?; - } - - if let Some(name) = &value_name { - res.rename("value", &name.item) - .map_err(|e| ShellError::GenericError { - error: "Error renaming column".into(), - msg: e.to_string(), - span: Some(name.span), - help: None, - inner: vec![], - })?; - } - let res = NuDataFrame::new(false, res); res.to_pipeline_data(plugin, engine, call.head) } @@ -263,11 +251,11 @@ fn command_lazy( call: &EvaluatedCall, df: NuLazyFrame, ) -> Result { - let id_col: Vec = call.get_flag("columns")?.expect("required value"); - let val_col: Vec = call.get_flag("values")?.expect("required value"); + let index_col: Vec = call.get_flag("index")?.expect("required value"); + let on_col: Vec = call.get_flag("on")?.expect("required value"); - let (id_col_string, _id_col_span) = convert_columns_string(id_col, call.head)?; - let (val_col_string, _val_col_span) = convert_columns_string(val_col, call.head)?; + let (index_col_string, _index_col_span) = convert_columns_string(index_col, call.head)?; + let (on_col_string, _on_col_span) = convert_columns_string(on_col, call.head)?; let value_name: Option = call.get_flag("value-name")?; let variable_name: Option = call.get_flag("variable-name")?; @@ -275,8 +263,8 @@ fn command_lazy( let streamable = call.has_flag("streamable")?; let unpivot_args = UnpivotArgs { - on: val_col_string.iter().map(Into::into).collect(), - index: id_col_string.iter().map(Into::into).collect(), + on: on_col_string.iter().map(Into::into).collect(), + index: index_col_string.iter().map(Into::into).collect(), value_name: value_name.map(Into::into), variable_name: variable_name.map(Into::into), streamable, diff --git a/crates/nu_plugin_python/nu_plugin_python_example.py b/crates/nu_plugin_python/nu_plugin_python_example.py index 34e54fea71..993e619366 100755 --- a/crates/nu_plugin_python/nu_plugin_python_example.py +++ b/crates/nu_plugin_python/nu_plugin_python_example.py @@ -7,7 +7,7 @@ # decode and encode information that is read and written to stdin and stdout # # To register the plugin use: -# register +# plugin add # # Be careful with the spans. Miette will crash if a span is outside the # size of the contents vector. We strongly suggest using the span found in the @@ -258,4 +258,4 @@ if __name__ == "__main__": if len(sys.argv) == 2 and sys.argv[1] == "--stdio": plugin() else: - print("Run me from inside nushell!") \ No newline at end of file + print("Run me from inside nushell!") diff --git a/crates/nu_plugin_query/README.md b/crates/nu_plugin_query/README.md index a3abd18082..621a004806 100644 --- a/crates/nu_plugin_query/README.md +++ b/crates/nu_plugin_query/README.md @@ -8,6 +8,6 @@ To install: > cargo install --path . ``` -To register (from inside Nushell): +To add the plugin (from inside Nushell): ``` -> register +> plugin add diff --git a/crates/nu_plugin_query/src/query_webpage_info.rs b/crates/nu_plugin_query/src/query_webpage_info.rs index e61387c8d1..f38f7241f5 100644 --- a/crates/nu_plugin_query/src/query_webpage_info.rs +++ b/crates/nu_plugin_query/src/query_webpage_info.rs @@ -158,9 +158,9 @@ impl<'a> serde::Serializer for &'a ValueSerializer { Ok(Value::nothing(self.span)) } - fn serialize_some(self, value: &T) -> Result + fn serialize_some(self, value: &T) -> Result where - T: Serialize, + T: Serialize + ?Sized, { value.serialize(self) } @@ -185,18 +185,18 @@ impl<'a> serde::Serializer for &'a ValueSerializer { Ok(Value::nothing(self.span)) } - fn serialize_newtype_struct( + fn serialize_newtype_struct( self, _name: &'static str, value: &T, ) -> Result where - T: Serialize, + T: Serialize + ?Sized, { value.serialize(self) } - fn serialize_newtype_variant( + fn serialize_newtype_variant( self, _name: &'static str, _variant_index: u32, @@ -204,7 +204,7 @@ impl<'a> serde::Serializer for &'a ValueSerializer { value: &T, ) -> Result where - T: Serialize, + T: Serialize + ?Sized, { value.serialize(self) } @@ -306,13 +306,9 @@ impl<'a> serde::ser::SerializeStruct for MapSerializer<'a> { type Ok = Value; type Error = Error; - fn serialize_field( - &mut self, - key: &'static str, - value: &T, - ) -> Result<(), Self::Error> + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> where - T: Serialize, + T: Serialize + ?Sized, { self.record .insert(key.to_owned(), value.serialize(self.serializer)?); @@ -328,9 +324,9 @@ impl<'a> serde::ser::SerializeMap for MapSerializer<'a> { type Ok = Value; type Error = Error; - fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> where - T: Serialize, + T: Serialize + ?Sized, { let value = serde_json::to_value(key).map_err(Error::new)?; let key = value @@ -341,9 +337,9 @@ impl<'a> serde::ser::SerializeMap for MapSerializer<'a> { Ok(()) } - fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> where - T: Serialize, + T: Serialize + ?Sized, { let key = self.current_key.take().ok_or(Error::new("key expected"))?; self.record.insert(key, value.serialize(self.serializer)?); @@ -359,13 +355,9 @@ impl<'a> serde::ser::SerializeStructVariant for MapSerializer<'a> { type Ok = Value; type Error = Error; - fn serialize_field( - &mut self, - key: &'static str, - value: &T, - ) -> Result<(), Self::Error> + fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Self::Error> where - T: Serialize, + T: Serialize + ?Sized, { self.record .insert(key.to_owned(), value.serialize(self.serializer)?); @@ -397,9 +389,9 @@ impl<'a> serde::ser::SerializeSeq for SeqSerializer<'a> { type Ok = Value; type Error = Error; - fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> where - T: Serialize, + T: Serialize + ?Sized, { self.seq.push(value.serialize(self.serializer)?); Ok(()) @@ -414,9 +406,9 @@ impl<'a> serde::ser::SerializeTuple for SeqSerializer<'a> { type Ok = Value; type Error = Error; - fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> where - T: Serialize, + T: Serialize + ?Sized, { self.seq.push(value.serialize(self.serializer)?); Ok(()) @@ -431,9 +423,9 @@ impl<'a> serde::ser::SerializeTupleStruct for SeqSerializer<'a> { type Ok = Value; type Error = Error; - fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> where - T: Serialize, + T: Serialize + ?Sized, { self.seq.push(value.serialize(self.serializer)?); Ok(()) @@ -448,9 +440,9 @@ impl<'a> serde::ser::SerializeTupleVariant for SeqSerializer<'a> { type Ok = Value; type Error = Error; - fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> where - T: Serialize, + T: Serialize + ?Sized, { self.seq.push(value.serialize(self.serializer)?); Ok(()) diff --git a/crates/nuon/README.md b/crates/nuon/README.md index d11aa8104c..158a9d2e7d 100644 --- a/crates/nuon/README.md +++ b/crates/nuon/README.md @@ -4,7 +4,7 @@ The NUON format is a superset of JSON designed to fit the feel of Nushell. Some of its extra features are - trailing commas are allowed - commas are optional in lists -- quotes are not required around keys or any _bare_ string that do not contain spaces +- quotes are not required around keys or any _bare_ string that do not contain spaces or special characters - comments are allowed, though not preserved when using [`from_nuon`] ## Example diff --git a/docker/Dockerfile b/docker/Dockerfile index 209b17d419..64c37f6230 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -24,7 +24,7 @@ RUN echo '/usr/bin/nu' >> /etc/shells \ && chmod +x /usr/bin/nu \ && chown -R nushell:nushell /home/nushell/.config/nushell \ && ls /usr/bin/nu_plugin* \ - | xargs -I{} su -c 'register {}' nushell \ + | xargs -I{} su -c 'plugin add {}' nushell \ && rm -rf /tmp/* USER nushell diff --git a/scripts/README.md b/scripts/README.md index 366ff76375..37388e74b5 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -23,5 +23,4 @@ | `coverage-local.sh` | x | x | x | | `install-all.ps1` | ? | x | ? | | `install-all.sh` | x | x | x | -| `register-plugins.nu` | x | x | x | | `uninstall-all.sh` | x | x | x | diff --git a/scripts/build.rs b/scripts/build.rs index beabba80dc..e366b4998c 100644 --- a/scripts/build.rs +++ b/scripts/build.rs @@ -14,5 +14,5 @@ fn main() { // Tango uses dynamic linking, to allow us to dynamically change between two bench suit at runtime. // This is currently not supported on non nightly rust, on windows. println!("cargo:rustc-link-arg-benches=-rdynamic"); - println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=scripts/build.rs"); } diff --git a/scripts/register-plugins.nu b/scripts/register-plugins.nu deleted file mode 100644 index 0447ffb0c1..0000000000 --- a/scripts/register-plugins.nu +++ /dev/null @@ -1,55 +0,0 @@ -use std log warning - -warning "./scripts/register-plugin.nu will be deprecated, please use the `toolkit plugin register` command instead" - -# are we on windows or not? -def windows? [] { - $nu.os-info.name == windows -} - -# filter out files that end in .d -def keep-plugin-executables [] { - if (windows?) { where name ends-with '.exe' } else { where name !~ '\.d' } -} - -# get list of all plugin files from their installed directory -let plugins = (ls ((which nu).path.0 | path dirname) | where name =~ nu_plugin | keep-plugin-executables) -for plugin in $plugins { - print -n $"registering ($plugin.name), " - nu -c $"register '($plugin.name)'" - print "success!" -} - -# print helpful message -print "\nplugins registered, please restart nushell" - -# Plugin Location -# https://github.com/nushell/nushell/tree/main/crates/nu_plugin_custom_values -# https://github.com/nushell/nushell/tree/main/crates/nu_plugin_example -# https://github.com/nushell/nushell/tree/main/crates/nu_plugin_gstat -# https://github.com/nushell/nushell/tree/main/crates/nu_plugin_inc -# https://github.com/nushell/nushell/tree/main/crates/nu_plugin_python -# https://github.com/nushell/nushell/tree/main/crates/nu_plugin_query -# https://github.com/fdncred/nu_plugin_from_parquet -# https://github.com/fdncred/nu_plugin_from_regex -# https://github.com/fdncred/nu_plugin_pnet -# https://github.com/JosephTLyons/nu_plugin_periodic_table -# https://github.com/Euphrasiologist/nu_plugin_bio -# https://github.com/realcundo/nu_plugin_dcm -# https://github.com/enerdgumen/nu_plugin_dotenv -# https://github.com/bluk/nu_plugin_from_bencode - -# Older plugins -# https://github.com/notryanb/nu_plugin_id3 -# https://github.com/notryanb/nu_plugin_weather -# https://github.com/tiffany352/nu-plugins/tree/main/from_nbt -# https://github.com/tiffany352/nu-plugins/tree/main/file_exists -# https://github.com/potan/nu_plugin_wifiscan -# https://github.com/autophagy/nu_plugin_from_dhall -# https://github.com/yanganto/nu_plugin_s3 -# https://github.com/lukasreuter/nu_plugin_unity -# https://github.com/filaretov/nu_plugin_path_temp -# https://github.com/cdecompilador/nu_plugin_bg -# https://github.com/aJuvan/nu_plugin_kubectl -# https://github.com/hedonihilist/nu_plugin_df - diff --git a/src/main.rs b/src/main.rs index 0e286df7da..422d6d8f93 100644 --- a/src/main.rs +++ b/src/main.rs @@ -106,7 +106,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() @@ -398,7 +398,7 @@ fn main() -> Result<()> { ); } - LanguageServer::initialize_stdio_connection()?.serve_requests(engine_state, ctrlc_bool)? + LanguageServer::initialize_stdio_connection()?.serve_requests(engine_state)? } else if let Some(commands) = parsed_nu_cli_args.commands.clone() { run_commands( &mut engine_state, diff --git a/src/run.rs b/src/run.rs index 6bb02451b9..10a5043b25 100644 --- a/src/run.rs +++ b/src/run.rs @@ -26,6 +26,10 @@ pub(crate) fn run_commands( let mut stack = Stack::new(); let start_time = std::time::Instant::now(); + if stack.has_env_var(engine_state, "NU_USE_IR") { + stack.use_ir = true; + } + // if the --no-config-file(-n) option is NOT passed, load the plugin file, // load the default env file or custom (depending on parsed_nu_cli_args.env_file), // and maybe a custom config file (depending on parsed_nu_cli_args.config_file) @@ -109,6 +113,10 @@ pub(crate) fn run_file( trace!("run_file"); let mut stack = Stack::new(); + if stack.has_env_var(engine_state, "NU_USE_IR") { + stack.use_ir = true; + } + // if the --no-config-file(-n) option is NOT passed, load the plugin file, // load the default env file or custom (depending on parsed_nu_cli_args.env_file), // and maybe a custom config file (depending on parsed_nu_cli_args.config_file) @@ -184,6 +192,10 @@ pub(crate) fn run_repl( let mut stack = Stack::new(); let start_time = std::time::Instant::now(); + if stack.has_env_var(engine_state, "NU_USE_IR") { + stack.use_ir = true; + } + if parsed_nu_cli_args.no_config_file.is_none() { setup_config( engine_state, diff --git a/src/signals.rs b/src/signals.rs index dfdc014993..375d29b79c 100644 --- a/src/signals.rs +++ b/src/signals.rs @@ -1,4 +1,7 @@ -use nu_protocol::engine::{ctrlc::Handlers, EngineState}; +use nu_protocol::{ + engine::{ctrlc::Handlers, EngineState}, + Signals, +}; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, @@ -9,6 +12,8 @@ pub(crate) fn ctrlc_protection( ctrlc_bool: &Arc, ctrlc_handlers: &Handlers, ) { + let interrupt = Arc::new(AtomicBool::new(false)); + engine_state.set_signals(Signals::new(interrupt.clone())); { let ctrlc_bool = ctrlc_bool.clone(); let ctrlc_handlers = ctrlc_handlers.clone(); diff --git a/tests/eval/mod.rs b/tests/eval/mod.rs index f3af92376a..e9afd401e9 100644 --- a/tests/eval/mod.rs +++ b/tests/eval/mod.rs @@ -1,7 +1,8 @@ -use nu_test_support::nu; +use nu_test_support::{nu, playground::Playground}; +use regex::Regex; #[test] -fn source_file_relative_to_file() { +fn record_with_redefined_key() { let actual = nu!("{x: 1, x: 2}"); assert!(actual.err.contains("redefined")); @@ -16,3 +17,455 @@ fn run_file_parse_error() { assert!(actual.err.contains("unknown type")); } + +enum ExpectedOut<'a> { + /// Equals a string exactly + Eq(&'a str), + /// Matches a regex + Matches(&'a str), + /// Produces an error (match regex) + Error(&'a str), + /// Drops a file that contains these contents + FileEq(&'a str, &'a str), +} +use self::ExpectedOut::*; + +fn test_eval(source: &str, expected_out: ExpectedOut) { + Playground::setup("test_eval_ast", |ast_dirs, _playground| { + Playground::setup("test_eval_ir", |ir_dirs, _playground| { + let actual_ast = nu!( + cwd: ast_dirs.test(), + use_ir: false, + source, + ); + let actual_ir = nu!( + cwd: ir_dirs.test(), + use_ir: true, + source, + ); + + match expected_out { + Eq(eq) => { + assert_eq!(actual_ast.out, eq); + assert_eq!(actual_ir.out, eq); + assert!(actual_ast.status.success()); + assert!(actual_ir.status.success()); + } + Matches(regex) => { + let compiled_regex = Regex::new(regex).expect("regex failed to compile"); + assert!( + compiled_regex.is_match(&actual_ast.out), + "AST eval out does not match: {}\n{}", + regex, + actual_ast.out + ); + assert!( + compiled_regex.is_match(&actual_ir.out), + "IR eval out does not match: {}\n{}", + regex, + actual_ir.out, + ); + assert!(actual_ast.status.success()); + assert!(actual_ir.status.success()); + } + Error(regex) => { + let compiled_regex = Regex::new(regex).expect("regex failed to compile"); + assert!( + compiled_regex.is_match(&actual_ast.err), + "AST eval err does not match: {}", + regex + ); + assert!( + compiled_regex.is_match(&actual_ir.err), + "IR eval err does not match: {}", + regex + ); + assert!(!actual_ast.status.success()); + assert!(!actual_ir.status.success()); + } + FileEq(path, contents) => { + let ast_contents = std::fs::read_to_string(ast_dirs.test().join(path)) + .expect("failed to read AST file"); + let ir_contents = std::fs::read_to_string(ir_dirs.test().join(path)) + .expect("failed to read IR file"); + assert_eq!(ast_contents.trim(), contents); + assert_eq!(ir_contents.trim(), contents); + assert!(actual_ast.status.success()); + assert!(actual_ir.status.success()); + } + } + assert_eq!(actual_ast.out, actual_ir.out); + }) + }); +} + +#[test] +fn literal_bool() { + test_eval("true", Eq("true")) +} + +#[test] +fn literal_int() { + test_eval("1", Eq("1")) +} + +#[test] +fn literal_float() { + test_eval("1.5", Eq("1.5")) +} + +#[test] +fn literal_filesize() { + test_eval("30MiB", Eq("30.0 MiB")) +} + +#[test] +fn literal_duration() { + test_eval("30ms", Eq("30ms")) +} + +#[test] +fn literal_binary() { + test_eval("0x[1f 2f f0]", Matches("Length.*1f.*2f.*f0")) +} + +#[test] +fn literal_closure() { + test_eval("{||}", Matches(" hello.txt", + FileEq("hello.txt", "hello"), + ) +} + +#[test] +fn let_variable() { + test_eval("let foo = 'test'; print $foo", Eq("test")) +} + +#[test] +fn let_variable_mutate_error() { + test_eval( + "let foo = 'test'; $foo = 'bar'; print $foo", + Error("immutable"), + ) +} + +#[test] +fn constant() { + test_eval("const foo = 1 + 2; print $foo", Eq("3")) +} + +#[test] +fn constant_assign_error() { + test_eval( + "const foo = 1 + 2; $foo = 4; print $foo", + Error("immutable"), + ) +} + +#[test] +fn mut_variable() { + test_eval("mut foo = 'test'; $foo = 'bar'; print $foo", Eq("bar")) +} + +#[test] +fn mut_variable_append_assign() { + test_eval( + "mut foo = 'test'; $foo ++= 'bar'; print $foo", + Eq("testbar"), + ) +} + +#[test] +fn bind_in_variable_to_input() { + test_eval("3 | (4 + $in)", Eq("7")) +} + +#[test] +fn if_true() { + test_eval("if true { 'foo' }", Eq("foo")) +} + +#[test] +fn if_false() { + test_eval("if false { 'foo' } | describe", Eq("nothing")) +} + +#[test] +fn if_else_true() { + test_eval("if 5 > 3 { 'foo' } else { 'bar' }", Eq("foo")) +} + +#[test] +fn if_else_false() { + test_eval("if 5 < 3 { 'foo' } else { 'bar' }", Eq("bar")) +} + +#[test] +fn match_empty_fallthrough() { + test_eval("match 42 { }; 'pass'", Eq("pass")) +} + +#[test] +fn match_value() { + test_eval("match 1 { 1 => 'pass', 2 => 'fail' }", Eq("pass")) +} + +#[test] +fn match_value_default() { + test_eval( + "match 3 { 1 => 'fail1', 2 => 'fail2', _ => 'pass' }", + Eq("pass"), + ) +} + +#[test] +fn match_value_fallthrough() { + test_eval("match 3 { 1 => 'fail1', 2 => 'fail2' }", Eq("")) +} + +#[test] +fn match_variable() { + test_eval( + "match 'pass' { $s => { print $s }, _ => { print 'fail' } }", + Eq("pass"), + ) +} + +#[test] +fn match_variable_in_list() { + test_eval("match [fail pass] { [$f, $p] => { print $p } }", Eq("pass")) +} + +#[test] +fn match_passthrough_input() { + test_eval( + "'yes' | match [pass fail] { [$p, ..] => (collect { |y| $y ++ $p }) }", + Eq("yespass"), + ) +} + +#[test] +fn while_mutate_var() { + test_eval("mut x = 2; while $x > 0 { print $x; $x -= 1 }", Eq("21")) +} + +#[test] +fn for_list() { + test_eval("for v in [1 2 3] { print ($v * 2) }", Eq(r"246")) +} + +#[test] +fn for_seq() { + test_eval("for v in (seq 1 4) { print ($v * 2) }", Eq("2468")) +} + +#[test] +fn early_return() { + test_eval("do { return 'foo'; 'bar' }", Eq("foo")) +} + +#[test] +fn early_return_from_if() { + test_eval("do { if true { return 'pass' }; 'fail' }", Eq("pass")) +} + +#[test] +fn early_return_from_loop() { + test_eval("do { loop { return 'pass' } }", Eq("pass")) +} + +#[test] +fn early_return_from_while() { + test_eval( + "do { let x = true; while $x { return 'pass' } }", + Eq("pass"), + ) +} + +#[test] +fn early_return_from_for() { + test_eval("do { for x in [pass fail] { return $x } }", Eq("pass")) +} + +#[test] +fn try_no_catch() { + test_eval("try { error make { msg: foo } }; 'pass'", Eq("pass")) +} + +#[test] +fn try_catch_no_var() { + test_eval( + "try { error make { msg: foo } } catch { 'pass' }", + Eq("pass"), + ) +} + +#[test] +fn try_catch_var() { + test_eval( + "try { error make { msg: foo } } catch { |err| $err.msg }", + Eq("foo"), + ) +} + +#[test] +fn try_catch_with_non_literal_closure_no_var() { + test_eval( + r#" + let error_handler = { || "pass" } + try { error make { msg: foobar } } catch $error_handler + "#, + Eq("pass"), + ) +} + +#[test] +fn try_catch_with_non_literal_closure() { + test_eval( + r#" + let error_handler = { |err| $err.msg } + try { error make { msg: foobar } } catch $error_handler + "#, + Eq("foobar"), + ) +} + +#[test] +fn row_condition() { + test_eval( + "[[a b]; [1 2] [3 4]] | where a < 3 | to nuon", + Eq("[[a, b]; [1, 2]]"), + ) +} + +#[test] +fn custom_command() { + test_eval( + r#" + def cmd [a: int, b: string = 'fail', ...c: string, --x: int] { $"($a)($b)($c)($x)" } + cmd 42 pass foo --x 30 + "#, + Eq("42pass[foo]30"), + ) +} diff --git a/tests/plugins/nu_plugin_nu_example.rs b/tests/plugins/nu_plugin_nu_example.rs index f178c64316..afeb487b60 100644 --- a/tests/plugins/nu_plugin_nu_example.rs +++ b/tests/plugins/nu_plugin_nu_example.rs @@ -4,7 +4,7 @@ use assert_cmd::Command; fn call() { // Add the `nu` binaries to the path env let path_env = std::env::join_paths( - std::iter::once(nu_test_support::fs::binaries()).chain( + std::iter::once(nu_test_support::fs::binaries().into()).chain( std::env::var_os(nu_test_support::NATIVE_PATH_ENV_VAR) .as_deref() .map(std::env::split_paths) diff --git a/tests/plugins/registry_file.rs b/tests/plugins/registry_file.rs index c0f0fa0724..2cb1473b6f 100644 --- a/tests/plugins/registry_file.rs +++ b/tests/plugins/registry_file.rs @@ -162,7 +162,7 @@ fn plugin_rm_then_restart_nu() { contents.upsert_plugin(PluginRegistryItem { name: "foo".into(), // this doesn't exist, but it should be ok - filename: dirs.test().join("nu_plugin_foo"), + filename: dirs.test().join("nu_plugin_foo").into(), shell: None, data: valid_plugin_item_data(), }); @@ -238,7 +238,7 @@ fn plugin_rm_from_custom_path() { contents.upsert_plugin(PluginRegistryItem { name: "foo".into(), // this doesn't exist, but it should be ok - filename: dirs.test().join("nu_plugin_foo"), + filename: dirs.test().join("nu_plugin_foo").into(), shell: None, data: valid_plugin_item_data(), }); @@ -286,7 +286,7 @@ fn plugin_rm_using_filename() { contents.upsert_plugin(PluginRegistryItem { name: "foo".into(), // this doesn't exist, but it should be ok - filename: dirs.test().join("nu_plugin_foo"), + filename: dirs.test().join("nu_plugin_foo").into(), shell: None, data: valid_plugin_item_data(), }); @@ -344,7 +344,7 @@ fn warning_on_invalid_plugin_item() { contents.upsert_plugin(PluginRegistryItem { name: "badtest".into(), // this doesn't exist, but it should be ok - filename: dirs.test().join("nu_plugin_badtest"), + filename: dirs.test().join("nu_plugin_badtest").into(), shell: None, data: PluginRegistryItemData::Invalid, }); diff --git a/tests/repl/test_config_path.rs b/tests/repl/test_config_path.rs index 534ac38a27..895b1bd8bf 100644 --- a/tests/repl/test_config_path.rs +++ b/tests/repl/test_config_path.rs @@ -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)) diff --git a/tests/repl/test_parser.rs b/tests/repl/test_parser.rs index 0efb1d087a..a7a25e1c67 100644 --- a/tests/repl/test_parser.rs +++ b/tests/repl/test_parser.rs @@ -572,39 +572,6 @@ fn unbalanced_parens2() -> TestResult { fail_test(r#"("("))"#, "unbalanced ( and )") } -#[test] -fn register_with_string_literal() -> TestResult { - fail_test(r#"register 'nu-plugin-math'"#, "File not found") -} - -#[test] -fn register_with_string_constant() -> TestResult { - let input = "\ -const file = 'nu-plugin-math' -register $file -"; - // should not fail with `not a constant` - fail_test(input, "File not found") -} - -#[test] -fn register_with_string_variable() -> TestResult { - let input = "\ -let file = 'nu-plugin-math' -register $file -"; - fail_test(input, "Value is not a parse-time constant") -} - -#[test] -fn register_with_non_string_constant() -> TestResult { - let input = "\ -const file = 6 -register $file -"; - fail_test(input, "expected string, found int") -} - #[test] fn plugin_use_with_string_literal() -> TestResult { fail_test( diff --git a/tests/shell/pipeline/commands/external.rs b/tests/shell/pipeline/commands/external.rs index d138b65766..a6efe2b6c9 100644 --- a/tests/shell/pipeline/commands/external.rs +++ b/tests/shell/pipeline/commands/external.rs @@ -149,17 +149,26 @@ fn command_substitution_wont_output_extra_newline() { assert_eq!(actual.out, "bar"); } -#[test] -fn basic_err_pipe_works() { - let actual = - nu!(r#"with-env { FOO: "bar" } { nu --testbin echo_env_stderr FOO e>| str length }"#); +#[rstest::rstest] +#[case("err>|")] +#[case("e>|")] +fn basic_err_pipe_works(#[case] redirection: &str) { + let actual = nu!( + r#"with-env { FOO: "bar" } { nu --testbin echo_env_stderr FOO {redirection} str length }"# + .replace("{redirection}", redirection) + ); assert_eq!(actual.out, "3"); } -#[test] -fn basic_outerr_pipe_works() { +#[rstest::rstest] +#[case("out+err>|")] +#[case("err+out>|")] +#[case("o+e>|")] +#[case("e+o>|")] +fn basic_outerr_pipe_works(#[case] redirection: &str) { let actual = nu!( - r#"with-env { FOO: "bar" } { nu --testbin echo_env_mixed out-err FOO FOO o+e>| str length }"# + r#"with-env { FOO: "bar" } { nu --testbin echo_env_mixed out-err FOO FOO {redirection} str length }"# + .replace("{redirection}", redirection) ); assert_eq!(actual.out, "7"); } diff --git a/tests/shell/pipeline/commands/internal.rs b/tests/shell/pipeline/commands/internal.rs index 84f0bf146f..59c9f31fce 100644 --- a/tests/shell/pipeline/commands/internal.rs +++ b/tests/shell/pipeline/commands/internal.rs @@ -1161,3 +1161,11 @@ fn command_not_found_error_shows_not_found_2() { && actual.err.contains("Did you mean `for`?") ); } + +#[test] +fn error_on_out_greater_pipe() { + let actual = nu!(r#""foo" o>| print"#); + assert!(actual + .err + .contains("Redirecting stdout to a pipe is the same as normal piping")) +}