Skip to main content

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, 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            // Hooks can optionally be a record in this form:
177            // {
178            //     condition: {|before, after| ... }  # block that evaluates to true/false
179            //     code: # block or a string
180            // }
181            // The condition block will be run to check whether the main hook (in `code`) should be run.
182            // If it returns true (the default if a condition block is not specified), the hook should be run.
183            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                // always run the hook
219                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    // If all went fine, preserve the environment of the called block
366    redirect_env(engine_state, stack, &callee_stack);
367
368    Ok(pipeline_data)
369}