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.
This commit is contained in:
Steven Xu 2023-03-20 15:05:22 +11:00 committed by GitHub
parent e36a2947b9
commit 1d3f6105f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 447 additions and 394 deletions

1
Cargo.lock generated
View File

@ -2798,6 +2798,7 @@ dependencies = [
"log", "log",
"lscolors", "lscolors",
"md-5", "md-5",
"miette",
"mime", "mime",
"mime_guess", "mime_guess",
"mockito", "mockito",

View File

@ -12,10 +12,10 @@ bench = false
[dev-dependencies] [dev-dependencies]
nu-test-support = { path = "../nu-test-support", version = "0.77.2" } 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 } rstest = { version = "0.16.0", default-features = false }
[dependencies] [dependencies]
nu-command = { path = "../nu-command", version = "0.77.2" }
nu-engine = { path = "../nu-engine", version = "0.77.2" } nu-engine = { path = "../nu-engine", version = "0.77.2" }
nu-path = { path = "../nu-path", version = "0.77.2" } nu-path = { path = "../nu-path", version = "0.77.2" }
nu-parser = { path = "../nu-parser", version = "0.77.2" } nu-parser = { path = "../nu-parser", version = "0.77.2" }

View File

@ -1,6 +1,6 @@
use crate::util::report_error;
use log::info; use log::info;
use miette::Result; use miette::Result;
use nu_command::util::report_error;
use nu_engine::{convert_env_values, eval_block}; use nu_engine::{convert_env_values, eval_block};
use nu_parser::parse; use nu_parser::parse;
use nu_protocol::engine::Stack; use nu_protocol::engine::Stack;

View File

@ -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")] #[cfg(feature = "plugin")]
use nu_parser::ParseError; use nu_parser::ParseError;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]

View File

@ -1,7 +1,8 @@
use crate::util::{eval_source, report_error}; use crate::util::eval_source;
use log::info; use log::info;
use log::trace; use log::trace;
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use nu_command::util::report_error;
use nu_engine::{convert_env_values, current_dir}; use nu_engine::{convert_env_values, current_dir};
use nu_parser::parse; use nu_parser::parse;
use nu_path::canonicalize_with; use nu_path::canonicalize_with;

View File

@ -18,13 +18,13 @@ pub use completions::{FileCompletion, NuCompleter};
pub use config_files::eval_config_contents; pub use config_files::eval_config_contents;
pub use eval_file::evaluate_file; pub use eval_file::evaluate_file;
pub use menus::{DescriptionMenu, NuHelpCompleter}; 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 nu_highlight::NuHighlight;
pub use print::Print; pub use print::Print;
pub use prompt::NushellPrompt; pub use prompt::NushellPrompt;
pub use repl::evaluate_repl; pub use repl::evaluate_repl;
pub use repl::{eval_env_change_hook, eval_hook};
pub use syntax_highlight::NuHighlighter; 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; pub use validation::NuValidator;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]

View File

@ -1,6 +1,6 @@
use crate::util::report_error;
use crate::NushellPrompt; use crate::NushellPrompt;
use log::trace; use log::trace;
use nu_command::util::report_error;
use nu_engine::eval_subexpression; use nu_engine::eval_subexpression;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},

View File

