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) {
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 Ok(pipeline_data) => {
244 output = pipeline_data;
245 }
246 Err(err) => {
247 report_shell_error(engine_state, &err);
248 }
249 }
250
251 for var_id in var_ids.iter() {
252 stack.remove_var(*var_id);
253 }
254 }
255 Value::Closure { val, .. } => {
256 run_hook(engine_state, stack, val, input, arguments, source_span)?;
257 }
258 other => {
259 return Err(ShellError::RuntimeTypeMismatch {
260 expected: Type::custom("string or closure"),
261 actual: other.get_type(),
262 span: source_span,
263 });
264 }
265 }
266 }
267 }
268 Value::Closure { val, .. } => {
269 output = run_hook(engine_state, stack, val, input, arguments, span)?;
270 }
271 other => {
272 return Err(ShellError::RuntimeTypeMismatch {
273 expected: Type::custom("string, closure, record, or list"),
274 actual: other.get_type(),
275 span: other.span(),
276 });
277 }
278 }
279
280 engine_state.merge_env(stack)?;
281
282 Ok(output)
283}
284
285fn run_hook(
286 engine_state: &EngineState,
287 stack: &mut Stack,
288 closure: &Closure,
289 optional_input: Option<PipelineData>,
290 arguments: Vec<(String, Value)>,
291 span: Span,
292) -> Result<PipelineData, ShellError> {
293 let block = engine_state.get_block(closure.block_id);
294
295 let input = optional_input.unwrap_or_else(PipelineData::empty);
296
297 let mut callee_stack = stack
298 .captures_to_stack_preserve_out_dest(closure.captures.clone())
299 .reset_pipes();
300
301 for (idx, PositionalArg { var_id, .. }) in
302 block.signature.required_positional.iter().enumerate()
303 {
304 if let Some(var_id) = var_id {
305 if let Some(arg) = arguments.get(idx) {
306 callee_stack.add_var(*var_id, arg.1.clone())
307 } else {
308 return Err(ShellError::IncompatibleParametersSingle {
309 msg: "This hook block has too many parameters".into(),
310 span,
311 });
312 }
313 }
314 }
315
316 let pipeline_data = eval_block_with_early_return::<WithoutDebug>(
317 engine_state,
318 &mut callee_stack,
319 block,
320 input,
321 )?;
322
323 if let PipelineData::Value(Value::Error { error, .. }, _) = pipeline_data {
324 return Err(*error);
325 }
326
327 redirect_env(engine_state, stack, &callee_stack);
329
330 Ok(pipeline_data)
331}