mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 10:36:00 +02:00
Reorganize plugin API around commands (#12170)
[Context on Discord](https://discord.com/channels/601130461678272522/855947301380947968/1216517833312309419) # Description This is a significant breaking change to the plugin API, but one I think is worthwhile. @ayax79 mentioned on Discord that while trying to start on a dataframes plugin, he was a little disappointed that more wasn't provided in terms of code organization for commands, particularly since there are *a lot* of `dfr` commands. This change treats plugins more like miniatures of the engine, with dispatch of the command name being handled inherently, each command being its own type, and each having their own signature within the trait impl for the command type rather than having to find a way to centralize it all into one `Vec`. For the example plugins that have multiple commands, I definitely like how this looks a lot better. This encourages doing code organization the right way and feels very good. For the plugins that have only one command, it's just a little bit more boilerplate - but still worth it, in my opinion. The `Box<dyn PluginCommand<Plugin = Self>>` type in `commands()` is a little bit hairy, particularly for Rust beginners, but ultimately not so bad, and it gives the desired flexibility for shared state for a whole plugin + the individual commands. # User-Facing Changes Pretty big breaking change to plugin API, but probably one that's worth making. ```rust use nu_plugin::*; use nu_protocol::{PluginSignature, PipelineData, Type, Value}; struct LowercasePlugin; struct Lowercase; // Plugins can now have multiple commands impl PluginCommand for Lowercase { type Plugin = LowercasePlugin; // The signature lives with the command fn signature(&self) -> PluginSignature { PluginSignature::build("lowercase") .usage("Convert each string in a stream to lowercase") .input_output_type(Type::List(Type::String.into()), Type::List(Type::String.into())) } // We also provide SimplePluginCommand which operates on Value like before fn run( &self, plugin: &LowercasePlugin, engine: &EngineInterface, call: &EvaluatedCall, input: PipelineData, ) -> Result<PipelineData, LabeledError> { let span = call.head; Ok(input.map(move |value| { value.as_str() .map(|string| Value::string(string.to_lowercase(), span)) // Errors in a stream should be returned as values. .unwrap_or_else(|err| Value::error(err, span)) }, None)?) } } // Plugin now just has a list of commands, and the custom value op stuff still goes here impl Plugin for LowercasePlugin { fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> { vec![Box::new(Lowercase)] } } fn main() { serve_plugin(&LowercasePlugin{}, MsgPackSerializer) } ``` Time this however you like - we're already breaking stuff for 0.92, so it might be good to do it now, but if it feels like a lot all at once, it could wait. # Tests + Formatting - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib` # After Submitting - [ ] Update examples in the book - [x] Fix #12088 to match - this change would actually simplify it a lot, because the methods are currently just duplicated between `Plugin` and `StreamingPlugin`, but they only need to be on `Plugin` with this change
This commit is contained in:
@ -16,18 +16,29 @@
|
||||
//! invoked by Nushell.
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use nu_plugin::{EvaluatedCall, LabeledError, MsgPackSerializer, Plugin, EngineInterface, serve_plugin};
|
||||
//! use nu_plugin::{EvaluatedCall, LabeledError, MsgPackSerializer, serve_plugin};
|
||||
//! use nu_plugin::{Plugin, PluginCommand, SimplePluginCommand, EngineInterface};
|
||||
//! use nu_protocol::{PluginSignature, Value};
|
||||
//!
|
||||
//! struct MyPlugin;
|
||||
//! struct MyCommand;
|
||||
//!
|
||||
//! impl Plugin for MyPlugin {
|
||||
//! fn signature(&self) -> Vec<PluginSignature> {
|
||||
//! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
|
||||
//! vec![Box::new(MyCommand)]
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl SimplePluginCommand for MyCommand {
|
||||
//! type Plugin = MyPlugin;
|
||||
//!
|
||||
//! fn signature(&self) -> PluginSignature {
|
||||
//! todo!();
|
||||
//! }
|
||||
//!
|
||||
//! fn run(
|
||||
//! &self,
|
||||
//! name: &str,
|
||||
//! plugin: &MyPlugin,
|
||||
//! engine: &EngineInterface,
|
||||
//! call: &EvaluatedCall,
|
||||
//! input: &Value
|
||||
@ -49,7 +60,9 @@ mod protocol;
|
||||
mod sequence;
|
||||
mod serializers;
|
||||
|
||||
pub use plugin::{serve_plugin, EngineInterface, Plugin, PluginEncoder, StreamingPlugin};
|
||||
pub use plugin::{
|
||||
serve_plugin, EngineInterface, Plugin, PluginCommand, PluginEncoder, SimplePluginCommand,
|
||||
};
|
||||
pub use protocol::{EvaluatedCall, LabeledError};
|
||||
pub use serializers::{json::JsonSerializer, msgpack::MsgPackSerializer};
|
||||
|
||||
|
206
crates/nu-plugin/src/plugin/command.rs
Normal file
206
crates/nu-plugin/src/plugin/command.rs
Normal file
@ -0,0 +1,206 @@
|
||||
use nu_protocol::{PipelineData, PluginSignature, Value};
|
||||
|
||||
use crate::{EngineInterface, EvaluatedCall, LabeledError, Plugin};
|
||||
|
||||
/// The API for a Nushell plugin command
|
||||
///
|
||||
/// This is the trait that Nushell plugin commands must implement. The methods defined on
|
||||
/// `PluginCommand` are invoked by [serve_plugin] during plugin registration and execution.
|
||||
///
|
||||
/// The plugin command must be able to be safely shared between threads, so that multiple
|
||||
/// invocations can be run in parallel. If interior mutability is desired, consider synchronization
|
||||
/// primitives such as [mutexes](std::sync::Mutex) and [channels](std::sync::mpsc).
|
||||
///
|
||||
/// This version of the trait expects stream input and output. If you have a simple plugin that just
|
||||
/// operates on plain values, consider using [`SimplePluginCommand`] instead.
|
||||
///
|
||||
/// # Examples
|
||||
/// Basic usage:
|
||||
/// ```
|
||||
/// # use nu_plugin::*;
|
||||
/// # use nu_protocol::{PluginSignature, PipelineData, Type, Value};
|
||||
/// struct LowercasePlugin;
|
||||
/// struct Lowercase;
|
||||
///
|
||||
/// impl PluginCommand for Lowercase {
|
||||
/// type Plugin = LowercasePlugin;
|
||||
///
|
||||
/// fn signature(&self) -> PluginSignature {
|
||||
/// PluginSignature::build("lowercase")
|
||||
/// .usage("Convert each string in a stream to lowercase")
|
||||
/// .input_output_type(Type::List(Type::String.into()), Type::List(Type::String.into()))
|
||||
/// }
|
||||
///
|
||||
/// fn run(
|
||||
/// &self,
|
||||
/// plugin: &LowercasePlugin,
|
||||
/// engine: &EngineInterface,
|
||||
/// call: &EvaluatedCall,
|
||||
/// input: PipelineData,
|
||||
/// ) -> Result<PipelineData, LabeledError> {
|
||||
/// let span = call.head;
|
||||
/// Ok(input.map(move |value| {
|
||||
/// value.as_str()
|
||||
/// .map(|string| Value::string(string.to_lowercase(), span))
|
||||
/// // Errors in a stream should be returned as values.
|
||||
/// .unwrap_or_else(|err| Value::error(err, span))
|
||||
/// }, None)?)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # impl Plugin for LowercasePlugin {
|
||||
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
|
||||
/// # vec![Box::new(Lowercase)]
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn main() {
|
||||
/// # serve_plugin(&LowercasePlugin{}, MsgPackSerializer)
|
||||
/// # }
|
||||
/// ```
|
||||
pub trait PluginCommand: Sync {
|
||||
/// The type of plugin this command runs on
|
||||
///
|
||||
/// Since [`.run()`] takes a reference to the plugin, it is necessary to define the type of
|
||||
/// plugin that the command expects here.
|
||||
type Plugin: Plugin;
|
||||
|
||||
/// The signature of the plugin command
|
||||
///
|
||||
/// These are aggregated from the [`Plugin`] and sent to the engine on `register`.
|
||||
fn signature(&self) -> PluginSignature;
|
||||
|
||||
/// Perform the actual behavior of the plugin command
|
||||
///
|
||||
/// The behavior of the plugin is defined by the implementation of this method. When Nushell
|
||||
/// invoked the plugin [serve_plugin] will call this method and print the serialized returned
|
||||
/// value or error to stdout, which Nushell will interpret.
|
||||
///
|
||||
/// `engine` provides an interface back to the Nushell engine. See [`EngineInterface`] docs for
|
||||
/// details on what methods are available.
|
||||
///
|
||||
/// The `call` contains metadata describing how the plugin command was invoked, including
|
||||
/// arguments, and `input` contains the structured data piped into the command.
|
||||
///
|
||||
/// This variant expects to receive and produce [`PipelineData`], which allows for stream-based
|
||||
/// handling of I/O. This is recommended if the plugin is expected to transform large
|
||||
/// lists or potentially large quantities of bytes. The API is more complex however, and
|
||||
/// [`SimplePluginCommand`] is recommended instead if this is not a concern.
|
||||
fn run(
|
||||
&self,
|
||||
plugin: &Self::Plugin,
|
||||
engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, LabeledError>;
|
||||
}
|
||||
|
||||
/// The API for a simple Nushell plugin command
|
||||
///
|
||||
/// This trait is an alternative to [`PluginCommand`], and operates on values instead of streams.
|
||||
/// Note that this may make handling large lists more difficult.
|
||||
///
|
||||
/// The plugin command must be able to be safely shared between threads, so that multiple
|
||||
/// invocations can be run in parallel. If interior mutability is desired, consider synchronization
|
||||
/// primitives such as [mutexes](std::sync::Mutex) and [channels](std::sync::mpsc).
|
||||
///
|
||||
/// # Examples
|
||||
/// Basic usage:
|
||||
/// ```
|
||||
/// # use nu_plugin::*;
|
||||
/// # use nu_protocol::{PluginSignature, Type, Value};
|
||||
/// struct HelloPlugin;
|
||||
/// struct Hello;
|
||||
///
|
||||
/// impl SimplePluginCommand for Hello {
|
||||
/// type Plugin = HelloPlugin;
|
||||
///
|
||||
/// fn signature(&self) -> PluginSignature {
|
||||
/// PluginSignature::build("hello")
|
||||
/// .input_output_type(Type::Nothing, Type::String)
|
||||
/// }
|
||||
///
|
||||
/// fn run(
|
||||
/// &self,
|
||||
/// plugin: &HelloPlugin,
|
||||
/// engine: &EngineInterface,
|
||||
/// call: &EvaluatedCall,
|
||||
/// input: &Value,
|
||||
/// ) -> Result<Value, LabeledError> {
|
||||
/// Ok(Value::string("Hello, World!".to_owned(), call.head))
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # impl Plugin for HelloPlugin {
|
||||
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
|
||||
/// # vec![Box::new(Hello)]
|
||||
/// # }
|
||||
/// # }
|
||||
/// #
|
||||
/// # fn main() {
|
||||
/// # serve_plugin(&HelloPlugin{}, MsgPackSerializer)
|
||||
/// # }
|
||||
/// ```
|
||||
pub trait SimplePluginCommand: Sync {
|
||||
/// The type of plugin this command runs on
|
||||
///
|
||||
/// Since [`.run()`] takes a reference to the plugin, it is necessary to define the type of
|
||||
/// plugin that the command expects here.
|
||||
type Plugin: Plugin;
|
||||
|
||||
/// The signature of the plugin command
|
||||
///
|
||||
/// These are aggregated from the [`Plugin`] and sent to the engine on `register`.
|
||||
fn signature(&self) -> PluginSignature;
|
||||
|
||||
/// Perform the actual behavior of the plugin command
|
||||
///
|
||||
/// The behavior of the plugin is defined by the implementation of this method. When Nushell
|
||||
/// invoked the plugin [serve_plugin] will call this method and print the serialized returned
|
||||
/// value or error to stdout, which Nushell will interpret.
|
||||
///
|
||||
/// `engine` provides an interface back to the Nushell engine. See [`EngineInterface`] docs for
|
||||
/// details on what methods are available.
|
||||
///
|
||||
/// The `call` contains metadata describing how the plugin command was invoked, including
|
||||
/// arguments, and `input` contains the structured data piped into the command.
|
||||
///
|
||||
/// This variant does not support streaming. Consider implementing [`PluginCommand`] directly
|
||||
/// if streaming is desired.
|
||||
fn run(
|
||||
&self,
|
||||
plugin: &Self::Plugin,
|
||||
engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
input: &Value,
|
||||
) -> Result<Value, LabeledError>;
|
||||
}
|
||||
|
||||
/// All [`SimplePluginCommand`]s can be used as [`PluginCommand`]s, but input streams will be fully
|
||||
/// consumed before the plugin command runs.
|
||||
impl<T> PluginCommand for T
|
||||
where
|
||||
T: SimplePluginCommand,
|
||||
{
|
||||
type Plugin = <Self as SimplePluginCommand>::Plugin;
|
||||
|
||||
fn signature(&self) -> PluginSignature {
|
||||
<Self as SimplePluginCommand>::signature(self)
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
plugin: &Self::Plugin,
|
||||
engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, LabeledError> {
|
||||
// Unwrap the PipelineData from input, consuming the potential stream, and pass it to the
|
||||
// simpler signature in Plugin
|
||||
let span = input.span().unwrap_or(call.head);
|
||||
let input_value = input.into_value(span);
|
||||
// Wrap the output in PipelineData::Value
|
||||
<Self as SimplePluginCommand>::run(self, plugin, engine, call, &input_value)
|
||||
.map(|value| PipelineData::Value(value, None))
|
||||
}
|
||||
}
|
@ -1,7 +1,13 @@
|
||||
use nu_engine::documentation::get_flags_section;
|
||||
use nu_protocol::ast::Operator;
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt::Write;
|
||||
use std::io::{BufReader, Read, Write as WriteTrait};
|
||||
use std::path::Path;
|
||||
use std::process::{Child, ChildStdout, Command as CommandSys, Stdio};
|
||||
use std::{env, thread};
|
||||
|
||||
use std::sync::mpsc::TrySendError;
|
||||
use std::sync::{mpsc, Arc, Mutex};
|
||||
@ -11,11 +17,6 @@ use crate::protocol::{
|
||||
CallInfo, CustomValueOp, LabeledError, PluginCustomValue, PluginInput, PluginOutput,
|
||||
};
|
||||
use crate::EncodingType;
|
||||
use std::fmt::Write;
|
||||
use std::io::{BufReader, Read, Write as WriteTrait};
|
||||
use std::path::Path;
|
||||
use std::process::{Child, ChildStdout, Command as CommandSys, Stdio};
|
||||
use std::{env, thread};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::process::CommandExt;
|
||||
@ -24,13 +25,13 @@ use std::os::unix::process::CommandExt;
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
use nu_protocol::{
|
||||
CustomValue, IntoSpanned, PipelineData, PluginSignature, ShellError, Spanned, Value,
|
||||
ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginSignature, ShellError, Spanned,
|
||||
Value,
|
||||
};
|
||||
|
||||
use self::gc::PluginGc;
|
||||
|
||||
use super::EvaluatedCall;
|
||||
|
||||
mod command;
|
||||
mod context;
|
||||
mod declaration;
|
||||
mod gc;
|
||||
@ -38,6 +39,7 @@ mod interface;
|
||||
mod persistent;
|
||||
mod source;
|
||||
|
||||
pub use command::{PluginCommand, SimplePluginCommand};
|
||||
pub use declaration::PluginDeclaration;
|
||||
pub use interface::EngineInterface;
|
||||
pub use persistent::PersistentPlugin;
|
||||
@ -215,13 +217,10 @@ where
|
||||
plugin.get(envs)?.get_signature()
|
||||
}
|
||||
|
||||
/// The basic API for a Nushell plugin
|
||||
/// The API for a Nushell plugin
|
||||
///
|
||||
/// This is the trait that Nushell plugins must implement. The methods defined on
|
||||
/// `Plugin` are invoked by [serve_plugin] during plugin registration and execution.
|
||||
///
|
||||
/// If large amounts of data are expected to need to be received or produced, it may be more
|
||||
/// appropriate to implement [StreamingPlugin] instead.
|
||||
/// A plugin defines multiple commands, which are added to the engine when the user calls
|
||||
/// `register`.
|
||||
///
|
||||
/// The plugin must be able to be safely shared between threads, so that multiple invocations can
|
||||
/// be run in parallel. If interior mutability is desired, consider synchronization primitives such
|
||||
@ -233,18 +232,25 @@ where
|
||||
/// # use nu_plugin::*;
|
||||
/// # use nu_protocol::{PluginSignature, Type, Value};
|
||||
/// struct HelloPlugin;
|
||||
/// struct Hello;
|
||||
///
|
||||
/// impl Plugin for HelloPlugin {
|
||||
/// fn signature(&self) -> Vec<PluginSignature> {
|
||||
/// let sig = PluginSignature::build("hello")
|
||||
/// .input_output_type(Type::Nothing, Type::String);
|
||||
/// fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
|
||||
/// vec![Box::new(Hello)]
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// vec![sig]
|
||||
/// impl SimplePluginCommand for Hello {
|
||||
/// type Plugin = HelloPlugin;
|
||||
///
|
||||
/// fn signature(&self) -> PluginSignature {
|
||||
/// PluginSignature::build("hello")
|
||||
/// .input_output_type(Type::Nothing, Type::String)
|
||||
/// }
|
||||
///
|
||||
/// fn run(
|
||||
/// &self,
|
||||
/// name: &str,
|
||||
/// plugin: &HelloPlugin,
|
||||
/// engine: &EngineInterface,
|
||||
/// call: &EvaluatedCall,
|
||||
/// input: &Value,
|
||||
@ -258,37 +264,14 @@ where
|
||||
/// # }
|
||||
/// ```
|
||||
pub trait Plugin: Sync {
|
||||
/// The signature of the plugin
|
||||
/// The commands supported by the plugin
|
||||
///
|
||||
/// This method returns the [PluginSignature]s that describe the capabilities
|
||||
/// of this plugin. Since a single plugin executable can support multiple invocation
|
||||
/// patterns we return a `Vec` of signatures.
|
||||
fn signature(&self) -> Vec<PluginSignature>;
|
||||
|
||||
/// Perform the actual behavior of the plugin
|
||||
/// Each [`PluginCommand`] contains both the signature of the command and the functionality it
|
||||
/// implements.
|
||||
///
|
||||
/// The behavior of the plugin is defined by the implementation of this method.
|
||||
/// When Nushell invoked the plugin [serve_plugin] will call this method and
|
||||
/// print the serialized returned value or error to stdout, which Nushell will
|
||||
/// interpret.
|
||||
///
|
||||
/// The `name` is only relevant for plugins that implement multiple commands as the
|
||||
/// invoked command will be passed in via this argument. The `call` contains
|
||||
/// metadata describing how the plugin was invoked and `input` contains the structured
|
||||
/// data passed to the command implemented by this [Plugin].
|
||||
///
|
||||
/// `engine` provides an interface back to the Nushell engine. See [`EngineInterface`] docs for
|
||||
/// details on what methods are available.
|
||||
///
|
||||
/// This variant does not support streaming. Consider implementing [StreamingPlugin] instead
|
||||
/// if streaming is desired.
|
||||
fn run(
|
||||
&self,
|
||||
name: &str,
|
||||
engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
input: &Value,
|
||||
) -> Result<Value, LabeledError>;
|
||||
/// This is only called once by [`serve_plugin`] at the beginning of your plugin's execution. It
|
||||
/// is not possible to change the defined commands during runtime.
|
||||
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>>;
|
||||
|
||||
/// Collapse a custom value to plain old data.
|
||||
///
|
||||
@ -397,269 +380,7 @@ pub trait Plugin: Sync {
|
||||
}
|
||||
}
|
||||
|
||||
/// The streaming API for a Nushell plugin
|
||||
///
|
||||
/// This is a more low-level version of the [Plugin] trait that supports operating on streams of
|
||||
/// data. If you don't need to operate on streams, consider using that trait instead.
|
||||
///
|
||||
/// The methods defined on `StreamingPlugin` are invoked by [serve_plugin] during plugin
|
||||
/// registration and execution.
|
||||
///
|
||||
/// # Examples
|
||||
/// Basic usage:
|
||||
/// ```
|
||||
/// # use nu_plugin::*;
|
||||
/// # use nu_protocol::{PluginSignature, PipelineData, Type, Value};
|
||||
/// struct LowercasePlugin;
|
||||
///
|
||||
/// impl StreamingPlugin for LowercasePlugin {
|
||||
/// fn signature(&self) -> Vec<PluginSignature> {
|
||||
/// let sig = PluginSignature::build("lowercase")
|
||||
/// .usage("Convert each string in a stream to lowercase")
|
||||
/// .input_output_type(Type::List(Type::String.into()), Type::List(Type::String.into()));
|
||||
///
|
||||
/// vec![sig]
|
||||
/// }
|
||||
///
|
||||
/// fn run(
|
||||
/// &self,
|
||||
/// name: &str,
|
||||
/// engine: &EngineInterface,
|
||||
/// call: &EvaluatedCall,
|
||||
/// input: PipelineData,
|
||||
/// ) -> Result<PipelineData, LabeledError> {
|
||||
/// let span = call.head;
|
||||
/// Ok(input.map(move |value| {
|
||||
/// value.as_str()
|
||||
/// .map(|string| Value::string(string.to_lowercase(), span))
|
||||
/// // Errors in a stream should be returned as values.
|
||||
/// .unwrap_or_else(|err| Value::error(err, span))
|
||||
/// }, None)?)
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # fn main() {
|
||||
/// # serve_plugin(&LowercasePlugin{}, MsgPackSerializer)
|
||||
/// # }
|
||||
/// ```
|
||||
pub trait StreamingPlugin: Sync {
|
||||
/// The signature of the plugin
|
||||
///
|
||||
/// This method returns the [PluginSignature]s that describe the capabilities
|
||||
/// of this plugin. Since a single plugin executable can support multiple invocation
|
||||
/// patterns we return a `Vec` of signatures.
|
||||
fn signature(&self) -> Vec<PluginSignature>;
|
||||
|
||||
/// Perform the actual behavior of the plugin
|
||||
///
|
||||
/// The behavior of the plugin is defined by the implementation of this method.
|
||||
/// When Nushell invoked the plugin [serve_plugin] will call this method and
|
||||
/// print the serialized returned value or error to stdout, which Nushell will
|
||||
/// interpret.
|
||||
///
|
||||
/// The `name` is only relevant for plugins that implement multiple commands as the
|
||||
/// invoked command will be passed in via this argument. The `call` contains
|
||||
/// metadata describing how the plugin was invoked and `input` contains the structured
|
||||
/// data passed to the command implemented by this [Plugin].
|
||||
///
|
||||
/// This variant expects to receive and produce [PipelineData], which allows for stream-based
|
||||
/// handling of I/O. This is recommended if the plugin is expected to transform large lists or
|
||||
/// potentially large quantities of bytes. The API is more complex however, and [Plugin] is
|
||||
/// recommended instead if this is not a concern.
|
||||
fn run(
|
||||
&self,
|
||||
name: &str,
|
||||
engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, LabeledError>;
|
||||
|
||||
/// Collapse a custom value to plain old data.
|
||||
///
|
||||
/// The default implementation of this method just calls [`CustomValue::to_base_value`], but
|
||||
/// the method can be implemented differently if accessing plugin state is desirable.
|
||||
fn custom_value_to_base_value(
|
||||
&self,
|
||||
engine: &EngineInterface,
|
||||
custom_value: Spanned<Box<dyn CustomValue>>,
|
||||
) -> Result<Value, LabeledError> {
|
||||
let _ = engine;
|
||||
custom_value
|
||||
.item
|
||||
.to_base_value(custom_value.span)
|
||||
.map_err(LabeledError::from)
|
||||
}
|
||||
|
||||
/// Follow a numbered cell path on a custom value - e.g. `value.0`.
|
||||
///
|
||||
/// The default implementation of this method just calls [`CustomValue::follow_path_int`], but
|
||||
/// the method can be implemented differently if accessing plugin state is desirable.
|
||||
fn custom_value_follow_path_int(
|
||||
&self,
|
||||
engine: &EngineInterface,
|
||||
custom_value: Spanned<Box<dyn CustomValue>>,
|
||||
index: Spanned<usize>,
|
||||
) -> Result<Value, LabeledError> {
|
||||
let _ = engine;
|
||||
custom_value
|
||||
.item
|
||||
.follow_path_int(custom_value.span, index.item, index.span)
|
||||
.map_err(LabeledError::from)
|
||||
}
|
||||
|
||||
/// Follow a named cell path on a custom value - e.g. `value.column`.
|
||||
///
|
||||
/// The default implementation of this method just calls [`CustomValue::follow_path_string`],
|
||||
/// but the method can be implemented differently if accessing plugin state is desirable.
|
||||
fn custom_value_follow_path_string(
|
||||
&self,
|
||||
engine: &EngineInterface,
|
||||
custom_value: Spanned<Box<dyn CustomValue>>,
|
||||
column_name: Spanned<String>,
|
||||
) -> Result<Value, LabeledError> {
|
||||
let _ = engine;
|
||||
custom_value
|
||||
.item
|
||||
.follow_path_string(custom_value.span, column_name.item, column_name.span)
|
||||
.map_err(LabeledError::from)
|
||||
}
|
||||
|
||||
/// Implement comparison logic for custom values.
|
||||
///
|
||||
/// The default implementation of this method just calls [`CustomValue::partial_cmp`], but
|
||||
/// the method can be implemented differently if accessing plugin state is desirable.
|
||||
///
|
||||
/// Note that returning an error here is unlikely to produce desired behavior, as `partial_cmp`
|
||||
/// lacks a way to produce an error. At the moment the engine just logs the error, and the
|
||||
/// comparison returns `None`.
|
||||
fn custom_value_partial_cmp(
|
||||
&self,
|
||||
engine: &EngineInterface,
|
||||
custom_value: Box<dyn CustomValue>,
|
||||
other_value: Value,
|
||||
) -> Result<Option<Ordering>, LabeledError> {
|
||||
let _ = engine;
|
||||
Ok(custom_value.partial_cmp(&other_value))
|
||||
}
|
||||
|
||||
/// Implement functionality for an operator on a custom value.
|
||||
///
|
||||
/// The default implementation of this method just calls [`CustomValue::operation`], but
|
||||
/// the method can be implemented differently if accessing plugin state is desirable.
|
||||
fn custom_value_operation(
|
||||
&self,
|
||||
engine: &EngineInterface,
|
||||
left: Spanned<Box<dyn CustomValue>>,
|
||||
operator: Spanned<Operator>,
|
||||
right: Value,
|
||||
) -> Result<Value, LabeledError> {
|
||||
let _ = engine;
|
||||
left.item
|
||||
.operation(left.span, operator.item, operator.span, &right)
|
||||
.map_err(LabeledError::from)
|
||||
}
|
||||
|
||||
/// Handle a notification that all copies of a custom value within the engine have been dropped.
|
||||
///
|
||||
/// This notification is only sent if [`CustomValue::notify_plugin_on_drop`] was true. Unlike
|
||||
/// the other custom value handlers, a span is not provided.
|
||||
///
|
||||
/// Note that a new custom value is created each time it is sent to the engine - if you intend
|
||||
/// to accept a custom value and send it back, you may need to implement some kind of unique
|
||||
/// reference counting in your plugin, as you will receive multiple drop notifications even if
|
||||
/// the data within is identical.
|
||||
///
|
||||
/// The default implementation does nothing. Any error generated here is unlikely to be visible
|
||||
/// to the user, and will only show up in the engine's log output.
|
||||
fn custom_value_dropped(
|
||||
&self,
|
||||
engine: &EngineInterface,
|
||||
custom_value: Box<dyn CustomValue>,
|
||||
) -> Result<(), LabeledError> {
|
||||
let _ = (engine, custom_value);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// All [Plugin]s can be used as [StreamingPlugin]s, but input streams will be fully consumed
|
||||
/// before the plugin runs.
|
||||
impl<T: Plugin> StreamingPlugin for T {
|
||||
fn signature(&self) -> Vec<PluginSignature> {
|
||||
<Self as Plugin>::signature(self)
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
name: &str,
|
||||
engine: &EngineInterface,
|
||||
call: &EvaluatedCall,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, LabeledError> {
|
||||
// Unwrap the PipelineData from input, consuming the potential stream, and pass it to the
|
||||
// simpler signature in Plugin
|
||||
let span = input.span().unwrap_or(call.head);
|
||||
let input_value = input.into_value(span);
|
||||
// Wrap the output in PipelineData::Value
|
||||
<Self as Plugin>::run(self, name, engine, call, &input_value)
|
||||
.map(|value| PipelineData::Value(value, None))
|
||||
}
|
||||
|
||||
fn custom_value_to_base_value(
|
||||
&self,
|
||||
engine: &EngineInterface,
|
||||
custom_value: Spanned<Box<dyn CustomValue>>,
|
||||
) -> Result<Value, LabeledError> {
|
||||
<Self as Plugin>::custom_value_to_base_value(self, engine, custom_value)
|
||||
}
|
||||
|
||||
fn custom_value_follow_path_int(
|
||||
&self,
|
||||
engine: &EngineInterface,
|
||||
custom_value: Spanned<Box<dyn CustomValue>>,
|
||||
index: Spanned<usize>,
|
||||
) -> Result<Value, LabeledError> {
|
||||
<Self as Plugin>::custom_value_follow_path_int(self, engine, custom_value, index)
|
||||
}
|
||||
|
||||
fn custom_value_follow_path_string(
|
||||
&self,
|
||||
engine: &EngineInterface,
|
||||
custom_value: Spanned<Box<dyn CustomValue>>,
|
||||
column_name: Spanned<String>,
|
||||
) -> Result<Value, LabeledError> {
|
||||
<Self as Plugin>::custom_value_follow_path_string(self, engine, custom_value, column_name)
|
||||
}
|
||||
|
||||
fn custom_value_partial_cmp(
|
||||
&self,
|
||||
engine: &EngineInterface,
|
||||
custom_value: Box<dyn CustomValue>,
|
||||
other_value: Value,
|
||||
) -> Result<Option<Ordering>, LabeledError> {
|
||||
<Self as Plugin>::custom_value_partial_cmp(self, engine, custom_value, other_value)
|
||||
}
|
||||
|
||||
fn custom_value_operation(
|
||||
&self,
|
||||
engine: &EngineInterface,
|
||||
left: Spanned<Box<dyn CustomValue>>,
|
||||
operator: Spanned<Operator>,
|
||||
right: Value,
|
||||
) -> Result<Value, LabeledError> {
|
||||
<Self as Plugin>::custom_value_operation(self, engine, left, operator, right)
|
||||
}
|
||||
|
||||
fn custom_value_dropped(
|
||||
&self,
|
||||
engine: &EngineInterface,
|
||||
custom_value: Box<dyn CustomValue>,
|
||||
) -> Result<(), LabeledError> {
|
||||
<Self as Plugin>::custom_value_dropped(self, engine, custom_value)
|
||||
}
|
||||
}
|
||||
|
||||
/// Function used to implement the communication protocol between
|
||||
/// nushell and an external plugin. Both [Plugin] and [StreamingPlugin] are supported.
|
||||
/// Function used to implement the communication protocol between nushell and an external plugin.
|
||||
///
|
||||
/// When creating a new plugin this function is typically used as the main entry
|
||||
/// point for the plugin, e.g.
|
||||
@ -670,19 +391,31 @@ impl<T: Plugin> StreamingPlugin for T {
|
||||
/// # struct MyPlugin;
|
||||
/// # impl MyPlugin { fn new() -> Self { Self }}
|
||||
/// # impl Plugin for MyPlugin {
|
||||
/// # fn signature(&self) -> Vec<PluginSignature> {todo!();}
|
||||
/// # fn run(&self, name: &str, engine: &EngineInterface, call: &EvaluatedCall, input: &Value)
|
||||
/// # -> Result<Value, LabeledError> {todo!();}
|
||||
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {todo!();}
|
||||
/// # }
|
||||
/// fn main() {
|
||||
/// serve_plugin(&MyPlugin::new(), MsgPackSerializer)
|
||||
/// }
|
||||
/// ```
|
||||
pub fn serve_plugin(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder + 'static) {
|
||||
pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static) {
|
||||
let mut args = env::args().skip(1);
|
||||
let number_of_args = args.len();
|
||||
let first_arg = args.next();
|
||||
|
||||
// Determine the plugin name, for errors
|
||||
let exe = std::env::current_exe().ok();
|
||||
|
||||
let plugin_name: String = exe
|
||||
.as_ref()
|
||||
.and_then(|path| path.file_stem())
|
||||
.map(|stem| stem.to_string_lossy().into_owned())
|
||||
.map(|stem| {
|
||||
stem.strip_prefix("nu_plugin_")
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or(stem)
|
||||
})
|
||||
.unwrap_or_else(|| "(unknown)".into());
|
||||
|
||||
if number_of_args == 0
|
||||
|| first_arg
|
||||
.as_ref()
|
||||
@ -708,6 +441,19 @@ pub fn serve_plugin(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder +
|
||||
std::process::exit(1)
|
||||
}
|
||||
|
||||
// Build commands map, to make running a command easier
|
||||
let mut commands: HashMap<String, _> = HashMap::new();
|
||||
|
||||
for command in plugin.commands() {
|
||||
if let Some(previous) = commands.insert(command.signature().sig.name.clone(), command) {
|
||||
eprintln!(
|
||||
"Plugin `{plugin_name}` warning: command `{}` shadowed by another command with the \
|
||||
same name. Check your command signatures",
|
||||
previous.signature().sig.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// tell nushell encoding.
|
||||
//
|
||||
// 1 byte
|
||||
@ -735,20 +481,6 @@ pub fn serve_plugin(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder +
|
||||
// We need to hold on to the interface to keep the manager alive. We can drop it at the end
|
||||
let interface = manager.get_interface();
|
||||
|
||||
// Determine the plugin name, for errors
|
||||
let exe = std::env::current_exe().ok();
|
||||
|
||||
let plugin_name: String = exe
|
||||
.as_ref()
|
||||
.and_then(|path| path.file_stem())
|
||||
.map(|stem| stem.to_string_lossy().into_owned())
|
||||
.map(|stem| {
|
||||
stem.strip_prefix("nu_plugin_")
|
||||
.map(|s| s.to_owned())
|
||||
.unwrap_or(stem)
|
||||
})
|
||||
.unwrap_or_else(|| "(unknown)".into());
|
||||
|
||||
// Try an operation that could result in ShellError. Exit if an I/O error is encountered.
|
||||
// Try to report the error to nushell otherwise, and failing that, panic.
|
||||
macro_rules! try_or_report {
|
||||
@ -802,7 +534,15 @@ pub fn serve_plugin(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder +
|
||||
thread::scope(|scope| {
|
||||
let run = |engine, call_info| {
|
||||
let CallInfo { name, call, input } = call_info;
|
||||
let result = plugin.run(&name, &engine, &call, input);
|
||||
let result = if let Some(command) = commands.get(&name) {
|
||||
command.run(plugin, &engine, &call, input)
|
||||
} else {
|
||||
Err(LabeledError {
|
||||
label: format!("Plugin command not found: `{name}`"),
|
||||
msg: format!("plugin `{plugin_name}` doesn't have this command"),
|
||||
span: Some(call.head),
|
||||
})
|
||||
};
|
||||
let write_result = engine
|
||||
.write_response(result)
|
||||
.and_then(|writer| writer.write());
|
||||
@ -828,7 +568,11 @@ pub fn serve_plugin(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder +
|
||||
match plugin_call {
|
||||
// Sending the signature back to nushell to create the declaration definition
|
||||
ReceivedPluginCall::Signature { engine } => {
|
||||
try_or_report!(engine, engine.write_signature(plugin.signature()));
|
||||
let sigs = commands
|
||||
.values()
|
||||
.map(|command| command.signature())
|
||||
.collect();
|
||||
try_or_report!(engine, engine.write_signature(sigs));
|
||||
}
|
||||
// Run the plugin on a background thread, handling any input or output streams
|
||||
ReceivedPluginCall::Run { engine, call } => {
|
||||
@ -866,7 +610,7 @@ pub fn serve_plugin(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder +
|
||||
}
|
||||
|
||||
fn custom_value_op(
|
||||
plugin: &impl StreamingPlugin,
|
||||
plugin: &impl Plugin,
|
||||
engine: &EngineInterface,
|
||||
custom_value: Spanned<PluginCustomValue>,
|
||||
op: CustomValueOp,
|
||||
@ -929,13 +673,14 @@ fn custom_value_op(
|
||||
}
|
||||
}
|
||||
|
||||
fn print_help(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder) {
|
||||
fn print_help(plugin: &impl Plugin, encoder: impl PluginEncoder) {
|
||||
println!("Nushell Plugin");
|
||||
println!("Encoder: {}", encoder.name());
|
||||
|
||||
let mut help = String::new();
|
||||
|
||||
plugin.signature().iter().for_each(|signature| {
|
||||
plugin.commands().into_iter().for_each(|command| {
|
||||
let signature = command.signature();
|
||||
let res = write!(help, "\nCommand: {}", signature.sig.name)
|
||||
.and_then(|_| writeln!(help, "\nUsage:\n > {}", signature.sig.usage))
|
||||
.and_then(|_| {
|
||||
|
Reference in New Issue
Block a user