@ -2,21 +2,21 @@ use crate::{
completions::NuCompleter, completions::NuCompleter,
prompt_update, prompt_update,
reedline_config::{add_menus, create_keybindings, KeybindingsMode}, reedline_config::{add_menus, create_keybindings, KeybindingsMode},
util::{eval_source, get_guaranteed_cwd, report_error, report_error_new}, util::eval_source,
NuHighlighter, NuValidator, NushellPrompt, NuHighlighter, NuValidator, NushellPrompt,
}; };
use crossterm::cursor::CursorShape; use crossterm::cursor::CursorShape;
use log::{trace, warn}; use log::{trace, warn};
use miette::{IntoDiagnostic, Result}; use miette::{IntoDiagnostic, Result};
use nu_color_config::StyleComputer; 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_parser::{lex, parse, trim_quotes_str};
use nu_protocol::{ use nu_protocol::{
ast::PathMember,
config::NuCursorShape, config::NuCursorShape,
engine::{EngineState, Stack, StateWorkingSet}, engine::{EngineState, Stack, StateWorkingSet},
format_duration, BlockId, HistoryFileFormat, PipelineData, PositionalArg, ShellError, Span, format_duration, HistoryFileFormat, PipelineData, ShellError, Span, Spanned, Value,
Spanned, Type, Value, VarId,
}; };
use nu_utils::utils::perf; use nu_utils::utils::perf;
use reedline::{CursorConfig, DefaultHinter, EditCommand, Emacs, SqliteBackedHistory, Vi}; use reedline::{CursorConfig, DefaultHinter, EditCommand, Emacs, SqliteBackedHistory, Vi};
@ -44,6 +44,7 @@ pub fn evaluate_repl(
prerun_command: Option<Spanned<String>>, prerun_command: Option<Spanned<String>>,
entire_start_time: Instant, entire_start_time: Instant,
) -> Result<()> { ) -> Result<()> {
use nu_command::hook;
use reedline::{FileBackedHistory, Reedline, Signal}; use reedline::{FileBackedHistory, Reedline, Signal};
let use_color = engine_state.get_config().use_ansi_coloring; let use_color = engine_state.get_config().use_ansi_coloring;
@ -400,7 +401,7 @@ pub fn evaluate_repl(
// fire the "env_change" hook // fire the "env_change" hook
let config = engine_state.get_config(); let config = engine_state.get_config();
if let Err(error) = 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) 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)) format!("\x1b]133;D;{}\x1b\\", exit_code.unwrap_or(0))
} }
pub fn eval_env_change_hook(
env_change_hook: Option<Value>,
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<PipelineData>,
arguments: Vec<(String, Value)>,
value: &Value,
) -> Result<PipelineData, ShellError> {
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<VarId> = 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<PipelineData>,
arguments: Vec<(String, Value)>,
span: Span,
) -> Result<PipelineData, ShellError> {
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> { fn run_ansi_sequence(seq: &str) -> Result<(), ShellError> {
io::stdout().write_all(seq.as_bytes()).map_err(|e| { io::stdout().write_all(seq.as_bytes()).map_err(|e| {
ShellError::GenericError( ShellError::GenericError(

View File

@ -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_engine::{eval_block, eval_block_with_early_return};
use nu_parser::{escape_quote_string, lex, parse, unescape_unquote_string, Token, TokenContents}; use nu_parser::{escape_quote_string, lex, parse, unescape_unquote_string, Token, TokenContents};
use nu_protocol::engine::StateWorkingSet; use nu_protocol::engine::StateWorkingSet;
use nu_protocol::CliError;
use nu_protocol::{ use nu_protocol::{
engine::{EngineState, Stack}, engine::{EngineState, Stack},
print_if_stream, PipelineData, ShellError, Span, Value, print_if_stream, PipelineData, ShellError, Span, Value,
@ -10,7 +10,7 @@ use nu_protocol::{
#[cfg(windows)] #[cfg(windows)]
use nu_utils::enable_vt_processing; use nu_utils::enable_vt_processing;
use nu_utils::utils::perf; 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. // 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -60,6 +60,7 @@ itertools = "0.10.0"
log = "0.4.14" log = "0.4.14"
lscolors = { version = "0.12.0", features = ["crossterm"], default-features = false } lscolors = { version = "0.12.0", features = ["crossterm"], default-features = false }
md5 = { package = "md-5", version = "0.10.0" } md5 = { package = "md-5", version = "0.10.0" }
miette = { version = "5.5.0", features = ["fancy-no-backtrace"] }
mime = "0.3.16" mime = "0.3.16"
mime_guess = "2.0.4" mime_guess = "2.0.4"
notify = "4.0.17" notify = "4.0.17"

View File

@ -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<Value>,
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<PipelineData>,
arguments: Vec<(String, Value)>,
value: &Value,
) -> Result<PipelineData, ShellError> {
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<VarId> = 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<PipelineData>,
arguments: Vec<(String, Value)>,
span: Span,
) -> Result<PipelineData, ShellError> {
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)
}

View File

@ -14,6 +14,7 @@ mod filters;
mod formats; mod formats;
mod generators; mod generators;
mod hash; mod hash;
pub mod hook;
mod input_handler; mod input_handler;
mod math; mod math;
mod misc; mod misc;
@ -26,6 +27,7 @@ mod shells;
mod sort_utils; mod sort_utils;
mod strings; mod strings;
mod system; mod system;
pub mod util;
mod viewers; mod viewers;
pub use bits::*; pub use bits::*;
@ -45,6 +47,7 @@ pub use filters::*;
pub use formats::*; pub use formats::*;
pub use generators::*; pub use generators::*;
pub use hash::*; pub use hash::*;
pub use hook::*;
pub use math::*; pub use math::*;
pub use misc::*; pub use misc::*;
pub use network::*; pub use network::*;
@ -55,6 +58,7 @@ pub use shells::*;
pub use sort_utils::*; pub use sort_utils::*;
pub use strings::*; pub use strings::*;
pub use system::*; pub use system::*;
pub use util::*;
pub use viewers::*; pub use viewers::*;
#[cfg(feature = "dataframe")] #[cfg(feature = "dataframe")]

View File

@ -1,3 +1,4 @@
use crate::hook::eval_hook;
use fancy_regex::Regex; use fancy_regex::Regex;
use itertools::Itertools; use itertools::Itertools;
use nu_engine::env_to_strings; 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 { Err(ShellError::ExternalCommand {
label, label,
help: err.to_string(), help: err_str,
span: self.name.span, span: self.name.span,
}) })
} }

View File

@ -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()
})
}

View File

@ -33,6 +33,7 @@ pub struct Hooks {
pub pre_execution: Option<Value>, pub pre_execution: Option<Value>,
pub env_change: Option<Value>, pub env_change: Option<Value>,
pub display_output: Option<Value>, pub display_output: Option<Value>,
pub command_not_found: Option<Value>,
} }
impl Hooks { impl Hooks {
@ -42,6 +43,7 @@ impl Hooks {
pre_execution: None, pre_execution: None,
env_change: None, env_change: None,
display_output: None, display_output: None,
command_not_found: None,
} }
} }
} }
@ -1529,9 +1531,10 @@ fn create_hooks(value: &Value) -> Result<Hooks, ShellError> {
"pre_execution" => hooks.pre_execution = Some(vals[idx].clone()), "pre_execution" => hooks.pre_execution = Some(vals[idx].clone()),
"env_change" => hooks.env_change = Some(vals[idx].clone()), "env_change" => hooks.env_change = Some(vals[idx].clone()),
"display_output" => hooks.display_output = 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 => { x => {
return Err(ShellError::UnsupportedConfigValue( 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(), .to_string(),
x.to_string(), x.to_string(),
*span, *span,

View File

@ -304,6 +304,9 @@ let-env config = {
display_output: {|| display_output: {||
if (term size).columns >= 100 { table -e } else { table } 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: [ menus: [
# Configuration for default nushell menus # Configuration for default nushell menus

View File

@ -1,4 +1,4 @@
use nu_cli::report_error; use nu_command::util::report_error;
use nu_engine::{get_full_help, CallExt}; use nu_engine::{get_full_help, CallExt};
use nu_parser::parse; use nu_parser::parse;
use nu_parser::{escape_for_script_arg, escape_quote_string}; use nu_parser::{escape_for_script_arg, escape_quote_string};

View File

@ -1,7 +1,8 @@
use log::info; use log::info;
#[cfg(feature = "plugin")] #[cfg(feature = "plugin")]
use nu_cli::read_plugin_file; 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_parser::ParseError;
use nu_path::canonicalize_with; use nu_path::canonicalize_with;
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};

View File

@ -17,8 +17,9 @@ use crate::{
use command::gather_commandline_args; use command::gather_commandline_args;
use log::Level; use log::Level;
use miette::Result; use miette::Result;
use nu_cli::{gather_parent_env_vars, get_init_cwd, report_error_new}; use nu_cli::gather_parent_env_vars;
use nu_command::create_default_context; 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_protocol::{util::BufferedReader, PipelineData, RawStream};
use nu_utils::utils::perf; use nu_utils::utils::perf;
use run::{run_commands, run_file, run_repl}; use run::{run_commands, run_file, run_repl};

View File

@ -1,7 +1,7 @@
use std::io::{self, BufRead, Read, Write}; 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::create_default_context;
use nu_command::hook::{eval_env_change_hook, eval_hook};
use nu_engine::eval_block; use nu_engine::eval_block;
use nu_parser::parse; use nu_parser::parse;
use nu_protocol::engine::{EngineState, Stack, StateWorkingSet}; use nu_protocol::engine::{EngineState, Stack, StateWorkingSet};