nu_cmd_base/
hook.rs

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            // Hooks can optionally be a record in this form:
140            // {
141            //     condition: {|before, after| ... }  # block that evaluates to true/false
142            //     code: # block or a string
143            // }
144            // The condition block will be run to check whether the main hook (in `code`) should be run.
145            // If it returns true (the default if a condition block is not specified), the hook should be run.
146            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                // always run the hook
182                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    // If all went fine, preserve the environment of the called block
328    redirect_env(engine_state, stack, &callee_stack);
329
330    Ok(pipeline_data)
331}