From 1d3f6105f58aa6c09ddc607739f364df20820474 Mon Sep 17 00:00:00 2001 From: Steven Xu Date: Mon, 20 Mar 2023 15:05:22 +1100 Subject: [PATCH] feat: add a `command_not_found` hook (#8314) # Description Add a `command_not_found` function to `$env.config.hooks`. If this function outputs a string, then it's included in the `help`. An example hook on *Arch Linux*, to find packages that contain the binary, looks like: ```nushell let-env config = { # ... hooks: { command_not_found: { |cmd_name| ( try { let pkgs = (pkgfile --binaries --verbose $cmd_name) ( $"(ansi $env.config.color_config.shape_external)($cmd_name)(ansi reset) " + $"may be found in the following packages:\n($pkgs)" ) } catch { null } ) } # ... ``` # User-Facing Changes - Add a `command_not_found` function to `$env.config.hooks`. # Tests + Formatting Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass # After Submitting If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. --- Cargo.lock | 1 + crates/nu-cli/Cargo.toml | 2 +- crates/nu-cli/src/commands.rs | 2 +- crates/nu-cli/src/config_files.rs | 3 +- crates/nu-cli/src/eval_file.rs | 3 +- crates/nu-cli/src/lib.rs | 4 +- crates/nu-cli/src/prompt_update.rs | 2 +- crates/nu-cli/src/repl.rs | 347 +----------------- crates/nu-cli/src/util.rs | 43 +-- crates/nu-command/Cargo.toml | 1 + crates/nu-command/src/hook.rs | 341 +++++++++++++++++ crates/nu-command/src/lib.rs | 4 + crates/nu-command/src/system/run_external.rs | 28 +- crates/nu-command/src/util.rs | 40 ++ crates/nu-protocol/src/config.rs | 5 +- .../src/sample_config/default_config.nu | 3 + src/command.rs | 2 +- src/config_files.rs | 3 +- src/main.rs | 5 +- src/test_bins.rs | 2 +- 20 files changed, 447 insertions(+), 394 deletions(-) create mode 100644 crates/nu-command/src/hook.rs create mode 100644 crates/nu-command/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 10aeda0d4..69d9c5d1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2798,6 +2798,7 @@ dependencies = [ "log", "lscolors", "md-5", + "miette", "mime", "mime_guess", "mockito", diff --git a/crates/nu-cli/Cargo.toml b/crates/nu-cli/Cargo.toml index 19acc628b..2077f893b 100644 --- a/crates/nu-cli/Cargo.toml +++ b/crates/nu-cli/Cargo.toml @@ -12,10 +12,10 @@ bench = false [dev-dependencies] nu-test-support = { path = "../nu-test-support", version = "0.77.2" } -nu-command = { path = "../nu-command", version = "0.77.2" } rstest = { version = "0.16.0", default-features = false } [dependencies] +nu-command = { path = "../nu-command", version = "0.77.2" } nu-engine = { path = "../nu-engine", version = "0.77.2" } nu-path = { path = "../nu-path", version = "0.77.2" } nu-parser = { path = "../nu-parser", version = "0.77.2" } diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index 3b107716a..5d78b6491 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -1,6 +1,6 @@ -use crate::util::report_error; use log::info; use miette::Result; +use nu_command::util::report_error; use nu_engine::{convert_env_values, eval_block}; use nu_parser::parse; use nu_protocol::engine::Stack; diff --git a/crates/nu-cli/src/config_files.rs b/crates/nu-cli/src/config_files.rs index 6bf2ccf08..509014eb4 100644 --- a/crates/nu-cli/src/config_files.rs +++ b/crates/nu-cli/src/config_files.rs @@ -1,4 +1,5 @@ -use crate::util::{eval_source, report_error}; +use crate::util::eval_source; +use nu_command::util::report_error; #[cfg(feature = "plugin")] use nu_parser::ParseError; #[cfg(feature = "plugin")] diff --git a/crates/nu-cli/src/eval_file.rs b/crates/nu-cli/src/eval_file.rs index 1785c3a83..20dec87f3 100644 --- a/crates/nu-cli/src/eval_file.rs +++ b/crates/nu-cli/src/eval_file.rs @@ -1,7 +1,8 @@ -use crate::util::{eval_source, report_error}; +use crate::util::eval_source; use log::info; use log::trace; use miette::{IntoDiagnostic, Result}; +use nu_command::util::report_error; use nu_engine::{convert_env_values, current_dir}; use nu_parser::parse; use nu_path::canonicalize_with; diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index 754909626..97e042b32 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -18,13 +18,13 @@ pub use completions::{FileCompletion, NuCompleter}; pub use config_files::eval_config_contents; pub use eval_file::evaluate_file; pub use menus::{DescriptionMenu, NuHelpCompleter}; +pub use nu_command::util::{get_init_cwd, report_error, report_error_new}; pub use nu_highlight::NuHighlight; pub use print::Print; pub use prompt::NushellPrompt; pub use repl::evaluate_repl; -pub use repl::{eval_env_change_hook, eval_hook}; pub use syntax_highlight::NuHighlighter; -pub use util::{eval_source, gather_parent_env_vars, get_init_cwd, report_error, report_error_new}; +pub use util::{eval_source, gather_parent_env_vars}; pub use validation::NuValidator; #[cfg(feature = "plugin")] diff --git a/crates/nu-cli/src/prompt_update.rs b/crates/nu-cli/src/prompt_update.rs index ff8fef1e1..c8c21fcdb 100644 --- a/crates/nu-cli/src/prompt_update.rs +++ b/crates/nu-cli/src/prompt_update.rs @@ -1,6 +1,6 @@ -use crate::util::report_error; use crate::NushellPrompt; use log::trace; +use nu_command::util::report_error; use nu_engine::eval_subexpression; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index 0b234e633..f5d1ad810 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -2,21 +2,21 @@ use crate::{ completions::NuCompleter, prompt_update, reedline_config::{add_menus, create_keybindings, KeybindingsMode}, - util::{eval_source, get_guaranteed_cwd, report_error, report_error_new}, + util::eval_source, NuHighlighter, NuValidator, NushellPrompt, }; use crossterm::cursor::CursorShape; use log::{trace, warn}; use miette::{IntoDiagnostic, Result}; use nu_color_config::StyleComputer; -use nu_engine::{convert_env_values, eval_block, eval_block_with_early_return}; +use nu_command::hook::eval_hook; +use nu_command::util::{get_guaranteed_cwd, report_error, report_error_new}; +use nu_engine::{convert_env_values, eval_block}; use nu_parser::{lex, parse, trim_quotes_str}; use nu_protocol::{ - ast::PathMember, config::NuCursorShape, engine::{EngineState, Stack, StateWorkingSet}, - format_duration, BlockId, HistoryFileFormat, PipelineData, PositionalArg, ShellError, Span, - Spanned, Type, Value, VarId, + format_duration, HistoryFileFormat, PipelineData, ShellError, Span, Spanned, Value, }; use nu_utils::utils::perf; use reedline::{CursorConfig, DefaultHinter, EditCommand, Emacs, SqliteBackedHistory, Vi}; @@ -44,6 +44,7 @@ pub fn evaluate_repl( prerun_command: Option>, entire_start_time: Instant, ) -> Result<()> { + use nu_command::hook; use reedline::{FileBackedHistory, Reedline, Signal}; let use_color = engine_state.get_config().use_ansi_coloring; @@ -400,7 +401,7 @@ pub fn evaluate_repl( // fire the "env_change" hook let config = engine_state.get_config(); if let Err(error) = - eval_env_change_hook(config.hooks.env_change.clone(), engine_state, stack) + hook::eval_env_change_hook(config.hooks.env_change.clone(), engine_state, stack) { report_error_new(engine_state, &error) } @@ -822,340 +823,6 @@ pub fn get_command_finished_marker(stack: &Stack, engine_state: &EngineState) -> format!("\x1b]133;D;{}\x1b\\", exit_code.unwrap_or(0)) } -pub fn eval_env_change_hook( - env_change_hook: Option, - engine_state: &mut EngineState, - stack: &mut Stack, -) -> Result<(), ShellError> { - if let Some(hook) = env_change_hook { - match hook { - Value::Record { - cols: env_names, - vals: hook_values, - .. - } => { - for (env_name, hook_value) in env_names.iter().zip(hook_values.iter()) { - let before = engine_state - .previous_env_vars - .get(env_name) - .cloned() - .unwrap_or_default(); - - let after = stack - .get_env_var(engine_state, env_name) - .unwrap_or_default(); - - if before != after { - eval_hook( - engine_state, - stack, - None, - vec![("$before".into(), before), ("$after".into(), after.clone())], - hook_value, - )?; - - engine_state - .previous_env_vars - .insert(env_name.to_string(), after); - } - } - } - x => { - return Err(ShellError::TypeMismatch { - err_message: "record for the 'env_change' hook".to_string(), - span: x.span()?, - }); - } - } - } - - Ok(()) -} - -pub fn eval_hook( - engine_state: &mut EngineState, - stack: &mut Stack, - input: Option, - arguments: Vec<(String, Value)>, - value: &Value, -) -> Result { - let value_span = value.span()?; - - // Hooks can optionally be a record in this form: - // { - // condition: {|before, after| ... } # block that evaluates to true/false - // code: # block or a string - // } - // The condition block will be run to check whether the main hook (in `code`) should be run. - // If it returns true (the default if a condition block is not specified), the hook should be run. - let condition_path = PathMember::String { - val: "condition".to_string(), - span: value_span, - optional: false, - }; - let mut output = PipelineData::empty(); - - let code_path = PathMember::String { - val: "code".to_string(), - span: value_span, - optional: false, - }; - - match value { - Value::List { vals, .. } => { - for val in vals { - eval_hook(engine_state, stack, None, arguments.clone(), val)?; - } - } - Value::Record { .. } => { - let do_run_hook = - if let Ok(condition) = value.clone().follow_cell_path(&[condition_path], false) { - match condition { - Value::Block { - val: block_id, - span: block_span, - .. - } - | Value::Closure { - val: block_id, - span: block_span, - .. - } => { - match run_hook_block( - engine_state, - stack, - block_id, - None, - arguments.clone(), - block_span, - ) { - Ok(pipeline_data) => { - if let PipelineData::Value(Value::Bool { val, .. }, ..) = - pipeline_data - { - val - } else { - return Err(ShellError::UnsupportedConfigValue( - "boolean output".to_string(), - "other PipelineData variant".to_string(), - block_span, - )); - } - } - Err(err) => { - return Err(err); - } - } - } - other => { - return Err(ShellError::UnsupportedConfigValue( - "block".to_string(), - format!("{}", other.get_type()), - other.span()?, - )); - } - } - } else { - // always run the hook - true - }; - - if do_run_hook { - match value.clone().follow_cell_path(&[code_path], false)? { - Value::String { - val, - span: source_span, - } => { - let (block, delta, vars) = { - let mut working_set = StateWorkingSet::new(engine_state); - - let mut vars: Vec<(VarId, Value)> = vec![]; - - for (name, val) in arguments { - let var_id = working_set.add_variable( - name.as_bytes().to_vec(), - val.span()?, - Type::Any, - false, - ); - - vars.push((var_id, val)); - } - - let (output, err) = - parse(&mut working_set, Some("hook"), val.as_bytes(), false, &[]); - if let Some(err) = err { - report_error(&working_set, &err); - - return Err(ShellError::UnsupportedConfigValue( - "valid source code".into(), - "source code with syntax errors".into(), - source_span, - )); - } - - (output, working_set.render(), vars) - }; - - engine_state.merge_delta(delta)?; - let input = PipelineData::empty(); - - let var_ids: Vec = vars - .into_iter() - .map(|(var_id, val)| { - stack.add_var(var_id, val); - var_id - }) - .collect(); - - match eval_block(engine_state, stack, &block, input, false, false) { - Ok(pipeline_data) => { - output = pipeline_data; - } - Err(err) => { - report_error_new(engine_state, &err); - } - } - - for var_id in var_ids.iter() { - stack.vars.remove(var_id); - } - } - Value::Block { - val: block_id, - span: block_span, - .. - } => { - run_hook_block( - engine_state, - stack, - block_id, - input, - arguments, - block_span, - )?; - } - Value::Closure { - val: block_id, - span: block_span, - .. - } => { - run_hook_block( - engine_state, - stack, - block_id, - input, - arguments, - block_span, - )?; - } - other => { - return Err(ShellError::UnsupportedConfigValue( - "block or string".to_string(), - format!("{}", other.get_type()), - other.span()?, - )); - } - } - } - } - Value::Block { - val: block_id, - span: block_span, - .. - } => { - output = run_hook_block( - engine_state, - stack, - *block_id, - input, - arguments, - *block_span, - )?; - } - Value::Closure { - val: block_id, - span: block_span, - .. - } => { - output = run_hook_block( - engine_state, - stack, - *block_id, - input, - arguments, - *block_span, - )?; - } - other => { - return Err(ShellError::UnsupportedConfigValue( - "block, record, or list of records".into(), - format!("{}", other.get_type()), - other.span()?, - )); - } - } - - let cwd = get_guaranteed_cwd(engine_state, stack); - engine_state.merge_env(stack, cwd)?; - - Ok(output) -} - -fn run_hook_block( - engine_state: &EngineState, - stack: &mut Stack, - block_id: BlockId, - optional_input: Option, - arguments: Vec<(String, Value)>, - span: Span, -) -> Result { - let block = engine_state.get_block(block_id); - - let input = optional_input.unwrap_or_else(PipelineData::empty); - - let mut callee_stack = stack.gather_captures(&block.captures); - - for (idx, PositionalArg { var_id, .. }) in - block.signature.required_positional.iter().enumerate() - { - if let Some(var_id) = var_id { - if let Some(arg) = arguments.get(idx) { - callee_stack.add_var(*var_id, arg.1.clone()) - } else { - return Err(ShellError::IncompatibleParametersSingle { - msg: "This hook block has too many parameters".into(), - span, - }); - } - } - } - - let pipeline_data = - eval_block_with_early_return(engine_state, &mut callee_stack, block, input, false, false)?; - - if let PipelineData::Value(Value::Error { error }, _) = pipeline_data { - return Err(*error); - } - - // If all went fine, preserve the environment of the called block - let caller_env_vars = 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) { - 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() { - stack.add_env_var(var, value); - } - Ok(pipeline_data) -} - fn run_ansi_sequence(seq: &str) -> Result<(), ShellError> { io::stdout().write_all(seq.as_bytes()).map_err(|e| { ShellError::GenericError( diff --git a/crates/nu-cli/src/util.rs b/crates/nu-cli/src/util.rs index e14c9dfa8..22872edfc 100644 --- a/crates/nu-cli/src/util.rs +++ b/crates/nu-cli/src/util.rs @@ -1,8 +1,8 @@ -use crate::repl::eval_hook; +use nu_command::hook::eval_hook; +use nu_command::util::{report_error, report_error_new}; use nu_engine::{eval_block, eval_block_with_early_return}; use nu_parser::{escape_quote_string, lex, parse, unescape_unquote_string, Token, TokenContents}; use nu_protocol::engine::StateWorkingSet; -use nu_protocol::CliError; use nu_protocol::{ engine::{EngineState, Stack}, print_if_stream, PipelineData, ShellError, Span, Value, @@ -10,7 +10,7 @@ use nu_protocol::{ #[cfg(windows)] use nu_utils::enable_vt_processing; use nu_utils::utils::perf; -use std::path::{Path, PathBuf}; +use std::path::Path; // This will collect environment variables from std::env and adds them to a stack. // @@ -310,43 +310,6 @@ fn set_last_exit_code(stack: &mut Stack, exit_code: i64) { ); } -pub fn report_error( - working_set: &StateWorkingSet, - error: &(dyn miette::Diagnostic + Send + Sync + 'static), -) { - eprintln!("Error: {:?}", 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_error_new( - engine_state: &EngineState, - error: &(dyn miette::Diagnostic + Send + Sync + 'static), -) { - let working_set = StateWorkingSet::new(engine_state); - - report_error(&working_set, error); -} - -pub fn get_init_cwd() -> PathBuf { - std::env::current_dir().unwrap_or_else(|_| { - std::env::var("PWD") - .map(Into::into) - .unwrap_or_else(|_| nu_path::home_dir().unwrap_or_default()) - }) -} - -pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf { - nu_engine::env::current_dir(engine_state, stack).unwrap_or_else(|e| { - let working_set = StateWorkingSet::new(engine_state); - report_error(&working_set, &e); - get_init_cwd() - }) -} - #[cfg(test)] mod test { use super::*; diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index bf149a64b..903fa120c 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -60,6 +60,7 @@ itertools = "0.10.0" log = "0.4.14" lscolors = { version = "0.12.0", features = ["crossterm"], default-features = false } md5 = { package = "md-5", version = "0.10.0" } +miette = { version = "5.5.0", features = ["fancy-no-backtrace"] } mime = "0.3.16" mime_guess = "2.0.4" notify = "4.0.17" diff --git a/crates/nu-command/src/hook.rs b/crates/nu-command/src/hook.rs new file mode 100644 index 000000000..45e767205 --- /dev/null +++ b/crates/nu-command/src/hook.rs @@ -0,0 +1,341 @@ +use crate::util::{get_guaranteed_cwd, report_error, report_error_new}; +use miette::Result; +use nu_engine::{eval_block, eval_block_with_early_return}; +use nu_parser::parse; +use nu_protocol::ast::PathMember; +use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; +use nu_protocol::{BlockId, PipelineData, PositionalArg, ShellError, Span, Type, Value, VarId}; + +pub fn eval_env_change_hook( + env_change_hook: Option, + engine_state: &mut EngineState, + stack: &mut Stack, +) -> Result<(), ShellError> { + if let Some(hook) = env_change_hook { + match hook { + Value::Record { + cols: env_names, + vals: hook_values, + .. + } => { + for (env_name, hook_value) in env_names.iter().zip(hook_values.iter()) { + let before = engine_state + .previous_env_vars + .get(env_name) + .cloned() + .unwrap_or_default(); + + let after = stack + .get_env_var(engine_state, env_name) + .unwrap_or_default(); + + if before != after { + eval_hook( + engine_state, + stack, + None, + vec![("$before".into(), before), ("$after".into(), after.clone())], + hook_value, + )?; + + engine_state + .previous_env_vars + .insert(env_name.to_string(), after); + } + } + } + x => { + return Err(ShellError::TypeMismatch { + err_message: "record for the 'env_change' hook".to_string(), + span: x.span()?, + }); + } + } + } + + Ok(()) +} + +pub fn eval_hook( + engine_state: &mut EngineState, + stack: &mut Stack, + input: Option, + arguments: Vec<(String, Value)>, + value: &Value, +) -> Result { + let value_span = value.span()?; + + // Hooks can optionally be a record in this form: + // { + // condition: {|before, after| ... } # block that evaluates to true/false + // code: # block or a string + // } + // The condition block will be run to check whether the main hook (in `code`) should be run. + // If it returns true (the default if a condition block is not specified), the hook should be run. + let condition_path = PathMember::String { + val: "condition".to_string(), + span: value_span, + optional: false, + }; + let mut output = PipelineData::empty(); + + let code_path = PathMember::String { + val: "code".to_string(), + span: value_span, + optional: false, + }; + + match value { + Value::List { vals, .. } => { + for val in vals { + eval_hook(engine_state, stack, None, arguments.clone(), val)?; + } + } + Value::Record { .. } => { + let do_run_hook = + if let Ok(condition) = value.clone().follow_cell_path(&[condition_path], false) { + match condition { + Value::Block { + val: block_id, + span: block_span, + .. + } + | Value::Closure { + val: block_id, + span: block_span, + .. + } => { + match run_hook_block( + engine_state, + stack, + block_id, + None, + arguments.clone(), + block_span, + ) { + Ok(pipeline_data) => { + if let PipelineData::Value(Value::Bool { val, .. }, ..) = + pipeline_data + { + val + } else { + return Err(ShellError::UnsupportedConfigValue( + "boolean output".to_string(), + "other PipelineData variant".to_string(), + block_span, + )); + } + } + Err(err) => { + return Err(err); + } + } + } + other => { + return Err(ShellError::UnsupportedConfigValue( + "block".to_string(), + format!("{}", other.get_type()), + other.span()?, + )); + } + } + } else { + // always run the hook + true + }; + + if do_run_hook { + match value.clone().follow_cell_path(&[code_path], false)? { + Value::String { + val, + span: source_span, + } => { + let (block, delta, vars) = { + let mut working_set = StateWorkingSet::new(engine_state); + + let mut vars: Vec<(VarId, Value)> = vec![]; + + for (name, val) in arguments { + let var_id = working_set.add_variable( + name.as_bytes().to_vec(), + val.span()?, + Type::Any, + false, + ); + + vars.push((var_id, val)); + } + + let (output, err) = + parse(&mut working_set, Some("hook"), val.as_bytes(), false, &[]); + if let Some(err) = err { + report_error(&working_set, &err); + + return Err(ShellError::UnsupportedConfigValue( + "valid source code".into(), + "source code with syntax errors".into(), + source_span, + )); + } + + (output, working_set.render(), vars) + }; + + engine_state.merge_delta(delta)?; + let input = PipelineData::empty(); + + let var_ids: Vec = vars + .into_iter() + .map(|(var_id, val)| { + stack.add_var(var_id, val); + var_id + }) + .collect(); + + match eval_block(engine_state, stack, &block, input, false, false) { + Ok(pipeline_data) => { + output = pipeline_data; + } + Err(err) => { + report_error_new(engine_state, &err); + } + } + + for var_id in var_ids.iter() { + stack.vars.remove(var_id); + } + } + Value::Block { + val: block_id, + span: block_span, + .. + } => { + run_hook_block( + engine_state, + stack, + block_id, + input, + arguments, + block_span, + )?; + } + Value::Closure { + val: block_id, + span: block_span, + .. + } => { + run_hook_block( + engine_state, + stack, + block_id, + input, + arguments, + block_span, + )?; + } + other => { + return Err(ShellError::UnsupportedConfigValue( + "block or string".to_string(), + format!("{}", other.get_type()), + other.span()?, + )); + } + } + } + } + Value::Block { + val: block_id, + span: block_span, + .. + } => { + output = run_hook_block( + engine_state, + stack, + *block_id, + input, + arguments, + *block_span, + )?; + } + Value::Closure { + val: block_id, + span: block_span, + .. + } => { + output = run_hook_block( + engine_state, + stack, + *block_id, + input, + arguments, + *block_span, + )?; + } + other => { + return Err(ShellError::UnsupportedConfigValue( + "block, record, or list of records".into(), + format!("{}", other.get_type()), + other.span()?, + )); + } + } + + let cwd = get_guaranteed_cwd(engine_state, stack); + engine_state.merge_env(stack, cwd)?; + + Ok(output) +} + +fn run_hook_block( + engine_state: &EngineState, + stack: &mut Stack, + block_id: BlockId, + optional_input: Option, + arguments: Vec<(String, Value)>, + span: Span, +) -> Result { + let block = engine_state.get_block(block_id); + + let input = optional_input.unwrap_or_else(PipelineData::empty); + + let mut callee_stack = stack.gather_captures(&block.captures); + + for (idx, PositionalArg { var_id, .. }) in + block.signature.required_positional.iter().enumerate() + { + if let Some(var_id) = var_id { + if let Some(arg) = arguments.get(idx) { + callee_stack.add_var(*var_id, arg.1.clone()) + } else { + return Err(ShellError::IncompatibleParametersSingle { + msg: "This hook block has too many parameters".into(), + span, + }); + } + } + } + + let pipeline_data = + eval_block_with_early_return(engine_state, &mut callee_stack, block, input, false, false)?; + + if let PipelineData::Value(Value::Error { error }, _) = pipeline_data { + return Err(*error); + } + + // If all went fine, preserve the environment of the called block + let caller_env_vars = 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) { + 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() { + stack.add_env_var(var, value); + } + Ok(pipeline_data) +} diff --git a/crates/nu-command/src/lib.rs b/crates/nu-command/src/lib.rs index a3ac6bd73..c0c2894b2 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -14,6 +14,7 @@ mod filters; mod formats; mod generators; mod hash; +pub mod hook; mod input_handler; mod math; mod misc; @@ -26,6 +27,7 @@ mod shells; mod sort_utils; mod strings; mod system; +pub mod util; mod viewers; pub use bits::*; @@ -45,6 +47,7 @@ pub use filters::*; pub use formats::*; pub use generators::*; pub use hash::*; +pub use hook::*; pub use math::*; pub use misc::*; pub use network::*; @@ -55,6 +58,7 @@ pub use shells::*; pub use sort_utils::*; pub use strings::*; pub use system::*; +pub use util::*; pub use viewers::*; #[cfg(feature = "dataframe")] diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index 1592f4a34..646c0a66c 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -1,3 +1,4 @@ +use crate::hook::eval_hook; use fancy_regex::Regex; use itertools::Itertools; use nu_engine::env_to_strings; @@ -324,9 +325,34 @@ impl ExternalCommand { } }; + let mut err_str = err.to_string(); + if engine_state.is_interactive { + let mut engine_state = engine_state.clone(); + if let Some(hook) = engine_state.config.hooks.command_not_found.clone() + { + if let Ok(PipelineData::Value(Value::String { val, .. }, ..)) = + eval_hook( + &mut engine_state, + stack, + None, + vec![( + "cmd_name".into(), + Value::string( + self.name.item.to_string(), + self.name.span, + ), + )], + &hook, + ) + { + err_str = format!("{}\n{}", err_str, val); + } + } + } + Err(ShellError::ExternalCommand { label, - help: err.to_string(), + help: err_str, span: self.name.span, }) } diff --git a/crates/nu-command/src/util.rs b/crates/nu-command/src/util.rs new file mode 100644 index 000000000..41fe5ebdb --- /dev/null +++ b/crates/nu-command/src/util.rs @@ -0,0 +1,40 @@ +use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; +use nu_protocol::CliError; +use std::path::PathBuf; + +pub fn report_error( + working_set: &StateWorkingSet, + error: &(dyn miette::Diagnostic + Send + Sync + 'static), +) { + eprintln!("Error: {:?}", 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_error_new( + engine_state: &EngineState, + error: &(dyn miette::Diagnostic + Send + Sync + 'static), +) { + let working_set = StateWorkingSet::new(engine_state); + + report_error(&working_set, error); +} + +pub fn get_init_cwd() -> PathBuf { + std::env::current_dir().unwrap_or_else(|_| { + std::env::var("PWD") + .map(Into::into) + .unwrap_or_else(|_| nu_path::home_dir().unwrap_or_default()) + }) +} + +pub fn get_guaranteed_cwd(engine_state: &EngineState, stack: &Stack) -> PathBuf { + nu_engine::env::current_dir(engine_state, stack).unwrap_or_else(|e| { + let working_set = StateWorkingSet::new(engine_state); + report_error(&working_set, &e); + crate::util::get_init_cwd() + }) +} diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index 7115ec7cf..992179e10 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -33,6 +33,7 @@ pub struct Hooks { pub pre_execution: Option, pub env_change: Option, pub display_output: Option, + pub command_not_found: Option, } impl Hooks { @@ -42,6 +43,7 @@ impl Hooks { pre_execution: None, env_change: None, display_output: None, + command_not_found: None, } } } @@ -1529,9 +1531,10 @@ fn create_hooks(value: &Value) -> Result { "pre_execution" => hooks.pre_execution = Some(vals[idx].clone()), "env_change" => hooks.env_change = Some(vals[idx].clone()), "display_output" => hooks.display_output = Some(vals[idx].clone()), + "command_not_found" => hooks.command_not_found = Some(vals[idx].clone()), x => { return Err(ShellError::UnsupportedConfigValue( - "'pre_prompt', 'pre_execution', 'env_change', 'display_output'" + "'pre_prompt', 'pre_execution', 'env_change', 'display_output', 'command_not_found'" .to_string(), x.to_string(), *span, diff --git a/crates/nu-utils/src/sample_config/default_config.nu b/crates/nu-utils/src/sample_config/default_config.nu index 56baec611..e9e66ecae 100644 --- a/crates/nu-utils/src/sample_config/default_config.nu +++ b/crates/nu-utils/src/sample_config/default_config.nu @@ -304,6 +304,9 @@ let-env config = { display_output: {|| if (term size).columns >= 100 { table -e } else { table } } + command_not_found: { + null # replace with source code to return an error message when a command is not found + } } menus: [ # Configuration for default nushell menus diff --git a/src/command.rs b/src/command.rs index 4436c95f1..ec73adf96 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,4 +1,4 @@ -use nu_cli::report_error; +use nu_command::util::report_error; use nu_engine::{get_full_help, CallExt}; use nu_parser::parse; use nu_parser::{escape_for_script_arg, escape_quote_string}; diff --git a/src/config_files.rs b/src/config_files.rs index 1a62bbf9d..9ba13227e 100644 --- a/src/config_files.rs +++ b/src/config_files.rs @@ -1,7 +1,8 @@ use log::info; #[cfg(feature = "plugin")] use nu_cli::read_plugin_file; -use nu_cli::{eval_config_contents, eval_source, report_error}; +use nu_cli::{eval_config_contents, eval_source}; +use nu_command::util::report_error; use nu_parser::ParseError; use nu_path::canonicalize_with; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; diff --git a/src/main.rs b/src/main.rs index d62ba568d..6f6adadd4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,8 +17,9 @@ use crate::{ use command::gather_commandline_args; use log::Level; use miette::Result; -use nu_cli::{gather_parent_env_vars, get_init_cwd, report_error_new}; -use nu_command::create_default_context; +use nu_cli::gather_parent_env_vars; +use nu_command::util::report_error_new; +use nu_command::{create_default_context, get_init_cwd}; use nu_protocol::{util::BufferedReader, PipelineData, RawStream}; use nu_utils::utils::perf; use run::{run_commands, run_file, run_repl}; diff --git a/src/test_bins.rs b/src/test_bins.rs index 37ce213c8..65cfa2fe6 100644 --- a/src/test_bins.rs +++ b/src/test_bins.rs @@ -1,7 +1,7 @@ use std::io::{self, BufRead, Read, Write}; -use nu_cli::{eval_env_change_hook, eval_hook}; use nu_command::create_default_context; +use nu_command::hook::{eval_env_change_hook, eval_hook}; use nu_engine::eval_block; use nu_parser::parse; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};