nu_plugin_engine/
context.rs

1use crate::util::MutableCow;
2use nu_engine::{ClosureEvalOnce, get_eval_block_with_early_return, get_full_help};
3use nu_plugin_protocol::EvaluatedCall;
4use nu_protocol::{
5    BlockId, Config, DeclId, IntoSpanned, OutDest, PipelineData, PluginIdentity, ShellError,
6    Signals, Span, Spanned, Value,
7    engine::{Call, Closure, EngineState, Redirection, Stack},
8    ir::{self, IrBlock},
9};
10use std::{
11    borrow::Cow,
12    collections::HashMap,
13    sync::{Arc, atomic::AtomicU32},
14};
15
16/// Object safe trait for abstracting operations required of the plugin context.
17pub trait PluginExecutionContext: Send + Sync {
18    /// A span pointing to the command being executed
19    fn span(&self) -> Span;
20    /// The [`Signals`] struct, if present
21    fn signals(&self) -> &Signals;
22    /// The pipeline externals state, for tracking the foreground process group, if present
23    fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>>;
24    /// Get engine configuration
25    fn get_config(&self) -> Result<Arc<Config>, ShellError>;
26    /// Get plugin configuration
27    fn get_plugin_config(&self) -> Result<Option<Value>, ShellError>;
28    /// Get an environment variable from `$env`
29    fn get_env_var(&self, name: &str) -> Result<Option<&Value>, ShellError>;
30    /// Get all environment variables
31    fn get_env_vars(&self) -> Result<HashMap<String, Value>, ShellError>;
32    /// Get current working directory
33    fn get_current_dir(&self) -> Result<Spanned<String>, ShellError>;
34    /// Set an environment variable
35    fn add_env_var(&mut self, name: String, value: Value) -> Result<(), ShellError>;
36    /// Get help for the current command
37    fn get_help(&self) -> Result<Spanned<String>, ShellError>;
38    /// Get the contents of a [`Span`]
39    fn get_span_contents(&self, span: Span) -> Result<Spanned<Vec<u8>>, ShellError>;
40    /// Evaluate a closure passed to the plugin
41    fn eval_closure(
42        &self,
43        closure: Spanned<Closure>,
44        positional: Vec<Value>,
45        input: PipelineData,
46        redirect_stdout: bool,
47        redirect_stderr: bool,
48    ) -> Result<PipelineData, ShellError>;
49    /// Find a declaration by name
50    fn find_decl(&self, name: &str) -> Result<Option<DeclId>, ShellError>;
51    /// Get the compiled IR for a block
52    fn get_block_ir(&self, block_id: BlockId) -> Result<IrBlock, ShellError>;
53    /// Call a declaration with arguments and input
54    fn call_decl(
55        &mut self,
56        decl_id: DeclId,
57        call: EvaluatedCall,
58        input: PipelineData,
59        redirect_stdout: bool,
60        redirect_stderr: bool,
61    ) -> Result<PipelineData, ShellError>;
62    /// Create an owned version of the context with `'static` lifetime
63    fn boxed(&self) -> Box<dyn PluginExecutionContext>;
64}
65
66/// The execution context of a plugin command. Can be borrowed.
67pub struct PluginExecutionCommandContext<'a> {
68    identity: Arc<PluginIdentity>,
69    engine_state: Cow<'a, EngineState>,
70    stack: MutableCow<'a, Stack>,
71    call: Call<'a>,
72}
73
74impl<'a> PluginExecutionCommandContext<'a> {
75    pub fn new(
76        identity: Arc<PluginIdentity>,
77        engine_state: &'a EngineState,
78        stack: &'a mut Stack,
79        call: &'a Call<'a>,
80    ) -> PluginExecutionCommandContext<'a> {
81        PluginExecutionCommandContext {
82            identity,
83            engine_state: Cow::Borrowed(engine_state),
84            stack: MutableCow::Borrowed(stack),
85            call: call.clone(),
86        }
87    }
88}
89
90impl PluginExecutionContext for PluginExecutionCommandContext<'_> {
91    fn span(&self) -> Span {
92        self.call.head
93    }
94
95    fn signals(&self) -> &Signals {
96        self.engine_state.signals()
97    }
98
99    fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>> {
100        Some(&self.engine_state.pipeline_externals_state)
101    }
102
103    fn get_config(&self) -> Result<Arc<Config>, ShellError> {
104        Ok(self.stack.get_config(&self.engine_state))
105    }
106
107    fn get_plugin_config(&self) -> Result<Option<Value>, ShellError> {
108        // Fetch the configuration for a plugin
109        //
110        // The `plugin` must match the registered name of a plugin.  For `plugin add
111        // nu_plugin_example` the plugin config lookup uses `"example"`
112        Ok(self
113            .get_config()?
114            .plugins
115            .get(self.identity.name())
116            .cloned()
117            .map(|value| {
118                let span = value.span();
119                match value {
120                    Value::Closure { val, .. } => {
121                        ClosureEvalOnce::new(&self.engine_state, &self.stack, *val)
122                            .run_with_input(PipelineData::empty())
123                            .and_then(|data| data.into_value(span))
124                            .unwrap_or_else(|err| Value::error(err, self.call.head))
125                    }
126                    _ => value.clone(),
127                }
128            }))
129    }
130
131    fn get_env_var(&self, name: &str) -> Result<Option<&Value>, ShellError> {
132        Ok(self
133            .stack
134            .get_env_var_insensitive(&self.engine_state, name)
135            .map(|(_, value)| value))
136    }
137
138    fn get_env_vars(&self) -> Result<HashMap<String, Value>, ShellError> {
139        Ok(self.stack.get_env_vars(&self.engine_state))
140    }
141
142    fn get_current_dir(&self) -> Result<Spanned<String>, ShellError> {
143        let cwd = self.engine_state.cwd_as_string(Some(&self.stack))?;
144        // The span is not really used, so just give it call.head
145        Ok(cwd.into_spanned(self.call.head))
146    }
147
148    fn add_env_var(&mut self, name: String, value: Value) -> Result<(), ShellError> {
149        self.stack.add_env_var(name, value);
150        Ok(())
151    }
152
153    fn get_help(&self) -> Result<Spanned<String>, ShellError> {
154        let decl = self.engine_state.get_decl(self.call.decl_id);
155
156        Ok(
157            get_full_help(decl, &self.engine_state, &mut self.stack.clone())
158                .into_spanned(self.call.head),
159        )
160    }
161
162    fn get_span_contents(&self, span: Span) -> Result<Spanned<Vec<u8>>, ShellError> {
163        Ok(self
164            .engine_state
165            .get_span_contents(span)
166            .to_vec()
167            .into_spanned(self.call.head))
168    }
169
170    fn eval_closure(
171        &self,
172        closure: Spanned<Closure>,
173        positional: Vec<Value>,
174        input: PipelineData,
175        redirect_stdout: bool,
176        redirect_stderr: bool,
177    ) -> Result<PipelineData, ShellError> {
178        let block = self
179            .engine_state
180            .try_get_block(closure.item.block_id)
181            .ok_or_else(|| ShellError::GenericError {
182                error: "Plugin misbehaving".into(),
183                msg: format!(
184                    "Tried to evaluate unknown block id: {}",
185                    closure.item.block_id.get()
186                ),
187                span: Some(closure.span),
188                help: None,
189                inner: vec![],
190            })?;
191
192        let mut stack = self
193            .stack
194            .captures_to_stack(closure.item.captures)
195            .reset_pipes();
196
197        let stack = &mut stack.push_redirection(
198            redirect_stdout.then_some(Redirection::Pipe(OutDest::PipeSeparate)),
199            redirect_stderr.then_some(Redirection::Pipe(OutDest::PipeSeparate)),
200        );
201
202        // Set up the positional arguments
203        for (idx, value) in positional.into_iter().enumerate() {
204            if let Some(arg) = block.signature.get_positional(idx) {
205                if let Some(var_id) = arg.var_id {
206                    stack.add_var(var_id, value);
207                } else {
208                    return Err(ShellError::NushellFailedSpanned {
209                        msg: "Error while evaluating closure from plugin".into(),
210                        label: "closure argument missing var_id".into(),
211                        span: closure.span,
212                    });
213                }
214            }
215        }
216
217        let eval_block_with_early_return = get_eval_block_with_early_return(&self.engine_state);
218
219        eval_block_with_early_return(&self.engine_state, stack, block, input).map(|p| p.body)
220    }
221
222    fn find_decl(&self, name: &str) -> Result<Option<DeclId>, ShellError> {
223        Ok(self.engine_state.find_decl(name.as_bytes(), &[]))
224    }
225
226    fn get_block_ir(&self, block_id: BlockId) -> Result<IrBlock, ShellError> {
227        let block =
228            self.engine_state
229                .try_get_block(block_id)
230                .ok_or_else(|| ShellError::GenericError {
231                    error: "Plugin misbehaving".into(),
232                    msg: format!("Tried to get IR for unknown block id: {}", block_id.get()),
233                    span: Some(self.call.head),
234                    help: None,
235                    inner: vec![],
236                })?;
237
238        block
239            .ir_block
240            .clone()
241            .ok_or_else(|| ShellError::GenericError {
242                error: "Block has no IR".into(),
243                msg: format!("Block {} was not compiled to IR", block_id.get()),
244                span: Some(self.call.head),
245                help: Some(
246                    "This block may be a declaration or built-in that has no IR representation"
247                        .into(),
248                ),
249                inner: vec![],
250            })
251    }
252
253    fn call_decl(
254        &mut self,
255        decl_id: DeclId,
256        call: EvaluatedCall,
257        input: PipelineData,
258        redirect_stdout: bool,
259        redirect_stderr: bool,
260    ) -> Result<PipelineData, ShellError> {
261        if decl_id.get() >= self.engine_state.num_decls() {
262            return Err(ShellError::GenericError {
263                error: "Plugin misbehaving".into(),
264                msg: format!("Tried to call unknown decl id: {}", decl_id.get()),
265                span: Some(call.head),
266                help: None,
267                inner: vec![],
268            });
269        }
270
271        let decl = self.engine_state.get_decl(decl_id);
272
273        let stack = &mut self.stack.push_redirection(
274            redirect_stdout.then_some(Redirection::Pipe(OutDest::PipeSeparate)),
275            redirect_stderr.then_some(Redirection::Pipe(OutDest::PipeSeparate)),
276        );
277
278        let mut call_builder = ir::Call::build(decl_id, call.head);
279
280        for positional in call.positional {
281            call_builder.add_positional(stack, positional.span(), positional);
282        }
283
284        for (name, value) in call.named {
285            if let Some(value) = value {
286                call_builder.add_named(stack, &name.item, "", name.span, value);
287            } else {
288                call_builder.add_flag(stack, &name.item, "", name.span);
289            }
290        }
291
292        call_builder.with(stack, |stack, call| {
293            decl.run(&self.engine_state, stack, call, input)
294        })
295    }
296
297    fn boxed(&self) -> Box<dyn PluginExecutionContext + 'static> {
298        Box::new(PluginExecutionCommandContext {
299            identity: self.identity.clone(),
300            engine_state: Cow::Owned(self.engine_state.clone().into_owned()),
301            stack: self.stack.owned(),
302            call: self.call.to_owned(),
303        })
304    }
305}
306
307/// A bogus execution context for testing that doesn't really implement anything properly
308#[cfg(test)]
309pub(crate) struct PluginExecutionBogusContext;
310
311#[cfg(test)]
312impl PluginExecutionContext for PluginExecutionBogusContext {
313    fn span(&self) -> Span {
314        Span::test_data()
315    }
316
317    fn signals(&self) -> &Signals {
318        &Signals::EMPTY
319    }
320
321    fn pipeline_externals_state(&self) -> Option<&Arc<(AtomicU32, AtomicU32)>> {
322        None
323    }
324
325    fn get_config(&self) -> Result<Arc<Config>, ShellError> {
326        Err(ShellError::NushellFailed {
327            msg: "get_config not implemented on bogus".into(),
328        })
329    }
330
331    fn get_plugin_config(&self) -> Result<Option<Value>, ShellError> {
332        Ok(None)
333    }
334
335    fn get_env_var(&self, _name: &str) -> Result<Option<&Value>, ShellError> {
336        Err(ShellError::NushellFailed {
337            msg: "get_env_var not implemented on bogus".into(),
338        })
339    }
340
341    fn get_env_vars(&self) -> Result<HashMap<String, Value>, ShellError> {
342        Err(ShellError::NushellFailed {
343            msg: "get_env_vars not implemented on bogus".into(),
344        })
345    }
346
347    fn get_current_dir(&self) -> Result<Spanned<String>, ShellError> {
348        Err(ShellError::NushellFailed {
349            msg: "get_current_dir not implemented on bogus".into(),
350        })
351    }
352
353    fn add_env_var(&mut self, _name: String, _value: Value) -> Result<(), ShellError> {
354        Err(ShellError::NushellFailed {
355            msg: "add_env_var not implemented on bogus".into(),
356        })
357    }
358
359    fn get_help(&self) -> Result<Spanned<String>, ShellError> {
360        Err(ShellError::NushellFailed {
361            msg: "get_help not implemented on bogus".into(),
362        })
363    }
364
365    fn get_span_contents(&self, _span: Span) -> Result<Spanned<Vec<u8>>, ShellError> {
366        Err(ShellError::NushellFailed {
367            msg: "get_span_contents not implemented on bogus".into(),
368        })
369    }
370
371    fn eval_closure(
372        &self,
373        _closure: Spanned<Closure>,
374        _positional: Vec<Value>,
375        _input: PipelineData,
376        _redirect_stdout: bool,
377        _redirect_stderr: bool,
378    ) -> Result<PipelineData, ShellError> {
379        Err(ShellError::NushellFailed {
380            msg: "eval_closure not implemented on bogus".into(),
381        })
382    }
383
384    fn find_decl(&self, _name: &str) -> Result<Option<DeclId>, ShellError> {
385        Err(ShellError::NushellFailed {
386            msg: "find_decl not implemented on bogus".into(),
387        })
388    }
389
390    fn get_block_ir(&self, _block_id: BlockId) -> Result<IrBlock, ShellError> {
391        Err(ShellError::NushellFailed {
392            msg: "get_block_ir not implemented on bogus".into(),
393        })
394    }
395
396    fn call_decl(
397        &mut self,
398        _decl_id: DeclId,
399        _call: EvaluatedCall,
400        _input: PipelineData,
401        _redirect_stdout: bool,
402        _redirect_stderr: bool,
403    ) -> Result<PipelineData, ShellError> {
404        Err(ShellError::NushellFailed {
405            msg: "call_decl not implemented on bogus".into(),
406        })
407    }
408
409    fn boxed(&self) -> Box<dyn PluginExecutionContext + 'static> {
410        Box::new(PluginExecutionBogusContext)
411    }
412}