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