Make plugins able to find and call other commands (#13407)

# Description

Adds functionality to the plugin interface to support calling internal
commands from plugins. For example, using `view ir --json`:

```rust
let closure: Value = call.req(0)?;

let Some(decl_id) = engine.find_decl("view ir")? else {
    return Err(LabeledError::new("`view ir` not found"));
};

let ir_json = engine.call_decl(
    decl_id,
    EvaluatedCall::new(call.head)
        .with_named("json".into_spanned(call.head), Value::bool(true, call.head))
        .with_positional(closure),
    PipelineData::Empty,
    true,
    false,
)?.into_value()?.into_string()?;

let ir = serde_json::from_value(&ir_json);

// ...
```

# User-Facing Changes

Plugin developers can now use `EngineInterface::find_decl()` and
`call_decl()` to call internal commands, which could be handy for
formatters like `to csv` or `to nuon`, or for reflection commands that
help gain insight into the engine.

# Tests + Formatting
- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

# After Submitting
- [ ] release notes
- [ ] update plugin protocol documentation: `FindDecl`, `CallDecl`
engine calls; `Identifier` engine call response
This commit is contained in:
Devyn Cairns
2024-07-18 22:54:21 -07:00
committed by GitHub
parent c19944f291
commit f3843a6176
11 changed files with 418 additions and 21 deletions

View File

@ -1,9 +1,10 @@
use crate::util::MutableCow;
use nu_engine::{get_eval_block_with_early_return, get_full_help, ClosureEvalOnce};
use nu_plugin_protocol::EvaluatedCall;
use nu_protocol::{
engine::{Call, Closure, EngineState, Redirection, Stack},
Config, IntoSpanned, OutDest, PipelineData, PluginIdentity, ShellError, Signals, Span, Spanned,
Value,
ir, Config, DeclId, IntoSpanned, OutDest, PipelineData, PluginIdentity, ShellError, Signals,
Span, Spanned, Value,
};
use std::{
borrow::Cow,
@ -44,6 +45,17 @@ pub trait PluginExecutionContext: Send + Sync {
redirect_stdout: bool,
redirect_stderr: bool,
) -> Result<PipelineData, ShellError>;
/// Find a declaration by name
fn find_decl(&self, name: &str) -> Result<Option<DeclId>, ShellError>;
/// Call a declaration with arguments and input
fn call_decl(
&mut self,
decl_id: DeclId,
call: EvaluatedCall,
input: PipelineData,
redirect_stdout: bool,
redirect_stderr: bool,
) -> Result<PipelineData, ShellError>;
/// Create an owned version of the context with `'static` lifetime
fn boxed(&self) -> Box<dyn PluginExecutionContext>;
}
@ -177,19 +189,10 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> {
.captures_to_stack(closure.item.captures)
.reset_pipes();
let stdout = if redirect_stdout {
Some(Redirection::Pipe(OutDest::Capture))
} else {
None
};
let stderr = if redirect_stderr {
Some(Redirection::Pipe(OutDest::Capture))
} else {
None
};
let stack = &mut stack.push_redirection(stdout, stderr);
let stack = &mut stack.push_redirection(
redirect_stdout.then_some(Redirection::Pipe(OutDest::Capture)),
redirect_stderr.then_some(Redirection::Pipe(OutDest::Capture)),
);
// Set up the positional arguments
for (idx, value) in positional.into_iter().enumerate() {
@ -211,6 +214,57 @@ impl<'a> PluginExecutionContext for PluginExecutionCommandContext<'a> {
eval_block_with_early_return(&self.engine_state, stack, block, input)
}
fn find_decl(&self, name: &str) -> Result<Option<DeclId>, ShellError> {
Ok(self.engine_state.find_decl(name.as_bytes(), &[]))
}
fn call_decl(
&mut self,
decl_id: DeclId,
call: EvaluatedCall,
input: PipelineData,
redirect_stdout: bool,
redirect_stderr: bool,
) -> Result<PipelineData, ShellError> {
if decl_id >= self.engine_state.num_decls() {
return Err(ShellError::GenericError {
error: "Plugin misbehaving".into(),
msg: format!("Tried to call unknown decl id: {}", decl_id),
span: Some(call.head),
help: None,
inner: vec![],
});
}
let decl = self.engine_state.get_decl(decl_id);
let stack = &mut self.stack.push_redirection(
redirect_stdout.then_some(Redirection::Pipe(OutDest::Capture)),
redirect_stderr.then_some(Redirection::Pipe(OutDest::Capture)),
);
let mut call_builder = ir::Call::build(decl_id, call.head);
for positional in call.positional {
call_builder.add_positional(stack, positional.span(), positional);
}
for (name, value) in call.named {
if let Some(value) = value {
call_builder.add_named(stack, &name.item, "", name.span, value);
} else {
call_builder.add_flag(stack, &name.item, "", name.span);
}
}
decl.run(
&self.engine_state,
stack,
&(&call_builder.finish()).into(),
input,
)
}
fn boxed(&self) -> Box<dyn PluginExecutionContext + 'static> {
Box::new(PluginExecutionCommandContext {
identity: self.identity.clone(),
@ -298,6 +352,25 @@ impl PluginExecutionContext for PluginExecutionBogusContext {
})
}
fn find_decl(&self, _name: &str) -> Result<Option<DeclId>, ShellError> {
Err(ShellError::NushellFailed {
msg: "find_decl not implemented on bogus".into(),
})
}
fn call_decl(
&mut self,
_decl_id: DeclId,
_call: EvaluatedCall,
_input: PipelineData,
_redirect_stdout: bool,
_redirect_stderr: bool,
) -> Result<PipelineData, ShellError> {
Err(ShellError::NushellFailed {
msg: "call_decl not implemented on bogus".into(),
})
}
fn boxed(&self) -> Box<dyn PluginExecutionContext + 'static> {
Box::new(PluginExecutionBogusContext)
}