1use miette::Result;
2use nu_engine::{eval_block, eval_block_with_early_return, redirect_env};
3use nu_parser::parse;
4use nu_protocol::{
5 PipelineData, PositionalArg, ShellError, Span, Type, Value, VarId,
6 debugger::WithoutDebug,
7 engine::{Closure, EngineState, EnvName, Stack, StateWorkingSet},
8 report_error::{report_parse_error, report_shell_error},
9 shell_error::generic::GenericError,
10};
11use std::{collections::HashMap, sync::Arc};
12
13pub fn eval_env_change_hook(
14 env_change_hook: &HashMap<EnvName, Vec<Value>>,
15 engine_state: &mut EngineState,
16 stack: &mut Stack,
17) -> Result<(), ShellError> {
18 for (env, hooks) in env_change_hook {
19 let before = engine_state.previous_env_vars.get(env);
20 let after = stack.get_env_var(engine_state, env.as_str());
21 if before != after {
22 let before = before.cloned().unwrap_or_default();
23 let after = after.cloned().unwrap_or_default();
24
25 eval_hooks(
26 engine_state,
27 stack,
28 vec![("$before".into(), before), ("$after".into(), after.clone())],
29 hooks,
30 "env_change",
31 )?;
32
33 Arc::make_mut(&mut engine_state.previous_env_vars).insert(env.clone(), after);
34 }
35 }
36
37 Ok(())
38}
39
40pub fn eval_pre_prompt_hooks(
41 engine_state: &mut EngineState,
42 stack: &mut Stack,
43) -> Result<(), ShellError> {
44 let hook = engine_state.get_config().hooks.pre_prompt.clone();
45 eval_hooks(engine_state, stack, vec![], &hook, "pre_prompt")
46}
47
48pub fn eval_pre_execution_hooks(
49 engine_state: &mut EngineState,
50 stack: &mut Stack,
51 commandline: impl Into<String>,
52) -> Result<(), ShellError> {
53 engine_state
54 .repl_state
55 .lock()
56 .expect("repl state mutex")
57 .buffer = commandline.into();
58
59 let hook = engine_state.get_config().hooks.pre_execution.clone();
60 eval_hooks(engine_state, stack, vec![], &hook, "pre_execution")
61}
62
63pub fn eval_repl_hooks(
64 engine_state: &mut EngineState,
65 stack: &mut Stack,
66 commandline: impl Into<String>,
67) -> Result<(), ShellError> {
68 engine_state.merge_env(stack)?;
69
70 eval_pre_prompt_hooks(engine_state, stack)?;
71
72 let hook = engine_state.get_config().hooks.env_change.clone();
73 eval_env_change_hook(&hook, engine_state, stack)?;
74
75 eval_pre_execution_hooks(engine_state, stack, commandline)
76}
77
78pub fn eval_hooks(
79 engine_state: &mut EngineState,
80 stack: &mut Stack,
81 arguments: Vec<(String, Value)>,
82 hooks: &[Value],
83 hook_name: &str,
84) -> Result<(), ShellError> {
85 for hook in hooks {
86 eval_hook(
87 engine_state,
88 stack,
89 None,
90 arguments.clone(),
91 hook,
92 &format!("{hook_name} list, recursive"),
93 )?;
94 }
95 Ok(())
96}
97
98pub fn eval_hook(
99 engine_state: &mut EngineState,
100 stack: &mut Stack,
101 input: Option<PipelineData>,
102 arguments: Vec<(String, Value)>,
103 value: &Value,
104 hook_name: &str,
105) -> Result<PipelineData, ShellError> {
106 let mut output = PipelineData::empty();
107
108 let span = value.span();
109 match value {
110 Value::String { val, .. } => {
111 let (block, delta, vars) = {
112 let mut working_set = StateWorkingSet::new(engine_state);
113
114 let mut vars: Vec<(VarId, Value)> = vec![];
115
116 for (name, val) in arguments {
117 let var_id = working_set.add_variable(
118 name.as_bytes().to_vec(),
119 val.span(),
120 Type::Any,
121 false,
122 );
123 vars.push((var_id, val));
124 }
125
126 let output = parse(
127 &mut working_set,
128 Some(&format!("{hook_name} hook")),
129 val.as_bytes(),
130 false,
131 );
132 if let Some(err) = working_set.parse_errors.first() {
133 report_parse_error(Some(stack), &working_set, err);
134 return Err(ShellError::Generic(GenericError::new(
135 format!("Failed to run {hook_name} hook"),
136 "source code has errors",
137 span,
138 )));
139 }
140
141 (output, working_set.render(), vars)
142 };
143
144 engine_state.merge_delta(delta)?;
145 let input = if let Some(input) = input {
146 input
147 } else {
148 PipelineData::empty()
149 };
150
151 let var_ids: Vec<VarId> = vars
152 .into_iter()
153 .map(|(var_id, val)| {
154 stack.add_var(var_id, val);
155 var_id
156 })
157 .collect();
158
159 match eval_block::<WithoutDebug>(engine_state, stack, &block, input).map(|p| p.body) {
160 Ok(pipeline_data) => {
161 output = pipeline_data;
162 }
163 Err(err) => {
164 report_shell_error(Some(stack), engine_state, &err);
165 }
166 }
167
168 for var_id in var_ids.iter() {
169 stack.remove_var(*var_id);
170 }
171 }
172 Value::List { vals, .. } => {
173 eval_hooks(engine_state, stack, arguments, vals, hook_name)?;
174 }
175 Value::Record { val, .. } => {
176 let do_run_hook = if let Some(condition) = val.get("condition") {
184 let other_span = condition.span();
185 if let Ok(closure) = condition.as_closure() {
186 match run_hook(
187 engine_state,
188 stack,
189 closure,
190 None,
191 arguments.clone(),
192 other_span,
193 ) {
194 Ok(pipeline_data) => {
195 if let PipelineData::Value(Value::Bool { val, .. }, ..) = pipeline_data
196 {
197 val
198 } else {
199 return Err(ShellError::RuntimeTypeMismatch {
200 expected: Type::Bool,
201 actual: pipeline_data.get_type(),
202 span: pipeline_data.span().unwrap_or(other_span),
203 });
204 }
205 }
206 Err(err) => {
207 return Err(err);
208 }
209 }
210 } else {
211 return Err(ShellError::RuntimeTypeMismatch {
212 expected: Type::Closure,
213 actual: condition.get_type(),
214 span: other_span,
215 });
216 }
217 } else {
218 true
220 };
221
222 if do_run_hook {
223 let Some(follow) = val.get("code") else {
224 return Err(ShellError::CantFindColumn {
225 col_name: "code".into(),
226 span: Some(span),
227 src_span: span,
228 });
229 };
230 let source_span = follow.span();
231 match follow {
232 Value::String { val, .. } => {
233 let (block, delta, vars) = {
234 let mut working_set = StateWorkingSet::new(engine_state);
235
236 let mut vars: Vec<(VarId, Value)> = vec![];
237
238 for (name, val) in arguments {
239 let var_id = working_set.add_variable(
240 name.as_bytes().to_vec(),
241 val.span(),
242 Type::Any,
243 false,
244 );
245 vars.push((var_id, val));
246 }
247
248 let output = parse(
249 &mut working_set,
250 Some(&format!("{hook_name} hook")),
251 val.as_bytes(),
252 false,
253 );
254 if let Some(err) = working_set.parse_errors.first() {
255 report_parse_error(Some(stack), &working_set, err);
256 return Err(ShellError::Generic(GenericError::new(
257 format!("Failed to run {hook_name} hook"),
258 "source code has errors",
259 span,
260 )));
261 }
262
263 (output, working_set.render(), vars)
264 };
265
266 engine_state.merge_delta(delta)?;
267 let input = PipelineData::empty();
268
269 let var_ids: Vec<VarId> = vars
270 .into_iter()
271 .map(|(var_id, val)| {
272 stack.add_var(var_id, val);
273 var_id
274 })
275 .collect();
276
277 match eval_block::<WithoutDebug>(engine_state, stack, &block, input)
278 .map(|p| p.body)
279 {
280 Ok(pipeline_data) => {
281 output = pipeline_data;
282 }
283 Err(err) => {
284 report_shell_error(Some(stack), engine_state, &err);
285 }
286 }
287
288 for var_id in var_ids.iter() {
289 stack.remove_var(*var_id);
290 }
291 }
292 Value::Closure { val, .. } => {
293 run_hook(engine_state, stack, val, input, arguments, source_span)?;
294 }
295 other => {
296 return Err(ShellError::RuntimeTypeMismatch {
297 expected: Type::custom("string or closure"),
298 actual: other.get_type(),
299 span: source_span,
300 });
301 }
302 }
303 }
304 }
305 Value::Closure { val, .. } => {
306 output = run_hook(engine_state, stack, val, input, arguments, span)?;
307 }
308 other => {
309 return Err(ShellError::RuntimeTypeMismatch {
310 expected: Type::custom("string, closure, record, or list"),
311 actual: other.get_type(),
312 span: other.span(),
313 });
314 }
315 }
316
317 engine_state.merge_env(stack)?;
318
319 Ok(output)
320}
321
322fn run_hook(
323 engine_state: &EngineState,
324 stack: &mut Stack,
325 closure: &Closure,
326 optional_input: Option<PipelineData>,
327 arguments: Vec<(String, Value)>,
328 span: Span,
329) -> Result<PipelineData, ShellError> {
330 let block = engine_state.get_block(closure.block_id);
331
332 let input = optional_input.unwrap_or_else(PipelineData::empty);
333
334 let mut callee_stack = stack
335 .captures_to_stack_preserve_out_dest(closure.captures.clone())
336 .reset_pipes();
337
338 for (idx, PositionalArg { var_id, .. }) in
339 block.signature.required_positional.iter().enumerate()
340 {
341 if let Some(var_id) = var_id {
342 if let Some(arg) = arguments.get(idx) {
343 callee_stack.add_var(*var_id, arg.1.clone())
344 } else {
345 return Err(ShellError::IncompatibleParametersSingle {
346 msg: "This hook block has too many parameters".into(),
347 span,
348 });
349 }
350 }
351 }
352
353 let pipeline_data = eval_block_with_early_return::<WithoutDebug>(
354 engine_state,
355 &mut callee_stack,
356 block,
357 input,
358 )?
359 .body;
360
361 if let PipelineData::Value(Value::Error { error, .. }, _) = pipeline_data {
362 return Err(*error);
363 }
364
365 redirect_env(engine_state, stack, &callee_stack);
367
368 Ok(pipeline_data)
369}