mirror of
https://github.com/nushell/nushell
synced 2024-12-26 04:53:09 +00:00
Expand Hooks Functionality (#5982)
* (WIP) Initial messy support for hooks as strings * Cleanup after running condition & hook code Also, remove prints * Move env hooks eval into its own function * Add env change hooks to simulator * Fix hooks simulator not running env hooks properly * Add missing hooks test file * Expand hooks tests * Add blocks as env hooks; Preserve hook environment * Add full eval to pre prompt/exec hooks; Fix panic * Rename env change hook back to orig. name * Print err on test failure; Add list of hooks test * Consolidate condition block; Fix panic; Misc * CHange test to use real file * Remove unused stuff * Fix potential panics; Clean up errors * Remove commented unused code * Clippy: Fix extra references * Add back support for old-style hooks * Reorder functions; Fmt * Fix test on Windows * Add more test cases; Simplify some error reporting * Add more tests for setting correct before/after * Move pre_prompt hook to the beginning Since we don't have a prompt or blocking on user input, all hooks just follow after each other.
This commit is contained in:
parent
f85a1d003c
commit
3676a8a48d
8 changed files with 917 additions and 128 deletions
|
@ -22,6 +22,7 @@ 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};
|
||||
pub use validation::NuValidator;
|
||||
|
|
|
@ -2,17 +2,18 @@ use crate::{
|
|||
completions::NuCompleter,
|
||||
prompt_update,
|
||||
reedline_config::{add_menus, create_keybindings, KeybindingsMode},
|
||||
util::{eval_source, report_error},
|
||||
util::{eval_source, get_init_cwd, report_error, report_error_new},
|
||||
NuHighlighter, NuValidator, NushellPrompt,
|
||||
};
|
||||
use log::{info, trace};
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use nu_color_config::get_color_config;
|
||||
use nu_engine::{convert_env_values, eval_block};
|
||||
use nu_parser::lex;
|
||||
use nu_parser::{lex, parse};
|
||||
use nu_protocol::{
|
||||
ast::PathMember,
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
BlockId, HistoryFileFormat, PipelineData, PositionalArg, ShellError, Span, Value,
|
||||
BlockId, HistoryFileFormat, PipelineData, PositionalArg, ShellError, Span, Type, Value, VarId,
|
||||
};
|
||||
use reedline::{DefaultHinter, Emacs, SqliteBackedHistory, Vi};
|
||||
use std::io::{self, Write};
|
||||
|
@ -79,7 +80,7 @@ pub fn evaluate_repl(
|
|||
|
||||
// Get the config once for the history `max_history_size`
|
||||
// Updating that will not be possible in one session
|
||||
let mut config = engine_state.get_config();
|
||||
let config = engine_state.get_config();
|
||||
|
||||
if is_perf_true {
|
||||
info!("setup reedline {}:{}:{}", file!(), line!(), column!());
|
||||
|
@ -130,7 +131,7 @@ pub fn evaluate_repl(
|
|||
sig_quit.store(false, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
config = engine_state.get_config();
|
||||
let config = engine_state.get_config();
|
||||
|
||||
if is_perf_true {
|
||||
info!("setup colors {}:{}:{}", file!(), line!(), column!());
|
||||
|
@ -236,58 +237,22 @@ pub fn evaluate_repl(
|
|||
|
||||
// Right before we start our prompt and take input from the user,
|
||||
// fire the "pre_prompt" hook
|
||||
if let Some(hook) = &config.hooks.pre_prompt {
|
||||
if let Err(err) = run_hook(engine_state, stack, vec![], hook) {
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
report_error(&working_set, &err);
|
||||
if let Some(hook) = config.hooks.pre_prompt.clone() {
|
||||
if let Err(err) = eval_hook(engine_state, stack, vec![], &hook) {
|
||||
report_error_new(engine_state, &err);
|
||||
}
|
||||
}
|
||||
|
||||
// Next, check all the environment variables they ask for
|
||||
// fire the "env_change" hook
|
||||
if let Some(hook) = config.hooks.env_change.clone() {
|
||||
match hook {
|
||||
Value::Record {
|
||||
cols, vals: blocks, ..
|
||||
} => {
|
||||
for (idx, env_var) in cols.iter().enumerate() {
|
||||
let before = engine_state
|
||||
.previous_env_vars
|
||||
.get(env_var)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let after = stack.get_env_var(engine_state, env_var).unwrap_or_default();
|
||||
if before != after {
|
||||
if let Err(err) = run_hook(
|
||||
engine_state,
|
||||
stack,
|
||||
vec![before, after.clone()],
|
||||
&blocks[idx],
|
||||
) {
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
report_error(&working_set, &err);
|
||||
}
|
||||
|
||||
engine_state
|
||||
.previous_env_vars
|
||||
.insert(env_var.to_string(), after);
|
||||
}
|
||||
}
|
||||
}
|
||||
x => {
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
report_error(
|
||||
&working_set,
|
||||
&ShellError::TypeMismatch(
|
||||
"record for 'env_change' hook".to_string(),
|
||||
x.span().unwrap_or_else(|_| Span::new(0, 0)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
let config = engine_state.get_config();
|
||||
if let Err(error) =
|
||||
eval_env_change_hook(config.hooks.env_change.clone(), engine_state, stack)
|
||||
{
|
||||
report_error_new(engine_state, &error)
|
||||
}
|
||||
|
||||
config = engine_state.get_config();
|
||||
let config = engine_state.get_config();
|
||||
|
||||
let shell_integration = config.shell_integration;
|
||||
if shell_integration {
|
||||
|
@ -328,10 +293,9 @@ pub fn evaluate_repl(
|
|||
|
||||
// Right before we start running the code the user gave us,
|
||||
// fire the "pre_execution" hook
|
||||
if let Some(hook) = &config.hooks.pre_execution {
|
||||
if let Err(err) = run_hook(engine_state, stack, vec![], hook) {
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
report_error(&working_set, &err);
|
||||
if let Some(hook) = config.hooks.pre_execution.clone() {
|
||||
if let Err(err) = eval_hook(engine_state, stack, vec![], &hook) {
|
||||
report_error_new(engine_state, &err);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -491,6 +455,280 @@ pub fn evaluate_repl(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
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,
|
||||
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(
|
||||
"record for the 'env_change' hook".to_string(),
|
||||
x.span()?,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn eval_hook(
|
||||
engine_state: &mut EngineState,
|
||||
stack: &mut Stack,
|
||||
arguments: Vec<(String, Value)>,
|
||||
value: &Value,
|
||||
) -> Result<(), ShellError> {
|
||||
let value_span = value.span()?;
|
||||
|
||||
let condition_path = PathMember::String {
|
||||
val: "condition".to_string(),
|
||||
span: value_span,
|
||||
};
|
||||
|
||||
let code_path = PathMember::String {
|
||||
val: "code".to_string(),
|
||||
span: value_span,
|
||||
};
|
||||
|
||||
match value {
|
||||
Value::List { vals, .. } => {
|
||||
for val in vals {
|
||||
eval_hook(engine_state, stack, 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,
|
||||
..
|
||||
} => {
|
||||
match run_hook_block(
|
||||
engine_state,
|
||||
stack,
|
||||
block_id,
|
||||
arguments.clone(),
|
||||
block_span,
|
||||
) {
|
||||
Ok(value) => match value {
|
||||
Value::Bool { val, .. } => val,
|
||||
other => {
|
||||
return Err(ShellError::UnsupportedConfigValue(
|
||||
"boolean output".to_string(),
|
||||
format!("{}", other.get_type()),
|
||||
other.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,
|
||||
);
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
let cwd = match nu_engine::env::current_dir(engine_state, stack) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
report_error_new(engine_state, &e);
|
||||
get_init_cwd()
|
||||
}
|
||||
};
|
||||
|
||||
let _ = engine_state.merge_delta(delta, Some(stack), &cwd);
|
||||
let input = PipelineData::new(value_span);
|
||||
|
||||
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(_) => {}
|
||||
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, 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,
|
||||
..
|
||||
} => {
|
||||
run_hook_block(engine_state, stack, *block_id, arguments, *block_span)?;
|
||||
}
|
||||
other => {
|
||||
return Err(ShellError::UnsupportedConfigValue(
|
||||
"block, record, or list of records".into(),
|
||||
format!("{}", other.get_type()),
|
||||
other.span()?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_hook_block(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
block_id: BlockId,
|
||||
arguments: Vec<(String, Value)>,
|
||||
span: Span,
|
||||
) -> Result<Value, ShellError> {
|
||||
let block = engine_state.get_block(block_id);
|
||||
|
||||
let input = PipelineData::new(span);
|
||||
|
||||
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(
|
||||
"This hook block has too many parameters".into(),
|
||||
span,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match eval_block(engine_state, &mut callee_stack, block, input, false, false) {
|
||||
Ok(pipeline_data) => match pipeline_data.into_value(span) {
|
||||
Value::Error { error } => Err(error),
|
||||
val => {
|
||||
// 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(val)
|
||||
}
|
||||
},
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_ansi_sequence(seq: &str) -> Result<(), ShellError> {
|
||||
match io::stdout().write_all(seq.as_bytes()) {
|
||||
Ok(it) => it,
|
||||
|
@ -514,63 +752,3 @@ fn run_ansi_sequence(seq: &str) -> Result<(), ShellError> {
|
|||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run_hook(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
arguments: Vec<Value>,
|
||||
value: &Value,
|
||||
) -> Result<(), ShellError> {
|
||||
match value {
|
||||
Value::List { vals, .. } => {
|
||||
for val in vals {
|
||||
run_hook(engine_state, stack, arguments.clone(), val)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Value::Block {
|
||||
val: block_id,
|
||||
span,
|
||||
..
|
||||
} => run_hook_block(engine_state, stack, *block_id, arguments, *span),
|
||||
x => match x.span() {
|
||||
Ok(span) => Err(ShellError::MissingConfigValue(
|
||||
"block for hook in config".into(),
|
||||
span,
|
||||
)),
|
||||
_ => Err(ShellError::MissingConfigValue(
|
||||
"block for hook in config".into(),
|
||||
Span { start: 0, end: 0 },
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_hook_block(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
block_id: BlockId,
|
||||
arguments: Vec<Value>,
|
||||
span: Span,
|
||||
) -> Result<(), ShellError> {
|
||||
let block = engine_state.get_block(block_id);
|
||||
let input = PipelineData::new(span);
|
||||
|
||||
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 {
|
||||
callee_stack.add_var(*var_id, arguments[idx].clone())
|
||||
}
|
||||
}
|
||||
|
||||
match eval_block(engine_state, &mut callee_stack, block, input, false, false) {
|
||||
Ok(pipeline_data) => match pipeline_data.into_value(span) {
|
||||
Value::Error { error } => Err(error),
|
||||
_ => Ok(()),
|
||||
},
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -297,6 +297,15 @@ pub fn report_error(
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
match std::env::current_dir() {
|
||||
Ok(cwd) => cwd,
|
||||
|
|
|
@ -31,7 +31,9 @@ impl std::fmt::Debug for CliError<'_> {
|
|||
.terminal_links(ansi_support)
|
||||
.build();
|
||||
|
||||
miette_handler.debug(self, f)?;
|
||||
// Ignore error to prevent format! panics. This can happen if span points at some
|
||||
// inaccessible location, for example by calling `report_error()` with wrong working set.
|
||||
let _ = miette_handler.debug(self, f);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
564
tests/hooks/mod.rs
Normal file
564
tests/hooks/mod.rs
Normal file
|
@ -0,0 +1,564 @@
|
|||
use super::nu_repl::nu_repl;
|
||||
|
||||
fn env_change_hook_code_list(name: &str, code_list: &[&str]) -> String {
|
||||
let mut list = String::new();
|
||||
|
||||
for code in code_list.iter() {
|
||||
list.push_str("{ code: ");
|
||||
list.push_str(code);
|
||||
list.push_str(" }\n");
|
||||
}
|
||||
|
||||
format!(
|
||||
r#"let-env config = {{
|
||||
hooks: {{
|
||||
env_change: {{
|
||||
{name} : [
|
||||
{list}
|
||||
]
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
fn env_change_hook(name: &str, code: &str) -> String {
|
||||
format!(
|
||||
r#"let-env config = {{
|
||||
hooks: {{
|
||||
env_change: {{
|
||||
{name} : {code}
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
fn env_change_hook_code(name: &str, code: &str) -> String {
|
||||
format!(
|
||||
r#"let-env config = {{
|
||||
hooks: {{
|
||||
env_change: {{
|
||||
{name} : {{
|
||||
code: {code}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
fn env_change_hook_code_condition(name: &str, condition: &str, code: &str) -> String {
|
||||
format!(
|
||||
r#"let-env config = {{
|
||||
hooks: {{
|
||||
env_change: {{
|
||||
{name} : {{
|
||||
condition: {condition}
|
||||
code: {code}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
fn pre_prompt_hook(code: &str) -> String {
|
||||
format!(
|
||||
r#"let-env config = {{
|
||||
hooks: {{
|
||||
pre_prompt: {code}
|
||||
}}
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
fn pre_prompt_hook_code(code: &str) -> String {
|
||||
format!(
|
||||
r#"let-env config = {{
|
||||
hooks: {{
|
||||
pre_prompt: {{
|
||||
code: {code}
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
fn pre_execution_hook(code: &str) -> String {
|
||||
format!(
|
||||
r#"let-env config = {{
|
||||
hooks: {{
|
||||
pre_execution: {code}
|
||||
}}
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
fn pre_execution_hook_code(code: &str) -> String {
|
||||
format!(
|
||||
r#"let-env config = {{
|
||||
hooks: {{
|
||||
pre_execution: {{
|
||||
code: {code}
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_change_define_command() {
|
||||
let inp = &[
|
||||
&env_change_hook_code("FOO", r#"'def foo [] { "got foo!" }'"#),
|
||||
"let-env FOO = 1",
|
||||
"foo",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "got foo!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_change_define_variable() {
|
||||
let inp = &[
|
||||
&env_change_hook_code("FOO", r#"'let x = "spam"'"#),
|
||||
"let-env FOO = 1",
|
||||
"$x",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_change_define_env_var() {
|
||||
let inp = &[
|
||||
&env_change_hook_code("FOO", r#"'let-env SPAM = "spam"'"#),
|
||||
"let-env FOO = 1",
|
||||
"$env.SPAM",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_change_define_alias() {
|
||||
let inp = &[
|
||||
&env_change_hook_code("FOO", r#"'alias spam = "spam"'"#),
|
||||
"let-env FOO = 1",
|
||||
"spam",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_change_simple_block_preserve_env_var() {
|
||||
let inp = &[
|
||||
&env_change_hook("FOO", r#"{ let-env SPAM = "spam" }"#),
|
||||
"let-env FOO = 1",
|
||||
"$env.SPAM",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_change_simple_block_list_shadow_env_var() {
|
||||
let inp = &[
|
||||
&env_change_hook(
|
||||
"FOO",
|
||||
r#"[
|
||||
{ let-env SPAM = "foo" }
|
||||
{ let-env SPAM = "spam" }
|
||||
]"#,
|
||||
),
|
||||
"let-env FOO = 1",
|
||||
"$env.SPAM",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_change_block_preserve_env_var() {
|
||||
let inp = &[
|
||||
&env_change_hook_code("FOO", r#"{ let-env SPAM = "spam" }"#),
|
||||
"let-env FOO = 1",
|
||||
"$env.SPAM",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_prompt_define_command() {
|
||||
let inp = &[
|
||||
&pre_prompt_hook_code(r#"'def foo [] { "got foo!" }'"#),
|
||||
"",
|
||||
"foo",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "got foo!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_prompt_simple_block_preserve_env_var() {
|
||||
let inp = &[
|
||||
&pre_prompt_hook(r#"{ let-env SPAM = "spam" }"#),
|
||||
"",
|
||||
"$env.SPAM",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_prompt_simple_block_list_shadow_env_var() {
|
||||
let inp = &[
|
||||
&pre_prompt_hook(
|
||||
r#"[
|
||||
{ let-env SPAM = "foo" }
|
||||
{ let-env SPAM = "spam" }
|
||||
]"#,
|
||||
),
|
||||
"",
|
||||
"$env.SPAM",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_prompt_block_preserve_env_var() {
|
||||
let inp = &[
|
||||
&pre_prompt_hook_code(r#"{ let-env SPAM = "spam" }"#),
|
||||
"",
|
||||
"$env.SPAM",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_execution_define_command() {
|
||||
let inp = &[
|
||||
&pre_execution_hook_code(r#"'def foo [] { "got foo!" }'"#),
|
||||
"",
|
||||
"foo",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "got foo!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_execution_simple_block_preserve_env_var() {
|
||||
let inp = &[
|
||||
&pre_execution_hook(r#"{ let-env SPAM = "spam" }"#),
|
||||
"",
|
||||
"$env.SPAM",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_execution_simple_block_list_shadow_env_var() {
|
||||
let inp = &[
|
||||
&pre_execution_hook(
|
||||
r#"[
|
||||
{ let-env SPAM = "foo" }
|
||||
{ let-env SPAM = "spam" }
|
||||
]"#,
|
||||
),
|
||||
"",
|
||||
"$env.SPAM",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_execution_block_preserve_env_var() {
|
||||
let inp = &[
|
||||
&pre_execution_hook_code(r#"{ let-env SPAM = "spam" }"#),
|
||||
"",
|
||||
"$env.SPAM",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_change_shadow_command() {
|
||||
let inp = &[
|
||||
&env_change_hook_code_list(
|
||||
"FOO",
|
||||
&[
|
||||
r#"'def foo [] { "got spam!" }'"#,
|
||||
r#"'def foo [] { "got foo!" }'"#,
|
||||
],
|
||||
),
|
||||
"let-env FOO = 1",
|
||||
"foo",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "got foo!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_change_block_dont_preserve_command() {
|
||||
let inp = &[
|
||||
&env_change_hook_code("FOO", r#"{ def foo [] { "foo" } }"#),
|
||||
"let-env FOO = 1",
|
||||
"foo",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
#[cfg(windows)]
|
||||
assert!(actual_repl.out != "foo");
|
||||
#[cfg(not(windows))]
|
||||
assert!(actual_repl.err.contains("ExternalCommand"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_change_block_condition_pwd() {
|
||||
let inp = &[
|
||||
&env_change_hook_code_condition(
|
||||
"PWD",
|
||||
r#"{|before, after| ($after | path basename) == samples }"#,
|
||||
r#"'source .nu-env'"#,
|
||||
),
|
||||
"cd samples",
|
||||
"$env.SPAM",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "spam");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_change_block_condition_correct_args() {
|
||||
let inp = &[
|
||||
r#"let-env FOO = 1"#,
|
||||
&env_change_hook_code_condition(
|
||||
"FOO",
|
||||
r#"{|before, after| $before == 1 and $after == 2}"#,
|
||||
r#"{|before, after| let-env SPAM = ($before == 1 and $after == 2) }"#,
|
||||
),
|
||||
"",
|
||||
r#"let-env FOO = 2"#,
|
||||
"$env.SPAM",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_change_dont_panic_with_many_args() {
|
||||
let inp = &[
|
||||
&env_change_hook_code("FOO", r#"{ |a, b, c| let-env SPAM = 'spam' }"#),
|
||||
"let-env FOO = 1",
|
||||
"",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert!(actual_repl.err.contains("IncompatibleParametersSingle"));
|
||||
assert_eq!(actual_repl.out, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn err_hook_wrong_env_type_1() {
|
||||
let inp = &[
|
||||
r#"let-env config = {
|
||||
hooks: {
|
||||
env_change: {
|
||||
FOO : 1
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
"let-env FOO = 1",
|
||||
"",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert!(actual_repl.err.contains("UnsupportedConfigValue"));
|
||||
assert_eq!(actual_repl.out, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn err_hook_wrong_env_type_2() {
|
||||
let inp = &[
|
||||
r#"let-env config = {
|
||||
hooks: {
|
||||
env_change: "print spam"
|
||||
}
|
||||
}"#,
|
||||
"",
|
||||
"",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert!(actual_repl.err.contains("TypeMismatch"));
|
||||
assert_eq!(actual_repl.out, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn err_hook_wrong_env_type_3() {
|
||||
let inp = &[
|
||||
r#"let-env config = {
|
||||
hooks: {
|
||||
env_change: {
|
||||
FOO : {
|
||||
code: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
"let-env FOO = 1",
|
||||
"",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert!(actual_repl.err.contains("UnsupportedConfigValue"));
|
||||
assert_eq!(actual_repl.out, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn err_hook_non_boolean_condition_output() {
|
||||
let inp = &[
|
||||
r#"let-env config = {
|
||||
hooks: {
|
||||
env_change: {
|
||||
FOO : {
|
||||
condition: { "foo" }
|
||||
code: "print spam"
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
"let-env FOO = 1",
|
||||
"",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert!(actual_repl.err.contains("UnsupportedConfigValue"));
|
||||
assert_eq!(actual_repl.out, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn err_hook_non_condition_not_a_block() {
|
||||
let inp = &[
|
||||
r#"let-env config = {
|
||||
hooks: {
|
||||
env_change: {
|
||||
FOO : {
|
||||
condition: "foo"
|
||||
code: "print spam"
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
"let-env FOO = 1",
|
||||
"",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert!(actual_repl.err.contains("UnsupportedConfigValue"));
|
||||
assert_eq!(actual_repl.out, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn err_hook_parse_error() {
|
||||
let inp = &[
|
||||
r#"let-env config = {
|
||||
hooks: {
|
||||
env_change: {
|
||||
FOO : {
|
||||
code: "def foo { 'foo' }"
|
||||
}
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
"let-env FOO = 1",
|
||||
"",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert!(actual_repl.err.contains("UnsupportedConfigValue"));
|
||||
assert_eq!(actual_repl.out, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn err_hook_dont_allow_string() {
|
||||
let inp = &[
|
||||
&pre_prompt_hook(r#"'def foo [] { "got foo!" }'"#),
|
||||
"",
|
||||
"foo",
|
||||
];
|
||||
|
||||
let actual_repl = nu_repl("tests/hooks", inp);
|
||||
|
||||
assert!(actual_repl.out.is_empty());
|
||||
assert!(actual_repl.err.contains("UnsupportedConfigValue"));
|
||||
}
|
1
tests/hooks/samples/.nu-env
Normal file
1
tests/hooks/samples/.nu-env
Normal file
|
@ -0,0 +1 @@
|
|||
load-env { SPAM: "spam" }
|
|
@ -1,5 +1,6 @@
|
|||
extern crate nu_test_support;
|
||||
|
||||
mod hooks;
|
||||
mod nu_repl;
|
||||
mod overlays;
|
||||
mod parsing;
|
||||
|
|
|
@ -1,15 +1,23 @@
|
|||
use nu_cli::{eval_env_change_hook, eval_hook};
|
||||
use nu_command::create_default_context;
|
||||
use nu_engine::eval_block;
|
||||
use nu_parser::parse;
|
||||
use nu_protocol::engine::{Stack, StateDelta, StateWorkingSet};
|
||||
use nu_protocol::{PipelineData, Span, Value};
|
||||
use nu_protocol::engine::{EngineState, Stack, StateDelta, StateWorkingSet};
|
||||
use nu_protocol::{CliError, PipelineData, Span, Value};
|
||||
use nu_test_support::fs::in_directory;
|
||||
use nu_test_support::Outcome;
|
||||
|
||||
fn outcome_err(msg: String) -> Outcome {
|
||||
fn outcome_err(
|
||||
engine_state: &EngineState,
|
||||
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
|
||||
) -> Outcome {
|
||||
let working_set = StateWorkingSet::new(&engine_state);
|
||||
|
||||
eprintln!("{}", format!("Error: {:?}", CliError(error, &working_set)));
|
||||
|
||||
Outcome {
|
||||
out: String::new(),
|
||||
err: msg,
|
||||
err: format!("{:?}", error),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,12 +44,39 @@ pub fn nu_repl(cwd: &str, source_lines: &[&str]) -> Outcome {
|
|||
|
||||
let delta = StateDelta::new(&engine_state);
|
||||
if let Err(err) = engine_state.merge_delta(delta, Some(&mut stack), cwd) {
|
||||
return outcome_err(format!("{:?}", &err));
|
||||
return outcome_err(&engine_state, &err);
|
||||
}
|
||||
|
||||
let mut last_output = String::new();
|
||||
|
||||
for (i, line) in source_lines.iter().enumerate() {
|
||||
// Check for pre_prompt hook
|
||||
let config = engine_state.get_config();
|
||||
if let Some(hook) = config.hooks.pre_prompt.clone() {
|
||||
if let Err(err) = eval_hook(&mut engine_state, &mut stack, vec![], &hook) {
|
||||
return outcome_err(&engine_state, &err);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for env change hook
|
||||
let config = engine_state.get_config();
|
||||
if let Err(err) = eval_env_change_hook(
|
||||
config.hooks.env_change.clone(),
|
||||
&mut engine_state,
|
||||
&mut stack,
|
||||
) {
|
||||
return outcome_err(&engine_state, &err);
|
||||
}
|
||||
|
||||
// Check for pre_execution hook
|
||||
let config = engine_state.get_config();
|
||||
if let Some(hook) = config.hooks.pre_execution.clone() {
|
||||
if let Err(err) = eval_hook(&mut engine_state, &mut stack, vec![], &hook) {
|
||||
return outcome_err(&engine_state, &err);
|
||||
}
|
||||
}
|
||||
|
||||
// Eval the REPL line
|
||||
let (block, delta) = {
|
||||
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||
let (block, err) = parse(
|
||||
|
@ -53,7 +88,7 @@ pub fn nu_repl(cwd: &str, source_lines: &[&str]) -> Outcome {
|
|||
);
|
||||
|
||||
if let Some(err) = err {
|
||||
return outcome_err(format!("{:?}", err));
|
||||
return outcome_err(&engine_state, &err);
|
||||
}
|
||||
(block, working_set.render())
|
||||
};
|
||||
|
@ -61,12 +96,12 @@ pub fn nu_repl(cwd: &str, source_lines: &[&str]) -> Outcome {
|
|||
let cwd = match nu_engine::env::current_dir(&engine_state, &stack) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return outcome_err(format!("{:?}", &e));
|
||||
return outcome_err(&engine_state, &e);
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = engine_state.merge_delta(delta, Some(&mut stack), &cwd) {
|
||||
return outcome_err(format!("{:?}", err));
|
||||
return outcome_err(&engine_state, &err);
|
||||
}
|
||||
|
||||
let input = PipelineData::new(Span::test_data());
|
||||
|
@ -75,17 +110,15 @@ pub fn nu_repl(cwd: &str, source_lines: &[&str]) -> Outcome {
|
|||
match eval_block(&engine_state, &mut stack, &block, input, false, false) {
|
||||
Ok(pipeline_data) => match pipeline_data.collect_string("", config) {
|
||||
Ok(s) => last_output = s,
|
||||
Err(err) => return outcome_err(format!("{:?}", err)),
|
||||
Err(err) => return outcome_err(&engine_state, &err),
|
||||
},
|
||||
Err(err) => return outcome_err(format!("{:?}", err)),
|
||||
Err(err) => return outcome_err(&engine_state, &err),
|
||||
}
|
||||
|
||||
// FIXME: permanent state changes like this hopefully in time can be removed
|
||||
// and be replaced by just passing the cwd in where needed
|
||||
if let Some(cwd) = stack.get_env_var(&engine_state, "PWD") {
|
||||
let path = match cwd.as_string() {
|
||||
Ok(p) => p,
|
||||
Err(err) => return outcome_err(format!("{:?}", err)),
|
||||
Err(err) => return outcome_err(&engine_state, &err),
|
||||
};
|
||||
let _ = std::env::set_current_dir(path);
|
||||
engine_state.add_env_var("PWD".into(), cwd);
|
||||
|
|
Loading…
Reference in a new issue