diff --git a/crates/nu-plugin/src/lib.rs b/crates/nu-plugin/src/lib.rs index fdc612b73d..799ffd96c0 100644 --- a/crates/nu-plugin/src/lib.rs +++ b/crates/nu-plugin/src/lib.rs @@ -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 { +//! fn commands(&self) -> Vec>> { +//! 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}; diff --git a/crates/nu-plugin/src/plugin/command.rs b/crates/nu-plugin/src/plugin/command.rs new file mode 100644 index 0000000000..93e8ecdefe --- /dev/null +++ b/crates/nu-plugin/src/plugin/command.rs @@ -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 { +/// 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>> { +/// # 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; +} + +/// 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 { +/// Ok(Value::string("Hello, World!".to_owned(), call.head)) +/// } +/// } +/// +/// # impl Plugin for HelloPlugin { +/// # fn commands(&self) -> Vec>> { +/// # 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; +} + +/// All [`SimplePluginCommand`]s can be used as [`PluginCommand`]s, but input streams will be fully +/// consumed before the plugin command runs. +impl PluginCommand for T +where + T: SimplePluginCommand, +{ + type Plugin = ::Plugin; + + fn signature(&self) -> PluginSignature { + ::signature(self) + } + + fn run( + &self, + plugin: &Self::Plugin, + engine: &EngineInterface, + call: &EvaluatedCall, + input: PipelineData, + ) -> Result { + // 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 + ::run(self, plugin, engine, call, &input_value) + .map(|value| PipelineData::Value(value, None)) + } +} diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs index 18527fc28b..4fe01debf4 100644 --- a/crates/nu-plugin/src/plugin/mod.rs +++ b/crates/nu-plugin/src/plugin/mod.rs @@ -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 { -/// let sig = PluginSignature::build("hello") -/// .input_output_type(Type::Nothing, Type::String); +/// fn commands(&self) -> Vec>> { +/// 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; - - /// 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; + /// 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>>; /// 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 { -/// 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 { -/// 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; - - /// 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; - - /// 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>, - ) -> Result { - 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>, - index: Spanned, - ) -> Result { - 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>, - column_name: Spanned, - ) -> Result { - 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, - other_value: Value, - ) -> Result, 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>, - operator: Spanned, - right: Value, - ) -> Result { - 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, - ) -> 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 StreamingPlugin for T { - fn signature(&self) -> Vec { - ::signature(self) - } - - fn run( - &self, - name: &str, - engine: &EngineInterface, - call: &EvaluatedCall, - input: PipelineData, - ) -> Result { - // 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 - ::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>, - ) -> Result { - ::custom_value_to_base_value(self, engine, custom_value) - } - - fn custom_value_follow_path_int( - &self, - engine: &EngineInterface, - custom_value: Spanned>, - index: Spanned, - ) -> Result { - ::custom_value_follow_path_int(self, engine, custom_value, index) - } - - fn custom_value_follow_path_string( - &self, - engine: &EngineInterface, - custom_value: Spanned>, - column_name: Spanned, - ) -> Result { - ::custom_value_follow_path_string(self, engine, custom_value, column_name) - } - - fn custom_value_partial_cmp( - &self, - engine: &EngineInterface, - custom_value: Box, - other_value: Value, - ) -> Result, LabeledError> { - ::custom_value_partial_cmp(self, engine, custom_value, other_value) - } - - fn custom_value_operation( - &self, - engine: &EngineInterface, - left: Spanned>, - operator: Spanned, - right: Value, - ) -> Result { - ::custom_value_operation(self, engine, left, operator, right) - } - - fn custom_value_dropped( - &self, - engine: &EngineInterface, - custom_value: Box, - ) -> Result<(), LabeledError> { - ::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 StreamingPlugin for T { /// # struct MyPlugin; /// # impl MyPlugin { fn new() -> Self { Self }} /// # impl Plugin for MyPlugin { -/// # fn signature(&self) -> Vec {todo!();} -/// # fn run(&self, name: &str, engine: &EngineInterface, call: &EvaluatedCall, input: &Value) -/// # -> Result {todo!();} +/// # fn commands(&self) -> Vec>> {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 = 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, 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(|_| { diff --git a/crates/nu-protocol/src/value/custom_value.rs b/crates/nu-protocol/src/value/custom_value.rs index 9c96741fe3..b7e29e51df 100644 --- a/crates/nu-protocol/src/value/custom_value.rs +++ b/crates/nu-protocol/src/value/custom_value.rs @@ -79,7 +79,7 @@ pub trait CustomValue: fmt::Debug + Send + Sync { /// copies of this custom value are dropped in the engine. /// /// The notification will take place via - /// [`.custom_value_dropped()`](crate::StreamingPlugin::custom_value_dropped) on the plugin. + /// [`.custom_value_dropped()`](crate::Plugin::custom_value_dropped) on the plugin. /// /// The default is `false`. fn notify_plugin_on_drop(&self) -> bool { diff --git a/crates/nu_plugin_custom_values/src/drop_check.rs b/crates/nu_plugin_custom_values/src/drop_check.rs index a9d5f5e72c..293fbfe294 100644 --- a/crates/nu_plugin_custom_values/src/drop_check.rs +++ b/crates/nu_plugin_custom_values/src/drop_check.rs @@ -1,14 +1,19 @@ -use nu_protocol::{record, CustomValue, ShellError, Span, Value}; +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{ + record, Category, CustomValue, PluginSignature, ShellError, Span, SyntaxShape, Value, +}; use serde::{Deserialize, Serialize}; +use crate::CustomValuePlugin; + #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct DropCheck { +pub struct DropCheckValue { pub(crate) msg: String, } -impl DropCheck { - pub(crate) fn new(msg: String) -> DropCheck { - DropCheck { msg } +impl DropCheckValue { + pub(crate) fn new(msg: String) -> DropCheckValue { + DropCheckValue { msg } } pub(crate) fn into_value(self, span: Span) -> Value { @@ -16,18 +21,18 @@ impl DropCheck { } pub(crate) fn notify(&self) { - eprintln!("DropCheck was dropped: {}", self.msg); + eprintln!("DropCheckValue was dropped: {}", self.msg); } } #[typetag::serde] -impl CustomValue for DropCheck { +impl CustomValue for DropCheckValue { fn clone_value(&self, span: Span) -> Value { self.clone().into_value(span) } fn value_string(&self) -> String { - "DropCheck".into() + "DropCheckValue".into() } fn to_base_value(&self, span: Span) -> Result { @@ -48,3 +53,26 @@ impl CustomValue for DropCheck { true } } + +pub struct DropCheck; + +impl SimplePluginCommand for DropCheck { + type Plugin = CustomValuePlugin; + + fn signature(&self) -> nu_protocol::PluginSignature { + PluginSignature::build("custom-value drop-check") + .usage("Generates a custom value that prints a message when dropped") + .required("msg", SyntaxShape::String, "the message to print on drop") + .category(Category::Experimental) + } + + fn run( + &self, + _plugin: &Self::Plugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + Ok(DropCheckValue::new(call.req(0)?).into_value(call.head)) + } +} diff --git a/crates/nu_plugin_custom_values/src/generate.rs b/crates/nu_plugin_custom_values/src/generate.rs new file mode 100644 index 0000000000..1d679bb98d --- /dev/null +++ b/crates/nu_plugin_custom_values/src/generate.rs @@ -0,0 +1,26 @@ +use crate::{cool_custom_value::CoolCustomValue, CustomValuePlugin}; + +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{Category, PluginSignature, Value}; + +pub struct Generate; + +impl SimplePluginCommand for Generate { + type Plugin = CustomValuePlugin; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("custom-value generate") + .usage("PluginSignature for a plugin that generates a custom value") + .category(Category::Experimental) + } + + fn run( + &self, + _plugin: &CustomValuePlugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + Ok(CoolCustomValue::new("abc").into_value(call.head)) + } +} diff --git a/crates/nu_plugin_custom_values/src/generate2.rs b/crates/nu_plugin_custom_values/src/generate2.rs new file mode 100644 index 0000000000..2c070226f5 --- /dev/null +++ b/crates/nu_plugin_custom_values/src/generate2.rs @@ -0,0 +1,42 @@ +use crate::{second_custom_value::SecondCustomValue, CustomValuePlugin}; + +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{Category, PluginSignature, SyntaxShape, Value}; + +pub struct Generate2; + +impl SimplePluginCommand for Generate2 { + type Plugin = CustomValuePlugin; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("custom-value generate2") + .usage("PluginSignature for a plugin that generates a different custom value") + .optional( + "closure", + SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), + "An optional closure to pass the custom value to", + ) + .category(Category::Experimental) + } + + fn run( + &self, + _plugin: &CustomValuePlugin, + engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + let second_custom_value = SecondCustomValue::new("xyz").into_value(call.head); + // If we were passed a closure, execute that instead + if let Some(closure) = call.opt(0)? { + let result = engine.eval_closure( + &closure, + vec![second_custom_value.clone()], + Some(second_custom_value), + )?; + Ok(result) + } else { + Ok(second_custom_value) + } + } +} diff --git a/crates/nu_plugin_custom_values/src/main.rs b/crates/nu_plugin_custom_values/src/main.rs index 078872d713..c91ff709b2 100644 --- a/crates/nu_plugin_custom_values/src/main.rs +++ b/crates/nu_plugin_custom_values/src/main.rs @@ -1,132 +1,49 @@ +use nu_plugin::{ + serve_plugin, EngineInterface, LabeledError, MsgPackSerializer, Plugin, PluginCommand, +}; + mod cool_custom_value; -mod drop_check; mod second_custom_value; -use cool_custom_value::CoolCustomValue; -use drop_check::DropCheck; -use second_custom_value::SecondCustomValue; +mod drop_check; +mod generate; +mod generate2; +mod update; +mod update_arg; -use nu_plugin::{serve_plugin, EngineInterface, MsgPackSerializer, Plugin}; -use nu_plugin::{EvaluatedCall, LabeledError}; -use nu_protocol::{Category, CustomValue, PluginSignature, ShellError, SyntaxShape, Value}; +use drop_check::{DropCheck, DropCheckValue}; +use generate::Generate; +use generate2::Generate2; +use nu_protocol::CustomValue; +use update::Update; +use update_arg::UpdateArg; -struct CustomValuePlugin; +pub struct CustomValuePlugin; impl Plugin for CustomValuePlugin { - fn signature(&self) -> Vec { + fn commands(&self) -> Vec>> { vec![ - PluginSignature::build("custom-value generate") - .usage("PluginSignature for a plugin that generates a custom value") - .category(Category::Experimental), - PluginSignature::build("custom-value generate2") - .usage("PluginSignature for a plugin that generates a different custom value") - .optional( - "closure", - SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), - "An optional closure to pass the custom value to", - ) - .category(Category::Experimental), - PluginSignature::build("custom-value update") - .usage("PluginSignature for a plugin that updates a custom value") - .category(Category::Experimental), - PluginSignature::build("custom-value update-arg") - .usage("PluginSignature for a plugin that updates a custom value as an argument") - .required( - "custom_value", - SyntaxShape::Any, - "the custom value to update", - ) - .category(Category::Experimental), - PluginSignature::build("custom-value drop-check") - .usage("Generates a custom value that prints a message when dropped") - .required("msg", SyntaxShape::String, "the message to print on drop") - .category(Category::Experimental), + Box::new(Generate), + Box::new(Generate2), + Box::new(Update), + Box::new(UpdateArg), + Box::new(DropCheck), ] } - fn run( - &self, - name: &str, - engine: &EngineInterface, - call: &EvaluatedCall, - input: &Value, - ) -> Result { - match name { - "custom-value generate" => self.generate(call, input), - "custom-value generate2" => self.generate2(engine, call), - "custom-value update" => self.update(call, input), - "custom-value update-arg" => self.update(call, &call.req(0)?), - "custom-value drop-check" => self.drop_check(call), - _ => Err(LabeledError { - label: "Plugin call with wrong name signature".into(), - msg: "the signature used to call the plugin does not match any name in the plugin signature vector".into(), - span: Some(call.head), - }), - } - } - fn custom_value_dropped( &self, _engine: &EngineInterface, custom_value: Box, ) -> Result<(), LabeledError> { // This is how we implement our drop behavior for DropCheck. - if let Some(drop_check) = custom_value.as_any().downcast_ref::() { + if let Some(drop_check) = custom_value.as_any().downcast_ref::() { drop_check.notify(); } Ok(()) } } -impl CustomValuePlugin { - fn generate(&self, call: &EvaluatedCall, _input: &Value) -> Result { - Ok(CoolCustomValue::new("abc").into_value(call.head)) - } - - fn generate2( - &self, - engine: &EngineInterface, - call: &EvaluatedCall, - ) -> Result { - let second_custom_value = SecondCustomValue::new("xyz").into_value(call.head); - // If we were passed a closure, execute that instead - if let Some(closure) = call.opt(0)? { - let result = engine.eval_closure( - &closure, - vec![second_custom_value.clone()], - Some(second_custom_value), - )?; - Ok(result) - } else { - Ok(second_custom_value) - } - } - - fn update(&self, call: &EvaluatedCall, input: &Value) -> Result { - if let Ok(mut value) = CoolCustomValue::try_from_value(input) { - value.cool += "xyz"; - return Ok(value.into_value(call.head)); - } - - if let Ok(mut value) = SecondCustomValue::try_from_value(input) { - value.something += "abc"; - return Ok(value.into_value(call.head)); - } - - Err(ShellError::CantConvert { - to_type: "cool or second".into(), - from_type: "non-cool and non-second".into(), - span: call.head, - help: None, - } - .into()) - } - - fn drop_check(&self, call: &EvaluatedCall) -> Result { - Ok(DropCheck::new(call.req(0)?).into_value(call.head)) - } -} - fn main() { serve_plugin(&CustomValuePlugin, MsgPackSerializer {}) } diff --git a/crates/nu_plugin_custom_values/src/update.rs b/crates/nu_plugin_custom_values/src/update.rs new file mode 100644 index 0000000000..35c04473aa --- /dev/null +++ b/crates/nu_plugin_custom_values/src/update.rs @@ -0,0 +1,44 @@ +use crate::{ + cool_custom_value::CoolCustomValue, second_custom_value::SecondCustomValue, CustomValuePlugin, +}; + +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{Category, PluginSignature, ShellError, Value}; + +pub struct Update; + +impl SimplePluginCommand for Update { + type Plugin = CustomValuePlugin; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("custom-value update") + .usage("PluginSignature for a plugin that updates a custom value") + .category(Category::Experimental) + } + + fn run( + &self, + _plugin: &CustomValuePlugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + if let Ok(mut value) = CoolCustomValue::try_from_value(input) { + value.cool += "xyz"; + return Ok(value.into_value(call.head)); + } + + if let Ok(mut value) = SecondCustomValue::try_from_value(input) { + value.something += "abc"; + return Ok(value.into_value(call.head)); + } + + Err(ShellError::CantConvert { + to_type: "cool or second".into(), + from_type: "non-cool and non-second".into(), + span: call.head, + help: None, + } + .into()) + } +} diff --git a/crates/nu_plugin_custom_values/src/update_arg.rs b/crates/nu_plugin_custom_values/src/update_arg.rs new file mode 100644 index 0000000000..f406e11ad8 --- /dev/null +++ b/crates/nu_plugin_custom_values/src/update_arg.rs @@ -0,0 +1,31 @@ +use crate::{update::Update, CustomValuePlugin}; + +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{Category, PluginSignature, SyntaxShape, Value}; + +pub struct UpdateArg; + +impl SimplePluginCommand for UpdateArg { + type Plugin = CustomValuePlugin; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("custom-value update-arg") + .usage("PluginSignature for a plugin that updates a custom value as an argument") + .required( + "custom_value", + SyntaxShape::Any, + "the custom value to update", + ) + .category(Category::Experimental) + } + + fn run( + &self, + plugin: &CustomValuePlugin, + engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + SimplePluginCommand::run(&Update, plugin, engine, call, &call.req(0)?) + } +} diff --git a/crates/nu_plugin_example/src/commands/mod.rs b/crates/nu_plugin_example/src/commands/mod.rs new file mode 100644 index 0000000000..38371a335b --- /dev/null +++ b/crates/nu_plugin_example/src/commands/mod.rs @@ -0,0 +1,13 @@ +mod nu_example_1; +mod nu_example_2; +mod nu_example_3; +mod nu_example_config; +mod nu_example_disable_gc; +mod nu_example_env; + +pub use nu_example_1::NuExample1; +pub use nu_example_2::NuExample2; +pub use nu_example_3::NuExample3; +pub use nu_example_config::NuExampleConfig; +pub use nu_example_disable_gc::NuExampleDisableGc; +pub use nu_example_env::NuExampleEnv; diff --git a/crates/nu_plugin_example/src/commands/nu_example_1.rs b/crates/nu_plugin_example/src/commands/nu_example_1.rs new file mode 100644 index 0000000000..6741358379 --- /dev/null +++ b/crates/nu_plugin_example/src/commands/nu_example_1.rs @@ -0,0 +1,43 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{Category, PluginExample, PluginSignature, SyntaxShape, Value}; + +use crate::Example; + +pub struct NuExample1; + +impl SimplePluginCommand for NuExample1 { + type Plugin = Example; + + fn signature(&self) -> PluginSignature { + // The signature defines the usage of the command inside Nu, and also automatically + // generates its help page. + PluginSignature::build("nu-example-1") + .usage("PluginSignature test 1 for plugin. Returns Value::Nothing") + .extra_usage("Extra usage for nu-example-1") + .search_terms(vec!["example".into()]) + .required("a", SyntaxShape::Int, "required integer value") + .required("b", SyntaxShape::String, "required string value") + .switch("flag", "a flag for the signature", Some('f')) + .optional("opt", SyntaxShape::Int, "Optional number") + .named("named", SyntaxShape::String, "named string", Some('n')) + .rest("rest", SyntaxShape::String, "rest value string") + .plugin_examples(vec![PluginExample { + example: "nu-example-1 3 bb".into(), + description: "running example with an int value and string value".into(), + result: None, + }]) + .category(Category::Experimental) + } + + fn run( + &self, + plugin: &Example, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + plugin.print_values(1, call, input)?; + + Ok(Value::nothing(call.head)) + } +} diff --git a/crates/nu_plugin_example/src/commands/nu_example_2.rs b/crates/nu_plugin_example/src/commands/nu_example_2.rs new file mode 100644 index 0000000000..b209375e4d --- /dev/null +++ b/crates/nu_plugin_example/src/commands/nu_example_2.rs @@ -0,0 +1,47 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{record, Category, PluginSignature, SyntaxShape, Value}; + +use crate::Example; + +pub struct NuExample2; + +impl SimplePluginCommand for NuExample2 { + type Plugin = Example; + + fn signature(&self) -> PluginSignature { + // The signature defines the usage of the command inside Nu, and also automatically + // generates its help page. + PluginSignature::build("nu-example-2") + .usage("PluginSignature test 2 for plugin. Returns list of records") + .required("a", SyntaxShape::Int, "required integer value") + .required("b", SyntaxShape::String, "required string value") + .switch("flag", "a flag for the signature", Some('f')) + .optional("opt", SyntaxShape::Int, "Optional number") + .named("named", SyntaxShape::String, "named string", Some('n')) + .rest("rest", SyntaxShape::String, "rest value string") + .category(Category::Experimental) + } + + fn run( + &self, + plugin: &Example, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + plugin.print_values(2, call, input)?; + + let vals = (0..10i64) + .map(|i| { + let record = record! { + "one" => Value::int(i, call.head), + "two" => Value::int(2 * i, call.head), + "three" => Value::int(3 * i, call.head), + }; + Value::record(record, call.head) + }) + .collect(); + + Ok(Value::list(vals, call.head)) + } +} diff --git a/crates/nu_plugin_example/src/commands/nu_example_3.rs b/crates/nu_plugin_example/src/commands/nu_example_3.rs new file mode 100644 index 0000000000..da4f2c7352 --- /dev/null +++ b/crates/nu_plugin_example/src/commands/nu_example_3.rs @@ -0,0 +1,40 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{Category, PluginSignature, SyntaxShape, Value}; + +use crate::Example; + +pub struct NuExample3; + +impl SimplePluginCommand for NuExample3 { + type Plugin = Example; + + fn signature(&self) -> PluginSignature { + // The signature defines the usage of the command inside Nu, and also automatically + // generates its help page. + PluginSignature::build("nu-example-3") + .usage("PluginSignature test 3 for plugin. Returns labeled error") + .required("a", SyntaxShape::Int, "required integer value") + .required("b", SyntaxShape::String, "required string value") + .switch("flag", "a flag for the signature", Some('f')) + .optional("opt", SyntaxShape::Int, "Optional number") + .named("named", SyntaxShape::String, "named string", Some('n')) + .rest("rest", SyntaxShape::String, "rest value string") + .category(Category::Experimental) + } + + fn run( + &self, + plugin: &Example, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + plugin.print_values(3, call, input)?; + + Err(LabeledError { + label: "ERROR from plugin".into(), + msg: "error message pointing to call head span".into(), + span: Some(call.head), + }) + } +} diff --git a/crates/nu_plugin_example/src/commands/nu_example_config.rs b/crates/nu_plugin_example/src/commands/nu_example_config.rs new file mode 100644 index 0000000000..1bbb11b7b4 --- /dev/null +++ b/crates/nu_plugin_example/src/commands/nu_example_config.rs @@ -0,0 +1,38 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{Category, PluginSignature, Type, Value}; + +use crate::Example; + +pub struct NuExampleConfig; + +impl SimplePluginCommand for NuExampleConfig { + type Plugin = Example; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("nu-example-config") + .usage("Show plugin configuration") + .extra_usage("The configuration is set under $env.config.plugins.example") + .category(Category::Experimental) + .search_terms(vec!["example".into(), "configuration".into()]) + .input_output_type(Type::Nothing, Type::Table(vec![])) + } + + fn run( + &self, + _plugin: &Example, + engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + let config = engine.get_plugin_config()?; + match config { + Some(config) => Ok(config.clone()), + None => Err(LabeledError { + label: "No config sent".into(), + msg: "Configuration for this plugin was not found in `$env.config.plugins.example`" + .into(), + span: Some(call.head), + }), + } + } +} diff --git a/crates/nu_plugin_example/src/commands/nu_example_disable_gc.rs b/crates/nu_plugin_example/src/commands/nu_example_disable_gc.rs new file mode 100644 index 0000000000..681e221d7d --- /dev/null +++ b/crates/nu_plugin_example/src/commands/nu_example_disable_gc.rs @@ -0,0 +1,52 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{Category, PluginSignature, Value}; + +use crate::Example; + +pub struct NuExampleDisableGc; + +impl SimplePluginCommand for NuExampleDisableGc { + type Plugin = Example; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("nu-example-disable-gc") + .usage("Disable the plugin garbage collector for `example`") + .extra_usage( + "\ +Plugins are garbage collected by default after a period of inactivity. This +behavior is configurable with `$env.config.plugin_gc.default`, or to change it +specifically for the example plugin, use +`$env.config.plugin_gc.plugins.example`. + +This command demonstrates how plugins can control this behavior and disable GC +temporarily if they need to. It is still possible to stop the plugin explicitly +using `plugin stop example`.", + ) + .search_terms(vec![ + "example".into(), + "gc".into(), + "plugin_gc".into(), + "garbage".into(), + ]) + .switch("reset", "Turn the garbage collector back on", None) + .category(Category::Experimental) + } + + fn run( + &self, + _plugin: &Example, + engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + let disabled = !call.has_flag("reset")?; + engine.set_gc_disabled(disabled)?; + Ok(Value::string( + format!( + "The plugin garbage collector for `example` is now *{}*.", + if disabled { "disabled" } else { "enabled" } + ), + call.head, + )) + } +} diff --git a/crates/nu_plugin_example/src/commands/nu_example_env.rs b/crates/nu_plugin_example/src/commands/nu_example_env.rs new file mode 100644 index 0000000000..cca15c1b71 --- /dev/null +++ b/crates/nu_plugin_example/src/commands/nu_example_env.rs @@ -0,0 +1,49 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{Category, PluginSignature, SyntaxShape, Type, Value}; + +use crate::Example; + +pub struct NuExampleEnv; + +impl SimplePluginCommand for NuExampleEnv { + type Plugin = Example; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("nu-example-env") + .usage("Get environment variable(s)") + .extra_usage("Returns all environment variables if no name provided") + .category(Category::Experimental) + .optional( + "name", + SyntaxShape::String, + "The name of the environment variable to get", + ) + .switch("cwd", "Get current working directory instead", None) + .search_terms(vec!["example".into(), "env".into()]) + .input_output_type(Type::Nothing, Type::Any) + } + + fn run( + &self, + _plugin: &Example, + engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + if call.has_flag("cwd")? { + // Get working directory + Ok(Value::string(engine.get_current_dir()?, call.head)) + } else if let Some(name) = call.opt::(0)? { + // Get single env var + Ok(engine + .get_env_var(name)? + .unwrap_or(Value::nothing(call.head))) + } else { + // Get all env vars, converting the map to a record + Ok(Value::record( + engine.get_env_vars()?.into_iter().collect(), + call.head, + )) + } + } +} diff --git a/crates/nu_plugin_example/src/example.rs b/crates/nu_plugin_example/src/example.rs index c7f4277fd9..b9e053946d 100644 --- a/crates/nu_plugin_example/src/example.rs +++ b/crates/nu_plugin_example/src/example.rs @@ -1,26 +1,10 @@ -use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError}; -use nu_protocol::{record, Value}; +use nu_plugin::{EvaluatedCall, LabeledError}; +use nu_protocol::Value; + pub struct Example; impl Example { - pub fn config( - &self, - engine: &EngineInterface, - call: &EvaluatedCall, - ) -> Result { - let config = engine.get_plugin_config()?; - match config { - Some(config) => Ok(config.clone()), - None => Err(LabeledError { - label: "No config sent".into(), - msg: "Configuration for this plugin was not found in `$env.config.plugins.example`" - .into(), - span: Some(call.head), - }), - } - } - - fn print_values( + pub fn print_values( &self, index: u32, call: &EvaluatedCall, @@ -66,75 +50,4 @@ impl Example { Ok(()) } - - pub fn test1(&self, call: &EvaluatedCall, input: &Value) -> Result { - self.print_values(1, call, input)?; - - Ok(Value::nothing(call.head)) - } - - pub fn test2(&self, call: &EvaluatedCall, input: &Value) -> Result { - self.print_values(2, call, input)?; - - let vals = (0..10i64) - .map(|i| { - let record = record! { - "one" => Value::int(i, call.head), - "two" => Value::int(2 * i, call.head), - "three" => Value::int(3 * i, call.head), - }; - Value::record(record, call.head) - }) - .collect(); - - Ok(Value::list(vals, call.head)) - } - - pub fn test3(&self, call: &EvaluatedCall, input: &Value) -> Result { - self.print_values(3, call, input)?; - - Err(LabeledError { - label: "ERROR from plugin".into(), - msg: "error message pointing to call head span".into(), - span: Some(call.head), - }) - } - - pub fn env( - &self, - engine: &EngineInterface, - call: &EvaluatedCall, - ) -> Result { - if call.has_flag("cwd")? { - // Get working directory - Ok(Value::string(engine.get_current_dir()?, call.head)) - } else if let Some(name) = call.opt::(0)? { - // Get single env var - Ok(engine - .get_env_var(name)? - .unwrap_or(Value::nothing(call.head))) - } else { - // Get all env vars, converting the map to a record - Ok(Value::record( - engine.get_env_vars()?.into_iter().collect(), - call.head, - )) - } - } - - pub fn disable_gc( - &self, - engine: &EngineInterface, - call: &EvaluatedCall, - ) -> Result { - let disabled = !call.has_flag("reset")?; - engine.set_gc_disabled(disabled)?; - Ok(Value::string( - format!( - "The plugin garbage collector for `example` is now *{}*.", - if disabled { "disabled" } else { "enabled" } - ), - call.head, - )) - } } diff --git a/crates/nu_plugin_example/src/lib.rs b/crates/nu_plugin_example/src/lib.rs index 995d09e8e1..11c901b13a 100644 --- a/crates/nu_plugin_example/src/lib.rs +++ b/crates/nu_plugin_example/src/lib.rs @@ -1,4 +1,24 @@ -mod example; -mod nu; +use nu_plugin::{Plugin, PluginCommand}; +mod commands; +mod example; + +pub use commands::*; pub use example::Example; + +impl Plugin for Example { + fn commands(&self) -> Vec>> { + // This is a list of all of the commands you would like Nu to register when your plugin is + // loaded. + // + // If it doesn't appear on this list, it won't be added. + vec![ + Box::new(NuExample1), + Box::new(NuExample2), + Box::new(NuExample3), + Box::new(NuExampleConfig), + Box::new(NuExampleEnv), + Box::new(NuExampleDisableGc), + ] + } +} diff --git a/crates/nu_plugin_example/src/nu/mod.rs b/crates/nu_plugin_example/src/nu/mod.rs deleted file mode 100644 index 4f1a1ab62a..0000000000 --- a/crates/nu_plugin_example/src/nu/mod.rs +++ /dev/null @@ -1,109 +0,0 @@ -use crate::Example; -use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, Plugin}; -use nu_protocol::{Category, PluginExample, PluginSignature, SyntaxShape, Type, Value}; - -impl Plugin for Example { - fn signature(&self) -> Vec { - // It is possible to declare multiple signature in a plugin - // Each signature will be converted to a command declaration once the - // plugin is registered to nushell - vec![ - PluginSignature::build("nu-example-1") - .usage("PluginSignature test 1 for plugin. Returns Value::Nothing") - .extra_usage("Extra usage for nu-example-1") - .search_terms(vec!["example".into()]) - .required("a", SyntaxShape::Int, "required integer value") - .required("b", SyntaxShape::String, "required string value") - .switch("flag", "a flag for the signature", Some('f')) - .optional("opt", SyntaxShape::Int, "Optional number") - .named("named", SyntaxShape::String, "named string", Some('n')) - .rest("rest", SyntaxShape::String, "rest value string") - .plugin_examples(vec![PluginExample { - example: "nu-example-1 3 bb".into(), - description: "running example with an int value and string value".into(), - result: None, - }]) - .category(Category::Experimental), - PluginSignature::build("nu-example-2") - .usage("PluginSignature test 2 for plugin. Returns list of records") - .required("a", SyntaxShape::Int, "required integer value") - .required("b", SyntaxShape::String, "required string value") - .switch("flag", "a flag for the signature", Some('f')) - .optional("opt", SyntaxShape::Int, "Optional number") - .named("named", SyntaxShape::String, "named string", Some('n')) - .rest("rest", SyntaxShape::String, "rest value string") - .category(Category::Experimental), - PluginSignature::build("nu-example-3") - .usage("PluginSignature test 3 for plugin. Returns labeled error") - .required("a", SyntaxShape::Int, "required integer value") - .required("b", SyntaxShape::String, "required string value") - .switch("flag", "a flag for the signature", Some('f')) - .optional("opt", SyntaxShape::Int, "Optional number") - .named("named", SyntaxShape::String, "named string", Some('n')) - .rest("rest", SyntaxShape::String, "rest value string") - .category(Category::Experimental), - PluginSignature::build("nu-example-config") - .usage("Show plugin configuration") - .extra_usage("The configuration is set under $env.config.plugins.example") - .category(Category::Experimental) - .search_terms(vec!["example".into(), "configuration".into()]) - .input_output_type(Type::Nothing, Type::Table(vec![])), - PluginSignature::build("nu-example-env") - .usage("Get environment variable(s)") - .extra_usage("Returns all environment variables if no name provided") - .category(Category::Experimental) - .optional( - "name", - SyntaxShape::String, - "The name of the environment variable to get", - ) - .switch("cwd", "Get current working directory instead", None) - .search_terms(vec!["example".into(), "env".into()]) - .input_output_type(Type::Nothing, Type::Any), - PluginSignature::build("nu-example-disable-gc") - .usage("Disable the plugin garbage collector for `example`") - .extra_usage( - "\ -Plugins are garbage collected by default after a period of inactivity. This -behavior is configurable with `$env.config.plugin_gc.default`, or to change it -specifically for the example plugin, use -`$env.config.plugin_gc.plugins.example`. - -This command demonstrates how plugins can control this behavior and disable GC -temporarily if they need to. It is still possible to stop the plugin explicitly -using `plugin stop example`.", - ) - .search_terms(vec![ - "example".into(), - "gc".into(), - "plugin_gc".into(), - "garbage".into(), - ]) - .switch("reset", "Turn the garbage collector back on", None) - .category(Category::Experimental), - ] - } - - fn run( - &self, - name: &str, - engine: &EngineInterface, - call: &EvaluatedCall, - input: &Value, - ) -> Result { - // You can use the name to identify what plugin signature was called - match name { - "nu-example-1" => self.test1(call, input), - "nu-example-2" => self.test2(call, input), - "nu-example-3" => self.test3(call, input), - "nu-example-config" => self.config(engine, call), - "nu-example-env" => self.env(engine, call), - "nu-example-disable-gc" => self.disable_gc(engine, call), - _ => Err(LabeledError { - label: "Plugin call with wrong name signature".into(), - msg: "the signature used to call the plugin does not match any name in the plugin signature vector".into(), - span: Some(call.head), - }), - } - } -} diff --git a/crates/nu_plugin_formats/src/from/eml.rs b/crates/nu_plugin_formats/src/from/eml.rs index 8ce2dc1af3..ed92bf105f 100644 --- a/crates/nu_plugin_formats/src/from/eml.rs +++ b/crates/nu_plugin_formats/src/from/eml.rs @@ -1,18 +1,48 @@ use eml_parser::eml::*; use eml_parser::EmlParser; use indexmap::map::IndexMap; -use nu_plugin::{EvaluatedCall, LabeledError}; -use nu_protocol::{record, PluginExample, ShellError, Span, Value}; +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{ + record, Category, PluginExample, PluginSignature, ShellError, Span, SyntaxShape, Type, Value, +}; + +use crate::FromCmds; const DEFAULT_BODY_PREVIEW: usize = 50; pub const CMD_NAME: &str = "from eml"; -pub fn from_eml_call(call: &EvaluatedCall, input: &Value) -> Result { - let preview_body: usize = call - .get_flag::("preview-body")? - .map(|l| if l < 0 { 0 } else { l as usize }) - .unwrap_or(DEFAULT_BODY_PREVIEW); - from_eml(input, preview_body, call.head) +pub struct FromEml; + +impl SimplePluginCommand for FromEml { + type Plugin = FromCmds; + + fn signature(&self) -> nu_protocol::PluginSignature { + PluginSignature::build(CMD_NAME) + .input_output_types(vec![(Type::String, Type::Record(vec![]))]) + .named( + "preview-body", + SyntaxShape::Int, + "How many bytes of the body to preview", + Some('b'), + ) + .usage("Parse text as .eml and create record.") + .plugin_examples(examples()) + .category(Category::Formats) + } + + fn run( + &self, + _plugin: &FromCmds, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + let preview_body: usize = call + .get_flag::("preview-body")? + .map(|l| if l < 0 { 0 } else { l as usize }) + .unwrap_or(DEFAULT_BODY_PREVIEW); + from_eml(input, preview_body, call.head) + } } pub fn examples() -> Vec { diff --git a/crates/nu_plugin_formats/src/from/ics.rs b/crates/nu_plugin_formats/src/from/ics.rs index a0a372fe9c..df20c33d28 100644 --- a/crates/nu_plugin_formats/src/from/ics.rs +++ b/crates/nu_plugin_formats/src/from/ics.rs @@ -1,52 +1,76 @@ use ical::parser::ical::component::*; use ical::property::Property; use indexmap::map::IndexMap; -use nu_plugin::{EvaluatedCall, LabeledError}; -use nu_protocol::{record, PluginExample, ShellError, Span, Value}; +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{ + record, Category, PluginExample, PluginSignature, ShellError, Span, Type, Value, +}; use std::io::BufReader; +use crate::FromCmds; + pub const CMD_NAME: &str = "from ics"; -pub fn from_ics_call(call: &EvaluatedCall, input: &Value) -> Result { - let span = input.span(); - let input_string = input.coerce_str()?; - let head = call.head; +pub struct FromIcs; - let input_string = input_string - .lines() - .enumerate() - .map(|(i, x)| { - if i == 0 { - x.trim().to_string() - } else if x.len() > 1 && (x.starts_with(' ') || x.starts_with('\t')) { - x[1..].trim_end().to_string() - } else { - format!("\n{}", x.trim()) - } - }) - .collect::(); +impl SimplePluginCommand for FromIcs { + type Plugin = FromCmds; - let input_bytes = input_string.as_bytes(); - let buf_reader = BufReader::new(input_bytes); - let parser = ical::IcalParser::new(buf_reader); - - let mut output = vec![]; - - for calendar in parser { - match calendar { - Ok(c) => output.push(calendar_to_value(c, head)), - Err(e) => output.push(Value::error( - ShellError::UnsupportedInput { - msg: format!("input cannot be parsed as .ics ({e})"), - input: "value originates from here".into(), - msg_span: head, - input_span: span, - }, - span, - )), - } + fn signature(&self) -> nu_protocol::PluginSignature { + PluginSignature::build(CMD_NAME) + .input_output_types(vec![(Type::String, Type::Table(vec![]))]) + .usage("Parse text as .ics and create table.") + .plugin_examples(examples()) + .category(Category::Formats) + } + + fn run( + &self, + _plugin: &FromCmds, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + let span = input.span(); + let input_string = input.coerce_str()?; + let head = call.head; + + let input_string = input_string + .lines() + .enumerate() + .map(|(i, x)| { + if i == 0 { + x.trim().to_string() + } else if x.len() > 1 && (x.starts_with(' ') || x.starts_with('\t')) { + x[1..].trim_end().to_string() + } else { + format!("\n{}", x.trim()) + } + }) + .collect::(); + + let input_bytes = input_string.as_bytes(); + let buf_reader = BufReader::new(input_bytes); + let parser = ical::IcalParser::new(buf_reader); + + let mut output = vec![]; + + for calendar in parser { + match calendar { + Ok(c) => output.push(calendar_to_value(c, head)), + Err(e) => output.push(Value::error( + ShellError::UnsupportedInput { + msg: format!("input cannot be parsed as .ics ({e})"), + input: "value originates from here".into(), + msg_span: head, + input_span: span, + }, + span, + )), + } + } + Ok(Value::list(output, head)) } - Ok(Value::list(output, head)) } pub fn examples() -> Vec { diff --git a/crates/nu_plugin_formats/src/from/ini.rs b/crates/nu_plugin_formats/src/from/ini.rs index ee5c8eec7b..18a7d511aa 100644 --- a/crates/nu_plugin_formats/src/from/ini.rs +++ b/crates/nu_plugin_formats/src/from/ini.rs @@ -1,52 +1,76 @@ -use nu_plugin::{EvaluatedCall, LabeledError}; -use nu_protocol::{record, PluginExample, Record, ShellError, Value}; +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{ + record, Category, PluginExample, PluginSignature, Record, ShellError, Type, Value, +}; + +use crate::FromCmds; pub const CMD_NAME: &str = "from ini"; -pub fn from_ini_call(call: &EvaluatedCall, input: &Value) -> Result { - let span = input.span(); - let input_string = input.coerce_str()?; - let head = call.head; +pub struct FromIni; - let ini_config: Result = ini::Ini::load_from_str(&input_string); - match ini_config { - Ok(config) => { - let mut sections = Record::new(); +impl SimplePluginCommand for FromIni { + type Plugin = FromCmds; - for (section, properties) in config.iter() { - let mut section_record = Record::new(); + fn signature(&self) -> PluginSignature { + PluginSignature::build(CMD_NAME) + .input_output_types(vec![(Type::String, Type::Record(vec![]))]) + .usage("Parse text as .ini and create table.") + .plugin_examples(examples()) + .category(Category::Formats) + } - // section's key value pairs - for (key, value) in properties.iter() { - section_record.push(key, Value::string(value, span)); - } + fn run( + &self, + _plugin: &FromCmds, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + let span = input.span(); + let input_string = input.coerce_str()?; + let head = call.head; - let section_record = Value::record(section_record, span); + let ini_config: Result = ini::Ini::load_from_str(&input_string); + match ini_config { + Ok(config) => { + let mut sections = Record::new(); - // section - match section { - Some(section_name) => { - sections.push(section_name, section_record); + for (section, properties) in config.iter() { + let mut section_record = Record::new(); + + // section's key value pairs + for (key, value) in properties.iter() { + section_record.push(key, Value::string(value, span)); } - None => { - // Section (None) allows for key value pairs without a section - if !properties.is_empty() { - sections.push(String::new(), section_record); + + let section_record = Value::record(section_record, span); + + // section + match section { + Some(section_name) => { + sections.push(section_name, section_record); + } + None => { + // Section (None) allows for key value pairs without a section + if !properties.is_empty() { + sections.push(String::new(), section_record); + } } } } - } - // all sections with all its key value pairs - Ok(Value::record(sections, span)) + // all sections with all its key value pairs + Ok(Value::record(sections, span)) + } + Err(err) => Err(ShellError::UnsupportedInput { + msg: format!("Could not load ini: {err}"), + input: "value originates from here".into(), + msg_span: head, + input_span: span, + } + .into()), } - Err(err) => Err(ShellError::UnsupportedInput { - msg: format!("Could not load ini: {err}"), - input: "value originates from here".into(), - msg_span: head, - input_span: span, - } - .into()), } } diff --git a/crates/nu_plugin_formats/src/from/vcf.rs b/crates/nu_plugin_formats/src/from/vcf.rs index 9262d3cc25..e3f3008234 100644 --- a/crates/nu_plugin_formats/src/from/vcf.rs +++ b/crates/nu_plugin_formats/src/from/vcf.rs @@ -1,49 +1,73 @@ use ical::parser::vcard::component::*; use ical::property::Property; use indexmap::map::IndexMap; -use nu_plugin::{EvaluatedCall, LabeledError}; -use nu_protocol::{record, PluginExample, ShellError, Span, Value}; +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{ + record, Category, PluginExample, PluginSignature, ShellError, Span, Type, Value, +}; + +use crate::FromCmds; pub const CMD_NAME: &str = "from vcf"; -pub fn from_vcf_call(call: &EvaluatedCall, input: &Value) -> Result { - let span = input.span(); - let input_string = input.coerce_str()?; - let head = call.head; +pub struct FromVcf; - let input_string = input_string - .lines() - .enumerate() - .map(|(i, x)| { - if i == 0 { - x.trim().to_string() - } else if x.len() > 1 && (x.starts_with(' ') || x.starts_with('\t')) { - x[1..].trim_end().to_string() - } else { - format!("\n{}", x.trim()) - } - }) - .collect::(); +impl SimplePluginCommand for FromVcf { + type Plugin = FromCmds; - let input_bytes = input_string.as_bytes(); - let cursor = std::io::Cursor::new(input_bytes); - let parser = ical::VcardParser::new(cursor); + fn signature(&self) -> PluginSignature { + PluginSignature::build(CMD_NAME) + .input_output_types(vec![(Type::String, Type::Table(vec![]))]) + .usage("Parse text as .vcf and create table.") + .plugin_examples(examples()) + .category(Category::Formats) + } - let iter = parser.map(move |contact| match contact { - Ok(c) => contact_to_value(c, head), - Err(e) => Value::error( - ShellError::UnsupportedInput { - msg: format!("input cannot be parsed as .vcf ({e})"), - input: "value originates from here".into(), - msg_span: head, - input_span: span, - }, - span, - ), - }); + fn run( + &self, + _plugin: &FromCmds, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + let span = input.span(); + let input_string = input.coerce_str()?; + let head = call.head; - let collected: Vec<_> = iter.collect(); - Ok(Value::list(collected, head)) + let input_string = input_string + .lines() + .enumerate() + .map(|(i, x)| { + if i == 0 { + x.trim().to_string() + } else if x.len() > 1 && (x.starts_with(' ') || x.starts_with('\t')) { + x[1..].trim_end().to_string() + } else { + format!("\n{}", x.trim()) + } + }) + .collect::(); + + let input_bytes = input_string.as_bytes(); + let cursor = std::io::Cursor::new(input_bytes); + let parser = ical::VcardParser::new(cursor); + + let iter = parser.map(move |contact| match contact { + Ok(c) => contact_to_value(c, head), + Err(e) => Value::error( + ShellError::UnsupportedInput { + msg: format!("input cannot be parsed as .vcf ({e})"), + input: "value originates from here".into(), + msg_span: head, + input_span: span, + }, + span, + ), + }); + + let collected: Vec<_> = iter.collect(); + Ok(Value::list(collected, head)) + } } pub fn examples() -> Vec { diff --git a/crates/nu_plugin_formats/src/lib.rs b/crates/nu_plugin_formats/src/lib.rs index 0f29dd9d7a..748d29cd21 100644 --- a/crates/nu_plugin_formats/src/lib.rs +++ b/crates/nu_plugin_formats/src/lib.rs @@ -1,60 +1,21 @@ mod from; -use from::{eml, ics, ini, vcf}; -use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, Plugin}; -use nu_protocol::{Category, PluginSignature, SyntaxShape, Type, Value}; +use nu_plugin::{Plugin, PluginCommand}; + +pub use from::eml::FromEml; +pub use from::ics::FromIcs; +pub use from::ini::FromIni; +pub use from::vcf::FromVcf; pub struct FromCmds; impl Plugin for FromCmds { - fn signature(&self) -> Vec { + fn commands(&self) -> Vec>> { vec![ - PluginSignature::build(eml::CMD_NAME) - .input_output_types(vec![(Type::String, Type::Record(vec![]))]) - .named( - "preview-body", - SyntaxShape::Int, - "How many bytes of the body to preview", - Some('b'), - ) - .usage("Parse text as .eml and create record.") - .plugin_examples(eml::examples()) - .category(Category::Formats), - PluginSignature::build(ics::CMD_NAME) - .input_output_types(vec![(Type::String, Type::Table(vec![]))]) - .usage("Parse text as .ics and create table.") - .plugin_examples(ics::examples()) - .category(Category::Formats), - PluginSignature::build(vcf::CMD_NAME) - .input_output_types(vec![(Type::String, Type::Table(vec![]))]) - .usage("Parse text as .vcf and create table.") - .plugin_examples(vcf::examples()) - .category(Category::Formats), - PluginSignature::build(ini::CMD_NAME) - .input_output_types(vec![(Type::String, Type::Record(vec![]))]) - .usage("Parse text as .ini and create table.") - .plugin_examples(ini::examples()) - .category(Category::Formats), + Box::new(FromEml), + Box::new(FromIcs), + Box::new(FromIni), + Box::new(FromVcf), ] } - - fn run( - &self, - name: &str, - _engine: &EngineInterface, - call: &EvaluatedCall, - input: &Value, - ) -> Result { - match name { - eml::CMD_NAME => eml::from_eml_call(call, input), - ics::CMD_NAME => ics::from_ics_call(call, input), - vcf::CMD_NAME => vcf::from_vcf_call(call, input), - ini::CMD_NAME => ini::from_ini_call(call, input), - _ => Err(LabeledError { - label: "Plugin call with wrong name signature".into(), - msg: "the signature used to call the plugin does not match any name in the plugin signature vector".into(), - span: Some(call.head), - }), - } - } } diff --git a/crates/nu_plugin_gstat/src/lib.rs b/crates/nu_plugin_gstat/src/lib.rs index c13f882478..4dc5ad3d02 100644 --- a/crates/nu_plugin_gstat/src/lib.rs +++ b/crates/nu_plugin_gstat/src/lib.rs @@ -2,3 +2,4 @@ mod gstat; mod nu; pub use gstat::GStat; +pub use nu::GStatPlugin; diff --git a/crates/nu_plugin_gstat/src/main.rs b/crates/nu_plugin_gstat/src/main.rs index d28d6d7e52..fe1ad625c0 100644 --- a/crates/nu_plugin_gstat/src/main.rs +++ b/crates/nu_plugin_gstat/src/main.rs @@ -1,6 +1,6 @@ use nu_plugin::{serve_plugin, MsgPackSerializer}; -use nu_plugin_gstat::GStat; +use nu_plugin_gstat::GStatPlugin; fn main() { - serve_plugin(&GStat::new(), MsgPackSerializer {}) + serve_plugin(&GStatPlugin, MsgPackSerializer {}) } diff --git a/crates/nu_plugin_gstat/src/nu/mod.rs b/crates/nu_plugin_gstat/src/nu/mod.rs index 7baf85663e..9092c99af1 100644 --- a/crates/nu_plugin_gstat/src/nu/mod.rs +++ b/crates/nu_plugin_gstat/src/nu/mod.rs @@ -1,26 +1,34 @@ use crate::GStat; -use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, Plugin}; +use nu_plugin::{ + EngineInterface, EvaluatedCall, LabeledError, Plugin, PluginCommand, SimplePluginCommand, +}; use nu_protocol::{Category, PluginSignature, Spanned, SyntaxShape, Value}; -impl Plugin for GStat { - fn signature(&self) -> Vec { - vec![PluginSignature::build("gstat") +pub struct GStatPlugin; + +impl Plugin for GStatPlugin { + fn commands(&self) -> Vec>> { + vec![Box::new(GStat)] + } +} + +impl SimplePluginCommand for GStat { + type Plugin = GStatPlugin; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("gstat") .usage("Get the git status of a repo") .optional("path", SyntaxShape::Filepath, "path to repo") - .category(Category::Custom("prompt".to_string()))] + .category(Category::Custom("prompt".to_string())) } fn run( &self, - name: &str, + _plugin: &GStatPlugin, engine: &EngineInterface, call: &EvaluatedCall, input: &Value, ) -> Result { - if name != "gstat" { - return Ok(Value::nothing(call.head)); - } - let repo_path: Option> = call.opt(0)?; // eprintln!("input value: {:#?}", &input); let current_dir = engine.get_current_dir()?; diff --git a/crates/nu_plugin_inc/src/lib.rs b/crates/nu_plugin_inc/src/lib.rs index e5428d8e6f..660506cdc2 100644 --- a/crates/nu_plugin_inc/src/lib.rs +++ b/crates/nu_plugin_inc/src/lib.rs @@ -2,3 +2,4 @@ mod inc; mod nu; pub use inc::Inc; +pub use nu::IncPlugin; diff --git a/crates/nu_plugin_inc/src/main.rs b/crates/nu_plugin_inc/src/main.rs index 47bcb3f950..4b8719dbc7 100644 --- a/crates/nu_plugin_inc/src/main.rs +++ b/crates/nu_plugin_inc/src/main.rs @@ -1,6 +1,6 @@ use nu_plugin::{serve_plugin, JsonSerializer}; -use nu_plugin_inc::Inc; +use nu_plugin_inc::IncPlugin; fn main() { - serve_plugin(&Inc::new(), JsonSerializer {}) + serve_plugin(&IncPlugin, JsonSerializer {}) } diff --git a/crates/nu_plugin_inc/src/nu/mod.rs b/crates/nu_plugin_inc/src/nu/mod.rs index 0dc7b078ac..ea75b7246b 100644 --- a/crates/nu_plugin_inc/src/nu/mod.rs +++ b/crates/nu_plugin_inc/src/nu/mod.rs @@ -1,11 +1,23 @@ use crate::inc::SemVerAction; use crate::Inc; -use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, Plugin}; +use nu_plugin::{ + EngineInterface, EvaluatedCall, LabeledError, Plugin, PluginCommand, SimplePluginCommand, +}; use nu_protocol::{ast::CellPath, PluginSignature, SyntaxShape, Value}; -impl Plugin for Inc { - fn signature(&self) -> Vec { - vec![PluginSignature::build("inc") +pub struct IncPlugin; + +impl Plugin for IncPlugin { + fn commands(&self) -> Vec>> { + vec![Box::new(Inc::new())] + } +} + +impl SimplePluginCommand for Inc { + type Plugin = IncPlugin; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("inc") .usage("Increment a value or version. Optionally use the column of a table.") .optional("cell_path", SyntaxShape::CellPath, "cell path to update") .switch( @@ -22,20 +34,16 @@ impl Plugin for Inc { "patch", "increment the patch version (eg 1.2.1 -> 1.2.2)", Some('p'), - )] + ) } fn run( &self, - name: &str, + _plugin: &IncPlugin, _engine: &EngineInterface, call: &EvaluatedCall, input: &Value, ) -> Result { - if name != "inc" { - return Ok(Value::nothing(call.head)); - } - let mut inc = self.clone(); let cell_path: Option = call.opt(0)?; diff --git a/crates/nu_plugin_query/src/lib.rs b/crates/nu_plugin_query/src/lib.rs index 4b8ebba399..8027c67493 100644 --- a/crates/nu_plugin_query/src/lib.rs +++ b/crates/nu_plugin_query/src/lib.rs @@ -1,4 +1,3 @@ -mod nu; mod query; mod query_json; mod query_web; @@ -6,7 +5,7 @@ mod query_xml; mod web_tables; pub use query::Query; -pub use query_json::execute_json_query; -pub use query_web::parse_selector_params; -pub use query_xml::execute_xpath_query; +pub use query_json::{execute_json_query, QueryJson}; +pub use query_web::{parse_selector_params, QueryWeb}; +pub use query_xml::{execute_xpath_query, QueryXml}; pub use web_tables::WebTable; diff --git a/crates/nu_plugin_query/src/nu/mod.rs b/crates/nu_plugin_query/src/nu/mod.rs deleted file mode 100644 index b726ce1961..0000000000 --- a/crates/nu_plugin_query/src/nu/mod.rs +++ /dev/null @@ -1,95 +0,0 @@ -use crate::Query; -use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, Plugin}; -use nu_protocol::{Category, PluginExample, PluginSignature, Spanned, SyntaxShape, Value}; - -impl Plugin for Query { - fn signature(&self) -> Vec { - vec![ - PluginSignature::build("query") - .usage("Show all the query commands") - .category(Category::Filters), - - PluginSignature::build("query json") - .usage("execute json query on json file (open --raw | query json 'query string')") - .required("query", SyntaxShape::String, "json query") - .category(Category::Filters), - - PluginSignature::build("query xml") - .usage("execute xpath query on xml") - .required("query", SyntaxShape::String, "xpath query") - .category(Category::Filters), - - PluginSignature::build("query web") - .usage("execute selector query on html/web") - .named("query", SyntaxShape::String, "selector query", Some('q')) - .switch("as-html", "return the query output as html", Some('m')) - .plugin_examples(web_examples()) - .named( - "attribute", - SyntaxShape::String, - "downselect based on the given attribute", - Some('a'), - ) - .named( - "as-table", - SyntaxShape::List(Box::new(SyntaxShape::String)), - "find table based on column header list", - Some('t'), - ) - .switch( - "inspect", - "run in inspect mode to provide more information for determining column headers", - Some('i'), - ) - .category(Category::Network), - ] - } - - fn run( - &self, - name: &str, - _engine: &EngineInterface, - call: &EvaluatedCall, - input: &Value, - ) -> Result { - // You can use the name to identify what plugin signature was called - let path: Option> = call.opt(0)?; - - match name { - "query" => { - self.query(name, call, input, path) - } - "query json" => self.query_json( name, call, input, path), - "query web" => self.query_web(name, call, input, path), - "query xml" => self.query_xml(name, call, input, path), - _ => Err(LabeledError { - label: "Plugin call with wrong name signature".into(), - msg: "the signature used to call the plugin does not match any name in the plugin signature vector".into(), - span: Some(call.head), - }), - } - } -} - -pub fn web_examples() -> Vec { - vec![PluginExample { - example: "http get https://phoronix.com | query web --query 'header' | flatten".into(), - description: "Retrieve all `
` elements from phoronix.com website".into(), - result: None, - }, PluginExample { - example: "http get https://en.wikipedia.org/wiki/List_of_cities_in_India_by_population | - query web --as-table [City 'Population(2011)[3]' 'Population(2001)[3][a]' 'State or unionterritory' 'Ref']".into(), - description: "Retrieve a html table from Wikipedia and parse it into a nushell table using table headers as guides".into(), - result: None - }, - PluginExample { - example: "http get https://www.nushell.sh | query web --query 'h2, h2 + p' | each {str join} | group 2 | each {rotate --ccw tagline description} | flatten".into(), - description: "Pass multiple css selectors to extract several elements within single query, group the query results together and rotate them to create a table".into(), - result: None, - }, - PluginExample { - example: "http get https://example.org | query web --query a --attribute href".into(), - description: "Retrieve a specific html attribute instead of the default text".into(), - result: None, - }] -} diff --git a/crates/nu_plugin_query/src/query.rs b/crates/nu_plugin_query/src/query.rs index d0da236a4c..bc38a26d14 100644 --- a/crates/nu_plugin_query/src/query.rs +++ b/crates/nu_plugin_query/src/query.rs @@ -1,9 +1,10 @@ -use crate::query_json::execute_json_query; -use crate::query_web::parse_selector_params; -use crate::query_xml::execute_xpath_query; +use crate::query_json::QueryJson; +use crate::query_web::QueryWeb; +use crate::query_xml::QueryXml; + use nu_engine::documentation::get_flags_section; -use nu_plugin::{EvaluatedCall, LabeledError, Plugin}; -use nu_protocol::{PluginSignature, Spanned, Value}; +use nu_plugin::{EvaluatedCall, LabeledError, Plugin, PluginCommand, SimplePluginCommand}; +use nu_protocol::{Category, PluginSignature, Value}; use std::fmt::Write; #[derive(Default)] @@ -17,48 +18,50 @@ impl Query { pub fn usage() -> &'static str { "Usage: query" } +} - pub fn query( - &self, - _name: &str, - call: &EvaluatedCall, - _value: &Value, - _path: Option>, - ) -> Result { - let help = get_brief_subcommand_help(&Query.signature()); - Ok(Value::string(help, call.head)) - } - - pub fn query_json( - &self, - name: &str, - call: &EvaluatedCall, - input: &Value, - query: Option>, - ) -> Result { - execute_json_query(name, call, input, query) - } - pub fn query_web( - &self, - _name: &str, - call: &EvaluatedCall, - input: &Value, - _rest: Option>, - ) -> Result { - parse_selector_params(call, input) - } - pub fn query_xml( - &self, - name: &str, - call: &EvaluatedCall, - input: &Value, - query: Option>, - ) -> Result { - execute_xpath_query(name, call, input, query) +impl Plugin for Query { + fn commands(&self) -> Vec>> { + vec![ + Box::new(QueryCommand), + Box::new(QueryJson), + Box::new(QueryXml), + Box::new(QueryWeb), + ] } } -pub fn get_brief_subcommand_help(sigs: &[PluginSignature]) -> String { +// With no subcommand +pub struct QueryCommand; + +impl SimplePluginCommand for QueryCommand { + type Plugin = Query; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("query") + .usage("Show all the query commands") + .category(Category::Filters) + } + + fn run( + &self, + _plugin: &Query, + _engine: &nu_plugin::EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + let help = get_brief_subcommand_help(); + Ok(Value::string(help, call.head)) + } +} + +pub fn get_brief_subcommand_help() -> String { + let sigs: Vec<_> = Query + .commands() + .into_iter() + .map(|cmd| cmd.signature()) + .collect(); + let mut help = String::new(); let _ = write!(help, "{}\n\n", sigs[0].sig.usage); let _ = write!(help, "Usage:\n > {}\n\n", sigs[0].sig.name); diff --git a/crates/nu_plugin_query/src/query_json.rs b/crates/nu_plugin_query/src/query_json.rs index 758572579a..b4960f7875 100644 --- a/crates/nu_plugin_query/src/query_json.rs +++ b/crates/nu_plugin_query/src/query_json.rs @@ -1,9 +1,37 @@ use gjson::Value as gjValue; -use nu_plugin::{EvaluatedCall, LabeledError}; -use nu_protocol::{Record, Span, Spanned, Value}; +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{Category, PluginSignature, Record, Span, Spanned, SyntaxShape, Value}; + +use crate::Query; + +pub struct QueryJson; + +impl SimplePluginCommand for QueryJson { + type Plugin = Query; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("query json") + .usage( + "execute json query on json file (open --raw | query json 'query string')", + ) + .required("query", SyntaxShape::String, "json query") + .category(Category::Filters) + } + + fn run( + &self, + _plugin: &Query, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + let query: Option> = call.opt(0)?; + + execute_json_query(call, input, query) + } +} pub fn execute_json_query( - _name: &str, call: &EvaluatedCall, input: &Value, query: Option>, diff --git a/crates/nu_plugin_query/src/query_web.rs b/crates/nu_plugin_query/src/query_web.rs index ba707c7f35..ce6d5247e2 100644 --- a/crates/nu_plugin_query/src/query_web.rs +++ b/crates/nu_plugin_query/src/query_web.rs @@ -1,8 +1,76 @@ -use crate::web_tables::WebTable; -use nu_plugin::{EvaluatedCall, LabeledError}; -use nu_protocol::{Record, Span, Value}; +use crate::{web_tables::WebTable, Query}; +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{Category, PluginExample, PluginSignature, Record, Span, SyntaxShape, Value}; use scraper::{Html, Selector as ScraperSelector}; +pub struct QueryWeb; + +impl SimplePluginCommand for QueryWeb { + type Plugin = Query; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("query web") + .usage("execute selector query on html/web") + .named("query", SyntaxShape::String, "selector query", Some('q')) + .switch("as-html", "return the query output as html", Some('m')) + .plugin_examples(web_examples()) + .named( + "attribute", + SyntaxShape::String, + "downselect based on the given attribute", + Some('a'), + ) + .named( + "as-table", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "find table based on column header list", + Some('t'), + ) + .switch( + "inspect", + "run in inspect mode to provide more information for determining column headers", + Some('i'), + ) + .category(Category::Network) + } + + fn run( + &self, + _plugin: &Query, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + parse_selector_params(call, input) + } +} + +pub fn web_examples() -> Vec { + vec![ + PluginExample { + example: "http get https://phoronix.com | query web --query 'header' | flatten".into(), + description: "Retrieve all `
` elements from phoronix.com website".into(), + result: None, + }, + PluginExample { + example: "http get https://en.wikipedia.org/wiki/List_of_cities_in_India_by_population | + query web --as-table [City 'Population(2011)[3]' 'Population(2001)[3][a]' 'State or unionterritory' 'Ref']".into(), + description: "Retrieve a html table from Wikipedia and parse it into a nushell table using table headers as guides".into(), + result: None + }, + PluginExample { + example: "http get https://www.nushell.sh | query web --query 'h2, h2 + p' | each {str join} | group 2 | each {rotate --ccw tagline description} | flatten".into(), + description: "Pass multiple css selectors to extract several elements within single query, group the query results together and rotate them to create a table".into(), + result: None, + }, + PluginExample { + example: "http get https://example.org | query web --query a --attribute href".into(), + description: "Retrieve a specific html attribute instead of the default text".into(), + result: None, + } + ] +} + pub struct Selector { pub query: String, pub as_html: bool, diff --git a/crates/nu_plugin_query/src/query_xml.rs b/crates/nu_plugin_query/src/query_xml.rs index 7f201d1540..fe65d47fda 100644 --- a/crates/nu_plugin_query/src/query_xml.rs +++ b/crates/nu_plugin_query/src/query_xml.rs @@ -1,10 +1,36 @@ -use nu_plugin::{EvaluatedCall, LabeledError}; -use nu_protocol::{record, Record, Span, Spanned, Value}; +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, SimplePluginCommand}; +use nu_protocol::{record, Category, PluginSignature, Record, Span, Spanned, SyntaxShape, Value}; use sxd_document::parser; use sxd_xpath::{Context, Factory}; +use crate::Query; + +pub struct QueryXml; + +impl SimplePluginCommand for QueryXml { + type Plugin = Query; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("query xml") + .usage("execute xpath query on xml") + .required("query", SyntaxShape::String, "xpath query") + .category(Category::Filters) + } + + fn run( + &self, + _plugin: &Query, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: &Value, + ) -> Result { + let query: Option> = call.opt(0)?; + + execute_xpath_query(call, input, query) + } +} + pub fn execute_xpath_query( - _name: &str, call: &EvaluatedCall, input: &Value, query: Option>, @@ -131,7 +157,7 @@ mod tests { span: Span::test_data(), }; - let actual = query("", &call, &text, Some(spanned_str)).expect("test should not fail"); + let actual = query(&call, &text, Some(spanned_str)).expect("test should not fail"); let expected = Value::list( vec![Value::test_record(record! { "count(//a/*[posit..." => Value::test_float(1.0), @@ -160,7 +186,7 @@ mod tests { span: Span::test_data(), }; - let actual = query("", &call, &text, Some(spanned_str)).expect("test should not fail"); + let actual = query(&call, &text, Some(spanned_str)).expect("test should not fail"); let expected = Value::list( vec![Value::test_record(record! { "count(//*[contain..." => Value::test_float(1.0), diff --git a/crates/nu_plugin_stream_example/README.md b/crates/nu_plugin_stream_example/README.md index 7957319bb0..f155d6a969 100644 --- a/crates/nu_plugin_stream_example/README.md +++ b/crates/nu_plugin_stream_example/README.md @@ -1,7 +1,6 @@ # Streaming Plugin Example -Crate with a simple example of the `StreamingPlugin` trait that needs to be implemented -in order to create a binary that can be registered into nushell declaration list +Crate with a simple example of a plugin with commands that produce streams ## `stream_example seq` diff --git a/crates/nu_plugin_stream_example/src/commands/collect_external.rs b/crates/nu_plugin_stream_example/src/commands/collect_external.rs new file mode 100644 index 0000000000..d603a568ad --- /dev/null +++ b/crates/nu_plugin_stream_example/src/commands/collect_external.rs @@ -0,0 +1,51 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, PluginCommand}; +use nu_protocol::{Category, PipelineData, PluginExample, PluginSignature, RawStream, Type, Value}; + +use crate::StreamExample; + +/// `> | stream_example collect-external` +pub struct CollectExternal; + +impl PluginCommand for CollectExternal { + type Plugin = StreamExample; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("stream_example collect-external") + .usage("Example transformer to raw external stream") + .search_terms(vec!["example".into()]) + .input_output_types(vec![ + (Type::List(Type::String.into()), Type::String), + (Type::List(Type::Binary.into()), Type::Binary), + ]) + .plugin_examples(vec![PluginExample { + example: "[a b] | stream_example collect-external".into(), + description: "collect strings into one stream".into(), + result: Some(Value::test_string("ab")), + }]) + .category(Category::Experimental) + } + + fn run( + &self, + _plugin: &StreamExample, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: PipelineData, + ) -> Result { + let stream = input.into_iter().map(|value| { + value + .as_str() + .map(|str| str.as_bytes()) + .or_else(|_| value.as_binary()) + .map(|bin| bin.to_vec()) + }); + Ok(PipelineData::ExternalStream { + stdout: Some(RawStream::new(Box::new(stream), None, call.head, None)), + stderr: None, + exit_code: None, + span: call.head, + metadata: None, + trim_end_newline: false, + }) + } +} diff --git a/crates/nu_plugin_stream_example/src/commands/for_each.rs b/crates/nu_plugin_stream_example/src/commands/for_each.rs new file mode 100644 index 0000000000..3b1f4245c6 --- /dev/null +++ b/crates/nu_plugin_stream_example/src/commands/for_each.rs @@ -0,0 +1,45 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, PluginCommand}; +use nu_protocol::{Category, PipelineData, PluginExample, PluginSignature, SyntaxShape, Type}; + +use crate::StreamExample; + +/// ` | stream_example for-each { |value| ... }` +pub struct ForEach; + +impl PluginCommand for ForEach { + type Plugin = StreamExample; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("stream_example for-each") + .usage("Example execution of a closure with a stream") + .extra_usage("Prints each value the closure returns to stderr") + .input_output_type(Type::ListStream, Type::Nothing) + .required( + "closure", + SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), + "The closure to run for each input value", + ) + .plugin_examples(vec![PluginExample { + example: "ls | get name | stream_example for-each { |f| ^file $f }".into(), + description: "example with an external command".into(), + result: None, + }]) + .category(Category::Experimental) + } + + fn run( + &self, + _plugin: &StreamExample, + engine: &EngineInterface, + call: &EvaluatedCall, + input: PipelineData, + ) -> Result { + let closure = call.req(0)?; + let config = engine.get_config()?; + for value in input { + let result = engine.eval_closure(&closure, vec![value.clone()], Some(value))?; + eprintln!("{}", result.to_expanded_string(", ", &config)); + } + Ok(PipelineData::Empty) + } +} diff --git a/crates/nu_plugin_stream_example/src/commands/generate.rs b/crates/nu_plugin_stream_example/src/commands/generate.rs new file mode 100644 index 0000000000..1bedbe85a5 --- /dev/null +++ b/crates/nu_plugin_stream_example/src/commands/generate.rs @@ -0,0 +1,79 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, PluginCommand}; +use nu_protocol::{ + Category, IntoInterruptiblePipelineData, PipelineData, PluginExample, PluginSignature, + SyntaxShape, Type, Value, +}; + +use crate::StreamExample; + +/// `stream_example generate { |previous| {out: ..., next: ...} }` +pub struct Generate; + +impl PluginCommand for Generate { + type Plugin = StreamExample; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("stream_example generate") + .usage("Example execution of a closure to produce a stream") + .extra_usage("See the builtin `generate` command") + .input_output_type(Type::Nothing, Type::ListStream) + .required( + "initial", + SyntaxShape::Any, + "The initial value to pass to the closure", + ) + .required( + "closure", + SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), + "The closure to run to generate values", + ) + .plugin_examples(vec![PluginExample { + example: + "stream_example generate 0 { |i| if $i <= 10 { {out: $i, next: ($i + 2)} } }" + .into(), + description: "Generate a sequence of numbers".into(), + result: Some(Value::test_list( + [0, 2, 4, 6, 8, 10] + .into_iter() + .map(Value::test_int) + .collect(), + )), + }]) + .category(Category::Experimental) + } + + fn run( + &self, + _plugin: &StreamExample, + engine: &EngineInterface, + call: &EvaluatedCall, + _input: PipelineData, + ) -> Result { + let engine = engine.clone(); + let call = call.clone(); + let initial: Value = call.req(0)?; + let closure = call.req(1)?; + + let mut next = (!initial.is_nothing()).then_some(initial); + + Ok(std::iter::from_fn(move || { + next.take() + .and_then(|value| { + engine + .eval_closure(&closure, vec![value.clone()], Some(value)) + .and_then(|record| { + if record.is_nothing() { + Ok(None) + } else { + let record = record.as_record()?; + next = record.get("next").cloned(); + Ok(record.get("out").cloned()) + } + }) + .transpose() + }) + .map(|result| result.unwrap_or_else(|err| Value::error(err, call.head))) + }) + .into_pipeline_data(None)) + } +} diff --git a/crates/nu_plugin_stream_example/src/commands/mod.rs b/crates/nu_plugin_stream_example/src/commands/mod.rs new file mode 100644 index 0000000000..7fbd507183 --- /dev/null +++ b/crates/nu_plugin_stream_example/src/commands/mod.rs @@ -0,0 +1,11 @@ +mod collect_external; +mod for_each; +mod generate; +mod seq; +mod sum; + +pub use collect_external::CollectExternal; +pub use for_each::ForEach; +pub use generate::Generate; +pub use seq::Seq; +pub use sum::Sum; diff --git a/crates/nu_plugin_stream_example/src/commands/seq.rs b/crates/nu_plugin_stream_example/src/commands/seq.rs new file mode 100644 index 0000000000..2190ffb122 --- /dev/null +++ b/crates/nu_plugin_stream_example/src/commands/seq.rs @@ -0,0 +1,47 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, PluginCommand}; +use nu_protocol::{ + Category, ListStream, PipelineData, PluginExample, PluginSignature, SyntaxShape, Type, Value, +}; + +use crate::StreamExample; + +/// `stream_example seq ` +pub struct Seq; + +impl PluginCommand for Seq { + type Plugin = StreamExample; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("stream_example seq") + .usage("Example stream generator for a list of values") + .search_terms(vec!["example".into()]) + .required("first", SyntaxShape::Int, "first number to generate") + .required("last", SyntaxShape::Int, "last number to generate") + .input_output_type(Type::Nothing, Type::List(Type::Int.into())) + .plugin_examples(vec![PluginExample { + example: "stream_example seq 1 3".into(), + description: "generate a sequence from 1 to 3".into(), + result: Some(Value::test_list(vec![ + Value::test_int(1), + Value::test_int(2), + Value::test_int(3), + ])), + }]) + .category(Category::Experimental) + } + + fn run( + &self, + _plugin: &StreamExample, + _engine: &EngineInterface, + call: &EvaluatedCall, + _input: PipelineData, + ) -> Result { + let first: i64 = call.req(0)?; + let last: i64 = call.req(1)?; + let span = call.head; + let iter = (first..=last).map(move |number| Value::int(number, span)); + let list_stream = ListStream::from_stream(iter, None); + Ok(PipelineData::ListStream(list_stream, None)) + } +} diff --git a/crates/nu_plugin_stream_example/src/commands/sum.rs b/crates/nu_plugin_stream_example/src/commands/sum.rs new file mode 100644 index 0000000000..68add17a05 --- /dev/null +++ b/crates/nu_plugin_stream_example/src/commands/sum.rs @@ -0,0 +1,91 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, PluginCommand}; +use nu_protocol::{Category, PipelineData, PluginExample, PluginSignature, Span, Type, Value}; + +use crate::StreamExample; + +/// ` | stream_example sum` +pub struct Sum; + +impl PluginCommand for Sum { + type Plugin = StreamExample; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("stream_example sum") + .usage("Example stream consumer for a list of values") + .search_terms(vec!["example".into()]) + .input_output_types(vec![ + (Type::List(Type::Int.into()), Type::Int), + (Type::List(Type::Float.into()), Type::Float), + ]) + .plugin_examples(vec![PluginExample { + example: "seq 1 5 | stream_example sum".into(), + description: "sum values from 1 to 5".into(), + result: Some(Value::test_int(15)), + }]) + .category(Category::Experimental) + } + + fn run( + &self, + _plugin: &StreamExample, + _engine: &EngineInterface, + call: &EvaluatedCall, + input: PipelineData, + ) -> Result { + let mut acc = IntOrFloat::Int(0); + let span = input.span(); + for value in input { + if let Ok(n) = value.as_i64() { + acc.add_i64(n); + } else if let Ok(n) = value.as_f64() { + acc.add_f64(n); + } else { + return Err(LabeledError { + label: "Stream only accepts ints and floats".into(), + msg: format!("found {}", value.get_type()), + span, + }); + } + } + Ok(PipelineData::Value(acc.to_value(call.head), None)) + } +} + +/// Accumulates numbers into either an int or a float. Changes type to float on the first +/// float received. +#[derive(Clone, Copy)] +enum IntOrFloat { + Int(i64), + Float(f64), +} + +impl IntOrFloat { + pub(crate) fn add_i64(&mut self, n: i64) { + match self { + IntOrFloat::Int(ref mut v) => { + *v += n; + } + IntOrFloat::Float(ref mut v) => { + *v += n as f64; + } + } + } + + pub(crate) fn add_f64(&mut self, n: f64) { + match self { + IntOrFloat::Int(v) => { + *self = IntOrFloat::Float(*v as f64 + n); + } + IntOrFloat::Float(ref mut v) => { + *v += n; + } + } + } + + pub(crate) fn to_value(self, span: Span) -> Value { + match self { + IntOrFloat::Int(v) => Value::int(v, span), + IntOrFloat::Float(v) => Value::float(v, span), + } + } +} diff --git a/crates/nu_plugin_stream_example/src/example.rs b/crates/nu_plugin_stream_example/src/example.rs deleted file mode 100644 index 4c432d03bd..0000000000 --- a/crates/nu_plugin_stream_example/src/example.rs +++ /dev/null @@ -1,115 +0,0 @@ -use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError}; -use nu_protocol::{IntoInterruptiblePipelineData, ListStream, PipelineData, RawStream, Value}; - -pub struct Example; - -mod int_or_float; -use self::int_or_float::IntOrFloat; - -impl Example { - pub fn seq( - &self, - call: &EvaluatedCall, - _input: PipelineData, - ) -> Result { - let first: i64 = call.req(0)?; - let last: i64 = call.req(1)?; - let span = call.head; - let iter = (first..=last).map(move |number| Value::int(number, span)); - let list_stream = ListStream::from_stream(iter, None); - Ok(PipelineData::ListStream(list_stream, None)) - } - - pub fn sum( - &self, - call: &EvaluatedCall, - input: PipelineData, - ) -> Result { - let mut acc = IntOrFloat::Int(0); - let span = input.span(); - for value in input { - if let Ok(n) = value.as_i64() { - acc.add_i64(n); - } else if let Ok(n) = value.as_f64() { - acc.add_f64(n); - } else { - return Err(LabeledError { - label: "Stream only accepts ints and floats".into(), - msg: format!("found {}", value.get_type()), - span, - }); - } - } - Ok(PipelineData::Value(acc.to_value(call.head), None)) - } - - pub fn collect_external( - &self, - call: &EvaluatedCall, - input: PipelineData, - ) -> Result { - let stream = input.into_iter().map(|value| { - value - .as_str() - .map(|str| str.as_bytes()) - .or_else(|_| value.as_binary()) - .map(|bin| bin.to_vec()) - }); - Ok(PipelineData::ExternalStream { - stdout: Some(RawStream::new(Box::new(stream), None, call.head, None)), - stderr: None, - exit_code: None, - span: call.head, - metadata: None, - trim_end_newline: false, - }) - } - - pub fn for_each( - &self, - engine: &EngineInterface, - call: &EvaluatedCall, - input: PipelineData, - ) -> Result { - let closure = call.req(0)?; - let config = engine.get_config()?; - for value in input { - let result = engine.eval_closure(&closure, vec![value.clone()], Some(value))?; - eprintln!("{}", result.to_expanded_string(", ", &config)); - } - Ok(PipelineData::Empty) - } - - pub fn generate( - &self, - engine: &EngineInterface, - call: &EvaluatedCall, - ) -> Result { - let engine = engine.clone(); - let call = call.clone(); - let initial: Value = call.req(0)?; - let closure = call.req(1)?; - - let mut next = (!initial.is_nothing()).then_some(initial); - - Ok(std::iter::from_fn(move || { - next.take() - .and_then(|value| { - engine - .eval_closure(&closure, vec![value.clone()], Some(value)) - .and_then(|record| { - if record.is_nothing() { - Ok(None) - } else { - let record = record.as_record()?; - next = record.get("next").cloned(); - Ok(record.get("out").cloned()) - } - }) - .transpose() - }) - .map(|result| result.unwrap_or_else(|err| Value::error(err, call.head))) - }) - .into_pipeline_data(None)) - } -} diff --git a/crates/nu_plugin_stream_example/src/example/int_or_float.rs b/crates/nu_plugin_stream_example/src/example/int_or_float.rs deleted file mode 100644 index ec596c852c..0000000000 --- a/crates/nu_plugin_stream_example/src/example/int_or_float.rs +++ /dev/null @@ -1,42 +0,0 @@ -use nu_protocol::Value; - -use nu_protocol::Span; - -/// Accumulates numbers into either an int or a float. Changes type to float on the first -/// float received. -#[derive(Clone, Copy)] -pub(crate) enum IntOrFloat { - Int(i64), - Float(f64), -} - -impl IntOrFloat { - pub(crate) fn add_i64(&mut self, n: i64) { - match self { - IntOrFloat::Int(ref mut v) => { - *v += n; - } - IntOrFloat::Float(ref mut v) => { - *v += n as f64; - } - } - } - - pub(crate) fn add_f64(&mut self, n: f64) { - match self { - IntOrFloat::Int(v) => { - *self = IntOrFloat::Float(*v as f64 + n); - } - IntOrFloat::Float(ref mut v) => { - *v += n; - } - } - } - - pub(crate) fn to_value(self, span: Span) -> Value { - match self { - IntOrFloat::Int(v) => Value::int(v, span), - IntOrFloat::Float(v) => Value::float(v, span), - } - } -} diff --git a/crates/nu_plugin_stream_example/src/lib.rs b/crates/nu_plugin_stream_example/src/lib.rs index 995d09e8e1..abec82c1ad 100644 --- a/crates/nu_plugin_stream_example/src/lib.rs +++ b/crates/nu_plugin_stream_example/src/lib.rs @@ -1,4 +1,50 @@ -mod example; -mod nu; +use nu_plugin::{ + EngineInterface, EvaluatedCall, LabeledError, Plugin, PluginCommand, SimplePluginCommand, +}; +use nu_protocol::{Category, PluginSignature, Value}; -pub use example::Example; +mod commands; +pub use commands::*; + +pub struct StreamExample; + +impl Plugin for StreamExample { + fn commands(&self) -> Vec>> { + vec![ + Box::new(Main), + Box::new(Seq), + Box::new(Sum), + Box::new(CollectExternal), + Box::new(ForEach), + Box::new(Generate), + ] + } +} + +/// `stream_example` +pub struct Main; + +impl SimplePluginCommand for Main { + type Plugin = StreamExample; + + fn signature(&self) -> PluginSignature { + PluginSignature::build("stream_example") + .usage("Examples for streaming plugins") + .search_terms(vec!["example".into()]) + .category(Category::Experimental) + } + + fn run( + &self, + _plugin: &StreamExample, + _engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + Err(LabeledError { + label: "No subcommand provided".into(), + msg: "add --help here to see usage".into(), + span: Some(call.head.past()), + }) + } +} diff --git a/crates/nu_plugin_stream_example/src/main.rs b/crates/nu_plugin_stream_example/src/main.rs index 538a0283aa..726219de74 100644 --- a/crates/nu_plugin_stream_example/src/main.rs +++ b/crates/nu_plugin_stream_example/src/main.rs @@ -1,12 +1,12 @@ use nu_plugin::{serve_plugin, MsgPackSerializer}; -use nu_plugin_stream_example::Example; +use nu_plugin_stream_example::StreamExample; fn main() { // When defining your plugin, you can select the Serializer that could be // used to encode and decode the messages. The available options are // MsgPackSerializer and JsonSerializer. Both are defined in the serializer // folder in nu-plugin. - serve_plugin(&Example {}, MsgPackSerializer {}) + serve_plugin(&StreamExample {}, MsgPackSerializer {}) // Note // When creating plugins in other languages one needs to consider how a plugin diff --git a/crates/nu_plugin_stream_example/src/nu/mod.rs b/crates/nu_plugin_stream_example/src/nu/mod.rs deleted file mode 100644 index 6592f7dba9..0000000000 --- a/crates/nu_plugin_stream_example/src/nu/mod.rs +++ /dev/null @@ -1,125 +0,0 @@ -use crate::Example; -use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, StreamingPlugin}; -use nu_protocol::{ - Category, PipelineData, PluginExample, PluginSignature, Span, SyntaxShape, Type, Value, -}; - -impl StreamingPlugin for Example { - fn signature(&self) -> Vec { - let span = Span::unknown(); - vec![ - PluginSignature::build("stream_example") - .usage("Examples for streaming plugins") - .search_terms(vec!["example".into()]) - .category(Category::Experimental), - PluginSignature::build("stream_example seq") - .usage("Example stream generator for a list of values") - .search_terms(vec!["example".into()]) - .required("first", SyntaxShape::Int, "first number to generate") - .required("last", SyntaxShape::Int, "last number to generate") - .input_output_type(Type::Nothing, Type::List(Type::Int.into())) - .plugin_examples(vec![PluginExample { - example: "stream_example seq 1 3".into(), - description: "generate a sequence from 1 to 3".into(), - result: Some(Value::list( - vec![ - Value::int(1, span), - Value::int(2, span), - Value::int(3, span), - ], - span, - )), - }]) - .category(Category::Experimental), - PluginSignature::build("stream_example sum") - .usage("Example stream consumer for a list of values") - .search_terms(vec!["example".into()]) - .input_output_types(vec![ - (Type::List(Type::Int.into()), Type::Int), - (Type::List(Type::Float.into()), Type::Float), - ]) - .plugin_examples(vec![PluginExample { - example: "seq 1 5 | stream_example sum".into(), - description: "sum values from 1 to 5".into(), - result: Some(Value::int(15, span)), - }]) - .category(Category::Experimental), - PluginSignature::build("stream_example collect-external") - .usage("Example transformer to raw external stream") - .search_terms(vec!["example".into()]) - .input_output_types(vec![ - (Type::List(Type::String.into()), Type::String), - (Type::List(Type::Binary.into()), Type::Binary), - ]) - .plugin_examples(vec![PluginExample { - example: "[a b] | stream_example collect-external".into(), - description: "collect strings into one stream".into(), - result: Some(Value::string("ab", span)), - }]) - .category(Category::Experimental), - PluginSignature::build("stream_example for-each") - .usage("Example execution of a closure with a stream") - .extra_usage("Prints each value the closure returns to stderr") - .input_output_type(Type::ListStream, Type::Nothing) - .required( - "closure", - SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), - "The closure to run for each input value", - ) - .plugin_examples(vec![PluginExample { - example: "ls | get name | stream_example for-each { |f| ^file $f }".into(), - description: "example with an external command".into(), - result: None, - }]) - .category(Category::Experimental), - PluginSignature::build("stream_example generate") - .usage("Example execution of a closure to produce a stream") - .extra_usage("See the builtin `generate` command") - .input_output_type(Type::Nothing, Type::ListStream) - .required( - "initial", - SyntaxShape::Any, - "The initial value to pass to the closure" - ) - .required( - "closure", - SyntaxShape::Closure(Some(vec![SyntaxShape::Any])), - "The closure to run to generate values", - ) - .plugin_examples(vec![PluginExample { - example: "stream_example generate 0 { |i| if $i <= 10 { {out: $i, next: ($i + 2)} } }".into(), - description: "Generate a sequence of numbers".into(), - result: Some(Value::test_list( - [0, 2, 4, 6, 8, 10].into_iter().map(Value::test_int).collect(), - )), - }]) - .category(Category::Experimental), - ] - } - - fn run( - &self, - name: &str, - engine: &EngineInterface, - call: &EvaluatedCall, - input: PipelineData, - ) -> Result { - match name { - "stream_example" => Err(LabeledError { - label: "No subcommand provided".into(), - msg: "add --help here to see usage".into(), - span: Some(call.head) - }), - "stream_example seq" => self.seq(call, input), - "stream_example sum" => self.sum(call, input), - "stream_example collect-external" => self.collect_external(call, input), - "stream_example for-each" => self.for_each(engine, call, input), - "stream_example generate" => self.generate(engine, call), - _ => Err(LabeledError { - label: "Plugin call with wrong name signature".into(), - msg: "the signature used to call the plugin does not match any name in the plugin signature vector".into(), - span: Some(call.head), - }), - } - } -} diff --git a/tests/plugins/custom_values.rs b/tests/plugins/custom_values.rs index ab4aafa19b..dd93a55e9f 100644 --- a/tests/plugins/custom_values.rs +++ b/tests/plugins/custom_values.rs @@ -177,6 +177,6 @@ fn drop_check_custom_value_prints_message_on_drop() { "do { |v| [$v $v] } (custom-value drop-check 'Hello') | ignore" ); - assert_eq!(actual.err, "DropCheck was dropped: Hello\n"); + assert_eq!(actual.err, "DropCheckValue was dropped: Hello\n"); assert!(actual.status.success()); }