nushell/crates/nu-command/src/hook.rs

343 lines
12 KiB
Rust
Raw Normal View History

use crate::util::get_guaranteed_cwd;
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::cli_error::{report_error, report_error_new};
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 =
parse(&mut working_set, Some("hook"), val.as_bytes(), false);
if let Some(err) = working_set.parse_errors.first() {
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() {
Move variables to var stack (#8604) # Description This moves the representation of variables on the stack to a Vec, which more closely resembles a stack. For small numbers of variables live at any one point, this tends to be more efficient than a HashMap. Having a stack-like vector also allows us to remember a stack position, temporarily push variables on, then quickly drop the stack back to the original size when we're done. We'll need this capability to allow matching inside of conditions. On this mac, a simple run of: `timeit { mut x = 1; while $x < 1000000 { $x += 1 } }` Went from 1 sec 86 ms, down to 1 sec 2 ms. Clearly, we have a lot more ground we can make up in looping speed 😅 but it's nice that for fixing this to make matching easier, we also get a win in terms of lookup speed for small numbers of variables. # User-Facing Changes Likely users won't (hopefully) see any negative impact and may even see a small positive impact. # 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 > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` # 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.
2023-03-25 00:56:45 +01:00
stack.remove_var(*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)
}