Reorganize plugin API around commands (#12170)

[Context on
Discord](https://discord.com/channels/601130461678272522/855947301380947968/1216517833312309419)

# Description
This is a significant breaking change to the plugin API, but one I think
is worthwhile. @ayax79 mentioned on Discord that while trying to start
on a dataframes plugin, he was a little disappointed that more wasn't
provided in terms of code organization for commands, particularly since
there are *a lot* of `dfr` commands.

This change treats plugins more like miniatures of the engine, with
dispatch of the command name being handled inherently, each command
being its own type, and each having their own signature within the trait
impl for the command type rather than having to find a way to centralize
it all into one `Vec`.

For the example plugins that have multiple commands, I definitely like
how this looks a lot better. This encourages doing code organization the
right way and feels very good.

For the plugins that have only one command, it's just a little bit more
boilerplate - but still worth it, in my opinion.

The `Box<dyn PluginCommand<Plugin = Self>>` type in `commands()` is a
little bit hairy, particularly for Rust beginners, but ultimately not so
bad, and it gives the desired flexibility for shared state for a whole
plugin + the individual commands.

# User-Facing Changes
Pretty big breaking change to plugin API, but probably one that's worth
making.

```rust
use nu_plugin::*;
use nu_protocol::{PluginSignature, PipelineData, Type, Value};

struct LowercasePlugin;
struct Lowercase;

// Plugins can now have multiple commands
impl PluginCommand for Lowercase {
    type Plugin = LowercasePlugin;

    // The signature lives with the command
    fn signature(&self) -> PluginSignature {
        PluginSignature::build("lowercase")
            .usage("Convert each string in a stream to lowercase")
            .input_output_type(Type::List(Type::String.into()), Type::List(Type::String.into()))
    }

    // We also provide SimplePluginCommand which operates on Value like before
    fn run(
        &self,
        plugin: &LowercasePlugin,
        engine: &EngineInterface,
        call: &EvaluatedCall,
        input: PipelineData,
    ) -> Result<PipelineData, LabeledError> {
        let span = call.head;
        Ok(input.map(move |value| {
            value.as_str()
                .map(|string| Value::string(string.to_lowercase(), span))
                // Errors in a stream should be returned as values.
                .unwrap_or_else(|err| Value::error(err, span))
        }, None)?)
    }
}

// Plugin now just has a list of commands, and the custom value op stuff still goes here
impl Plugin for LowercasePlugin {
    fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
        vec![Box::new(Lowercase)]
    }
}

fn main() {
    serve_plugin(&LowercasePlugin{}, MsgPackSerializer)
}
```

Time this however you like - we're already breaking stuff for 0.92, so
it might be good to do it now, but if it feels like a lot all at once,
it could wait.

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

# After Submitting
- [ ] Update examples in the book
- [x] Fix #12088 to match - this change would actually simplify it a
lot, because the methods are currently just duplicated between `Plugin`
and `StreamingPlugin`, but they only need to be on `Plugin` with this
change
This commit is contained in:
Devyn Cairns 2024-03-14 14:40:02 -07:00 committed by GitHub
parent b6c7656194
commit 9cf2e873b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
50 changed files with 1646 additions and 1291 deletions

View file

@ -16,18 +16,29 @@
//! invoked by Nushell.
//!
//! ```rust,no_run
//! use nu_plugin::{EvaluatedCall, LabeledError, MsgPackSerializer, Plugin, EngineInterface, serve_plugin};
//! use nu_plugin::{EvaluatedCall, LabeledError, MsgPackSerializer, serve_plugin};
//! use nu_plugin::{Plugin, PluginCommand, SimplePluginCommand, EngineInterface};
//! use nu_protocol::{PluginSignature, Value};
//!
//! struct MyPlugin;
//! struct MyCommand;
//!
//! impl Plugin for MyPlugin {
//! fn signature(&self) -> Vec<PluginSignature> {
//! fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
//! vec![Box::new(MyCommand)]
//! }
//! }
//!
//! impl SimplePluginCommand for MyCommand {
//! type Plugin = MyPlugin;
//!
//! fn signature(&self) -> PluginSignature {
//! todo!();
//! }
//!
//! fn run(
//! &self,
//! name: &str,
//! plugin: &MyPlugin,
//! engine: &EngineInterface,
//! call: &EvaluatedCall,
//! input: &Value
@ -49,7 +60,9 @@ mod protocol;
mod sequence;
mod serializers;
pub use plugin::{serve_plugin, EngineInterface, Plugin, PluginEncoder, StreamingPlugin};
pub use plugin::{
serve_plugin, EngineInterface, Plugin, PluginCommand, PluginEncoder, SimplePluginCommand,
};
pub use protocol::{EvaluatedCall, LabeledError};
pub use serializers::{json::JsonSerializer, msgpack::MsgPackSerializer};

View file

@ -0,0 +1,206 @@
use nu_protocol::{PipelineData, PluginSignature, Value};
use crate::{EngineInterface, EvaluatedCall, LabeledError, Plugin};
/// The API for a Nushell plugin command
///
/// This is the trait that Nushell plugin commands must implement. The methods defined on
/// `PluginCommand` are invoked by [serve_plugin] during plugin registration and execution.
///
/// The plugin command must be able to be safely shared between threads, so that multiple
/// invocations can be run in parallel. If interior mutability is desired, consider synchronization
/// primitives such as [mutexes](std::sync::Mutex) and [channels](std::sync::mpsc).
///
/// This version of the trait expects stream input and output. If you have a simple plugin that just
/// operates on plain values, consider using [`SimplePluginCommand`] instead.
///
/// # Examples
/// Basic usage:
/// ```
/// # use nu_plugin::*;
/// # use nu_protocol::{PluginSignature, PipelineData, Type, Value};
/// struct LowercasePlugin;
/// struct Lowercase;
///
/// impl PluginCommand for Lowercase {
/// type Plugin = LowercasePlugin;
///
/// fn signature(&self) -> PluginSignature {
/// PluginSignature::build("lowercase")
/// .usage("Convert each string in a stream to lowercase")
/// .input_output_type(Type::List(Type::String.into()), Type::List(Type::String.into()))
/// }
///
/// fn run(
/// &self,
/// plugin: &LowercasePlugin,
/// engine: &EngineInterface,
/// call: &EvaluatedCall,
/// input: PipelineData,
/// ) -> Result<PipelineData, LabeledError> {
/// let span = call.head;
/// Ok(input.map(move |value| {
/// value.as_str()
/// .map(|string| Value::string(string.to_lowercase(), span))
/// // Errors in a stream should be returned as values.
/// .unwrap_or_else(|err| Value::error(err, span))
/// }, None)?)
/// }
/// }
///
/// # impl Plugin for LowercasePlugin {
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// # vec![Box::new(Lowercase)]
/// # }
/// # }
/// #
/// # fn main() {
/// # serve_plugin(&LowercasePlugin{}, MsgPackSerializer)
/// # }
/// ```
pub trait PluginCommand: Sync {
/// The type of plugin this command runs on
///
/// Since [`.run()`] takes a reference to the plugin, it is necessary to define the type of
/// plugin that the command expects here.
type Plugin: Plugin;
/// The signature of the plugin command
///
/// These are aggregated from the [`Plugin`] and sent to the engine on `register`.
fn signature(&self) -> PluginSignature;
/// Perform the actual behavior of the plugin command
///
/// The behavior of the plugin is defined by the implementation of this method. When Nushell
/// invoked the plugin [serve_plugin] will call this method and print the serialized returned
/// value or error to stdout, which Nushell will interpret.
///
/// `engine` provides an interface back to the Nushell engine. See [`EngineInterface`] docs for
/// details on what methods are available.
///
/// The `call` contains metadata describing how the plugin command was invoked, including
/// arguments, and `input` contains the structured data piped into the command.
///
/// This variant expects to receive and produce [`PipelineData`], which allows for stream-based
/// handling of I/O. This is recommended if the plugin is expected to transform large
/// lists or potentially large quantities of bytes. The API is more complex however, and
/// [`SimplePluginCommand`] is recommended instead if this is not a concern.
fn run(
&self,
plugin: &Self::Plugin,
engine: &EngineInterface,
call: &EvaluatedCall,
input: PipelineData,
) -> Result<PipelineData, LabeledError>;
}
/// The API for a simple Nushell plugin command
///
/// This trait is an alternative to [`PluginCommand`], and operates on values instead of streams.
/// Note that this may make handling large lists more difficult.
///
/// The plugin command must be able to be safely shared between threads, so that multiple
/// invocations can be run in parallel. If interior mutability is desired, consider synchronization
/// primitives such as [mutexes](std::sync::Mutex) and [channels](std::sync::mpsc).
///
/// # Examples
/// Basic usage:
/// ```
/// # use nu_plugin::*;
/// # use nu_protocol::{PluginSignature, Type, Value};
/// struct HelloPlugin;
/// struct Hello;
///
/// impl SimplePluginCommand for Hello {
/// type Plugin = HelloPlugin;
///
/// fn signature(&self) -> PluginSignature {
/// PluginSignature::build("hello")
/// .input_output_type(Type::Nothing, Type::String)
/// }
///
/// fn run(
/// &self,
/// plugin: &HelloPlugin,
/// engine: &EngineInterface,
/// call: &EvaluatedCall,
/// input: &Value,
/// ) -> Result<Value, LabeledError> {
/// Ok(Value::string("Hello, World!".to_owned(), call.head))
/// }
/// }
///
/// # impl Plugin for HelloPlugin {
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// # vec![Box::new(Hello)]
/// # }
/// # }
/// #
/// # fn main() {
/// # serve_plugin(&HelloPlugin{}, MsgPackSerializer)
/// # }
/// ```
pub trait SimplePluginCommand: Sync {
/// The type of plugin this command runs on
///
/// Since [`.run()`] takes a reference to the plugin, it is necessary to define the type of
/// plugin that the command expects here.
type Plugin: Plugin;
/// The signature of the plugin command
///
/// These are aggregated from the [`Plugin`] and sent to the engine on `register`.
fn signature(&self) -> PluginSignature;
/// Perform the actual behavior of the plugin command
///
/// The behavior of the plugin is defined by the implementation of this method. When Nushell
/// invoked the plugin [serve_plugin] will call this method and print the serialized returned
/// value or error to stdout, which Nushell will interpret.
///
/// `engine` provides an interface back to the Nushell engine. See [`EngineInterface`] docs for
/// details on what methods are available.
///
/// The `call` contains metadata describing how the plugin command was invoked, including
/// arguments, and `input` contains the structured data piped into the command.
///
/// This variant does not support streaming. Consider implementing [`PluginCommand`] directly
/// if streaming is desired.
fn run(
&self,
plugin: &Self::Plugin,
engine: &EngineInterface,
call: &EvaluatedCall,
input: &Value,
) -> Result<Value, LabeledError>;
}
/// All [`SimplePluginCommand`]s can be used as [`PluginCommand`]s, but input streams will be fully
/// consumed before the plugin command runs.
impl<T> PluginCommand for T
where
T: SimplePluginCommand,
{
type Plugin = <Self as SimplePluginCommand>::Plugin;
fn signature(&self) -> PluginSignature {
<Self as SimplePluginCommand>::signature(self)
}
fn run(
&self,
plugin: &Self::Plugin,
engine: &EngineInterface,
call: &EvaluatedCall,
input: PipelineData,
) -> Result<PipelineData, LabeledError> {
// Unwrap the PipelineData from input, consuming the potential stream, and pass it to the
// simpler signature in Plugin
let span = input.span().unwrap_or(call.head);
let input_value = input.into_value(span);
// Wrap the output in PipelineData::Value
<Self as SimplePluginCommand>::run(self, plugin, engine, call, &input_value)
.map(|value| PipelineData::Value(value, None))
}
}

View file

@ -1,7 +1,13 @@
use nu_engine::documentation::get_flags_section;
use nu_protocol::ast::Operator;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fmt::Write;
use std::io::{BufReader, Read, Write as WriteTrait};
use std::path::Path;
use std::process::{Child, ChildStdout, Command as CommandSys, Stdio};
use std::{env, thread};
use std::sync::mpsc::TrySendError;
use std::sync::{mpsc, Arc, Mutex};
@ -11,11 +17,6 @@ use crate::protocol::{
CallInfo, CustomValueOp, LabeledError, PluginCustomValue, PluginInput, PluginOutput,
};
use crate::EncodingType;
use std::fmt::Write;
use std::io::{BufReader, Read, Write as WriteTrait};
use std::path::Path;
use std::process::{Child, ChildStdout, Command as CommandSys, Stdio};
use std::{env, thread};
#[cfg(unix)]
use std::os::unix::process::CommandExt;
@ -24,13 +25,13 @@ use std::os::unix::process::CommandExt;
use std::os::windows::process::CommandExt;
use nu_protocol::{
CustomValue, IntoSpanned, PipelineData, PluginSignature, ShellError, Spanned, Value,
ast::Operator, CustomValue, IntoSpanned, PipelineData, PluginSignature, ShellError, Spanned,
Value,
};
use self::gc::PluginGc;
use super::EvaluatedCall;
mod command;
mod context;
mod declaration;
mod gc;
@ -38,6 +39,7 @@ mod interface;
mod persistent;
mod source;
pub use command::{PluginCommand, SimplePluginCommand};
pub use declaration::PluginDeclaration;
pub use interface::EngineInterface;
pub use persistent::PersistentPlugin;
@ -215,13 +217,10 @@ where
plugin.get(envs)?.get_signature()
}
/// The basic API for a Nushell plugin
/// The API for a Nushell plugin
///
/// This is the trait that Nushell plugins must implement. The methods defined on
/// `Plugin` are invoked by [serve_plugin] during plugin registration and execution.
///
/// If large amounts of data are expected to need to be received or produced, it may be more
/// appropriate to implement [StreamingPlugin] instead.
/// A plugin defines multiple commands, which are added to the engine when the user calls
/// `register`.
///
/// The plugin must be able to be safely shared between threads, so that multiple invocations can
/// be run in parallel. If interior mutability is desired, consider synchronization primitives such
@ -233,18 +232,25 @@ where
/// # use nu_plugin::*;
/// # use nu_protocol::{PluginSignature, Type, Value};
/// struct HelloPlugin;
/// struct Hello;
///
/// impl Plugin for HelloPlugin {
/// fn signature(&self) -> Vec<PluginSignature> {
/// let sig = PluginSignature::build("hello")
/// .input_output_type(Type::Nothing, Type::String);
/// fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {
/// vec![Box::new(Hello)]
/// }
/// }
///
/// vec![sig]
/// impl SimplePluginCommand for Hello {
/// type Plugin = HelloPlugin;
///
/// fn signature(&self) -> PluginSignature {
/// PluginSignature::build("hello")
/// .input_output_type(Type::Nothing, Type::String)
/// }
///
/// fn run(
/// &self,
/// name: &str,
/// plugin: &HelloPlugin,
/// engine: &EngineInterface,
/// call: &EvaluatedCall,
/// input: &Value,
@ -258,37 +264,14 @@ where
/// # }
/// ```
pub trait Plugin: Sync {
/// The signature of the plugin
/// The commands supported by the plugin
///
/// This method returns the [PluginSignature]s that describe the capabilities
/// of this plugin. Since a single plugin executable can support multiple invocation
/// patterns we return a `Vec` of signatures.
fn signature(&self) -> Vec<PluginSignature>;
/// Perform the actual behavior of the plugin
/// Each [`PluginCommand`] contains both the signature of the command and the functionality it
/// implements.
///
/// The behavior of the plugin is defined by the implementation of this method.
/// When Nushell invoked the plugin [serve_plugin] will call this method and
/// print the serialized returned value or error to stdout, which Nushell will
/// interpret.
///
/// The `name` is only relevant for plugins that implement multiple commands as the
/// invoked command will be passed in via this argument. The `call` contains
/// metadata describing how the plugin was invoked and `input` contains the structured
/// data passed to the command implemented by this [Plugin].
///
/// `engine` provides an interface back to the Nushell engine. See [`EngineInterface`] docs for
/// details on what methods are available.
///
/// This variant does not support streaming. Consider implementing [StreamingPlugin] instead
/// if streaming is desired.
fn run(
&self,
name: &str,
engine: &EngineInterface,
call: &EvaluatedCall,
input: &Value,
) -> Result<Value, LabeledError>;
/// This is only called once by [`serve_plugin`] at the beginning of your plugin's execution. It
/// is not possible to change the defined commands during runtime.
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>>;
/// Collapse a custom value to plain old data.
///
@ -397,269 +380,7 @@ pub trait Plugin: Sync {
}
}
/// The streaming API for a Nushell plugin
///
/// This is a more low-level version of the [Plugin] trait that supports operating on streams of
/// data. If you don't need to operate on streams, consider using that trait instead.
///
/// The methods defined on `StreamingPlugin` are invoked by [serve_plugin] during plugin
/// registration and execution.
///
/// # Examples
/// Basic usage:
/// ```
/// # use nu_plugin::*;
/// # use nu_protocol::{PluginSignature, PipelineData, Type, Value};
/// struct LowercasePlugin;
///
/// impl StreamingPlugin for LowercasePlugin {
/// fn signature(&self) -> Vec<PluginSignature> {
/// let sig = PluginSignature::build("lowercase")
/// .usage("Convert each string in a stream to lowercase")
/// .input_output_type(Type::List(Type::String.into()), Type::List(Type::String.into()));
///
/// vec![sig]
/// }
///
/// fn run(
/// &self,
/// name: &str,
/// engine: &EngineInterface,
/// call: &EvaluatedCall,
/// input: PipelineData,
/// ) -> Result<PipelineData, LabeledError> {
/// let span = call.head;
/// Ok(input.map(move |value| {
/// value.as_str()
/// .map(|string| Value::string(string.to_lowercase(), span))
/// // Errors in a stream should be returned as values.
/// .unwrap_or_else(|err| Value::error(err, span))
/// }, None)?)
/// }
/// }
///
/// # fn main() {
/// # serve_plugin(&LowercasePlugin{}, MsgPackSerializer)
/// # }
/// ```
pub trait StreamingPlugin: Sync {
/// The signature of the plugin
///
/// This method returns the [PluginSignature]s that describe the capabilities
/// of this plugin. Since a single plugin executable can support multiple invocation
/// patterns we return a `Vec` of signatures.
fn signature(&self) -> Vec<PluginSignature>;
/// Perform the actual behavior of the plugin
///
/// The behavior of the plugin is defined by the implementation of this method.
/// When Nushell invoked the plugin [serve_plugin] will call this method and
/// print the serialized returned value or error to stdout, which Nushell will
/// interpret.
///
/// The `name` is only relevant for plugins that implement multiple commands as the
/// invoked command will be passed in via this argument. The `call` contains
/// metadata describing how the plugin was invoked and `input` contains the structured
/// data passed to the command implemented by this [Plugin].
///
/// This variant expects to receive and produce [PipelineData], which allows for stream-based
/// handling of I/O. This is recommended if the plugin is expected to transform large lists or
/// potentially large quantities of bytes. The API is more complex however, and [Plugin] is
/// recommended instead if this is not a concern.
fn run(
&self,
name: &str,
engine: &EngineInterface,
call: &EvaluatedCall,
input: PipelineData,
) -> Result<PipelineData, LabeledError>;
/// Collapse a custom value to plain old data.
///
/// The default implementation of this method just calls [`CustomValue::to_base_value`], but
/// the method can be implemented differently if accessing plugin state is desirable.
fn custom_value_to_base_value(
&self,
engine: &EngineInterface,
custom_value: Spanned<Box<dyn CustomValue>>,
) -> Result<Value, LabeledError> {
let _ = engine;
custom_value
.item
.to_base_value(custom_value.span)
.map_err(LabeledError::from)
}
/// Follow a numbered cell path on a custom value - e.g. `value.0`.
///
/// The default implementation of this method just calls [`CustomValue::follow_path_int`], but
/// the method can be implemented differently if accessing plugin state is desirable.
fn custom_value_follow_path_int(
&self,
engine: &EngineInterface,
custom_value: Spanned<Box<dyn CustomValue>>,
index: Spanned<usize>,
) -> Result<Value, LabeledError> {
let _ = engine;
custom_value
.item
.follow_path_int(custom_value.span, index.item, index.span)
.map_err(LabeledError::from)
}
/// Follow a named cell path on a custom value - e.g. `value.column`.
///
/// The default implementation of this method just calls [`CustomValue::follow_path_string`],
/// but the method can be implemented differently if accessing plugin state is desirable.
fn custom_value_follow_path_string(
&self,
engine: &EngineInterface,
custom_value: Spanned<Box<dyn CustomValue>>,
column_name: Spanned<String>,
) -> Result<Value, LabeledError> {
let _ = engine;
custom_value
.item
.follow_path_string(custom_value.span, column_name.item, column_name.span)
.map_err(LabeledError::from)
}
/// Implement comparison logic for custom values.
///
/// The default implementation of this method just calls [`CustomValue::partial_cmp`], but
/// the method can be implemented differently if accessing plugin state is desirable.
///
/// Note that returning an error here is unlikely to produce desired behavior, as `partial_cmp`
/// lacks a way to produce an error. At the moment the engine just logs the error, and the
/// comparison returns `None`.
fn custom_value_partial_cmp(
&self,
engine: &EngineInterface,
custom_value: Box<dyn CustomValue>,
other_value: Value,
) -> Result<Option<Ordering>, LabeledError> {
let _ = engine;
Ok(custom_value.partial_cmp(&other_value))
}
/// Implement functionality for an operator on a custom value.
///
/// The default implementation of this method just calls [`CustomValue::operation`], but
/// the method can be implemented differently if accessing plugin state is desirable.
fn custom_value_operation(
&self,
engine: &EngineInterface,
left: Spanned<Box<dyn CustomValue>>,
operator: Spanned<Operator>,
right: Value,
) -> Result<Value, LabeledError> {
let _ = engine;
left.item
.operation(left.span, operator.item, operator.span, &right)
.map_err(LabeledError::from)
}
/// Handle a notification that all copies of a custom value within the engine have been dropped.
///
/// This notification is only sent if [`CustomValue::notify_plugin_on_drop`] was true. Unlike
/// the other custom value handlers, a span is not provided.
///
/// Note that a new custom value is created each time it is sent to the engine - if you intend
/// to accept a custom value and send it back, you may need to implement some kind of unique
/// reference counting in your plugin, as you will receive multiple drop notifications even if
/// the data within is identical.
///
/// The default implementation does nothing. Any error generated here is unlikely to be visible
/// to the user, and will only show up in the engine's log output.
fn custom_value_dropped(
&self,
engine: &EngineInterface,
custom_value: Box<dyn CustomValue>,
) -> Result<(), LabeledError> {
let _ = (engine, custom_value);
Ok(())
}
}
/// All [Plugin]s can be used as [StreamingPlugin]s, but input streams will be fully consumed
/// before the plugin runs.
impl<T: Plugin> StreamingPlugin for T {
fn signature(&self) -> Vec<PluginSignature> {
<Self as Plugin>::signature(self)
}
fn run(
&self,
name: &str,
engine: &EngineInterface,
call: &EvaluatedCall,
input: PipelineData,
) -> Result<PipelineData, LabeledError> {
// Unwrap the PipelineData from input, consuming the potential stream, and pass it to the
// simpler signature in Plugin
let span = input.span().unwrap_or(call.head);
let input_value = input.into_value(span);
// Wrap the output in PipelineData::Value
<Self as Plugin>::run(self, name, engine, call, &input_value)
.map(|value| PipelineData::Value(value, None))
}
fn custom_value_to_base_value(
&self,
engine: &EngineInterface,
custom_value: Spanned<Box<dyn CustomValue>>,
) -> Result<Value, LabeledError> {
<Self as Plugin>::custom_value_to_base_value(self, engine, custom_value)
}
fn custom_value_follow_path_int(
&self,
engine: &EngineInterface,
custom_value: Spanned<Box<dyn CustomValue>>,
index: Spanned<usize>,
) -> Result<Value, LabeledError> {
<Self as Plugin>::custom_value_follow_path_int(self, engine, custom_value, index)
}
fn custom_value_follow_path_string(
&self,
engine: &EngineInterface,
custom_value: Spanned<Box<dyn CustomValue>>,
column_name: Spanned<String>,
) -> Result<Value, LabeledError> {
<Self as Plugin>::custom_value_follow_path_string(self, engine, custom_value, column_name)
}
fn custom_value_partial_cmp(
&self,
engine: &EngineInterface,
custom_value: Box<dyn CustomValue>,
other_value: Value,
) -> Result<Option<Ordering>, LabeledError> {
<Self as Plugin>::custom_value_partial_cmp(self, engine, custom_value, other_value)
}
fn custom_value_operation(
&self,
engine: &EngineInterface,
left: Spanned<Box<dyn CustomValue>>,
operator: Spanned<Operator>,
right: Value,
) -> Result<Value, LabeledError> {
<Self as Plugin>::custom_value_operation(self, engine, left, operator, right)
}
fn custom_value_dropped(
&self,
engine: &EngineInterface,
custom_value: Box<dyn CustomValue>,
) -> Result<(), LabeledError> {
<Self as Plugin>::custom_value_dropped(self, engine, custom_value)
}
}
/// Function used to implement the communication protocol between
/// nushell and an external plugin. Both [Plugin] and [StreamingPlugin] are supported.
/// Function used to implement the communication protocol between nushell and an external plugin.
///
/// When creating a new plugin this function is typically used as the main entry
/// point for the plugin, e.g.
@ -670,19 +391,31 @@ impl<T: Plugin> StreamingPlugin for T {
/// # struct MyPlugin;
/// # impl MyPlugin { fn new() -> Self { Self }}
/// # impl Plugin for MyPlugin {
/// # fn signature(&self) -> Vec<PluginSignature> {todo!();}
/// # fn run(&self, name: &str, engine: &EngineInterface, call: &EvaluatedCall, input: &Value)
/// # -> Result<Value, LabeledError> {todo!();}
/// # fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin=Self>>> {todo!();}
/// # }
/// fn main() {
/// serve_plugin(&MyPlugin::new(), MsgPackSerializer)
/// }
/// ```
pub fn serve_plugin(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder + 'static) {
pub fn serve_plugin(plugin: &impl Plugin, encoder: impl PluginEncoder + 'static) {
let mut args = env::args().skip(1);
let number_of_args = args.len();
let first_arg = args.next();
// Determine the plugin name, for errors
let exe = std::env::current_exe().ok();
let plugin_name: String = exe
.as_ref()
.and_then(|path| path.file_stem())
.map(|stem| stem.to_string_lossy().into_owned())
.map(|stem| {
stem.strip_prefix("nu_plugin_")
.map(|s| s.to_owned())
.unwrap_or(stem)
})
.unwrap_or_else(|| "(unknown)".into());
if number_of_args == 0
|| first_arg
.as_ref()
@ -708,6 +441,19 @@ pub fn serve_plugin(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder +
std::process::exit(1)
}
// Build commands map, to make running a command easier
let mut commands: HashMap<String, _> = HashMap::new();
for command in plugin.commands() {
if let Some(previous) = commands.insert(command.signature().sig.name.clone(), command) {
eprintln!(
"Plugin `{plugin_name}` warning: command `{}` shadowed by another command with the \
same name. Check your command signatures",
previous.signature().sig.name
);
}
}
// tell nushell encoding.
//
// 1 byte
@ -735,20 +481,6 @@ pub fn serve_plugin(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder +
// We need to hold on to the interface to keep the manager alive. We can drop it at the end
let interface = manager.get_interface();
// Determine the plugin name, for errors
let exe = std::env::current_exe().ok();
let plugin_name: String = exe
.as_ref()
.and_then(|path| path.file_stem())
.map(|stem| stem.to_string_lossy().into_owned())
.map(|stem| {
stem.strip_prefix("nu_plugin_")
.map(|s| s.to_owned())
.unwrap_or(stem)
})
.unwrap_or_else(|| "(unknown)".into());
// Try an operation that could result in ShellError. Exit if an I/O error is encountered.
// Try to report the error to nushell otherwise, and failing that, panic.
macro_rules! try_or_report {
@ -802,7 +534,15 @@ pub fn serve_plugin(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder +
thread::scope(|scope| {
let run = |engine, call_info| {
let CallInfo { name, call, input } = call_info;
let result = plugin.run(&name, &engine, &call, input);
let result = if let Some(command) = commands.get(&name) {
command.run(plugin, &engine, &call, input)
} else {
Err(LabeledError {
label: format!("Plugin command not found: `{name}`"),
msg: format!("plugin `{plugin_name}` doesn't have this command"),
span: Some(call.head),
})
};
let write_result = engine
.write_response(result)
.and_then(|writer| writer.write());
@ -828,7 +568,11 @@ pub fn serve_plugin(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder +
match plugin_call {
// Sending the signature back to nushell to create the declaration definition
ReceivedPluginCall::Signature { engine } => {
try_or_report!(engine, engine.write_signature(plugin.signature()));
let sigs = commands
.values()
.map(|command| command.signature())
.collect();
try_or_report!(engine, engine.write_signature(sigs));
}
// Run the plugin on a background thread, handling any input or output streams
ReceivedPluginCall::Run { engine, call } => {
@ -866,7 +610,7 @@ pub fn serve_plugin(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder +
}
fn custom_value_op(
plugin: &impl StreamingPlugin,
plugin: &impl Plugin,
engine: &EngineInterface,
custom_value: Spanned<PluginCustomValue>,
op: CustomValueOp,
@ -929,13 +673,14 @@ fn custom_value_op(
}
}
fn print_help(plugin: &impl StreamingPlugin, encoder: impl PluginEncoder) {
fn print_help(plugin: &impl Plugin, encoder: impl PluginEncoder) {
println!("Nushell Plugin");
println!("Encoder: {}", encoder.name());
let mut help = String::new();
plugin.signature().iter().for_each(|signature| {
plugin.commands().into_iter().for_each(|command| {
let signature = command.signature();
let res = write!(help, "\nCommand: {}", signature.sig.name)
.and_then(|_| writeln!(help, "\nUsage:\n > {}", signature.sig.usage))
.and_then(|_| {

View file

@ -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 {

View file

@ -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<Value, ShellError> {
@ -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<Value, LabeledError> {
Ok(DropCheckValue::new(call.req(0)?).into_value(call.head))
}
}

View file

@ -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<Value, LabeledError> {
Ok(CoolCustomValue::new("abc").into_value(call.head))
}
}

View file

@ -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<Value, LabeledError> {
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)
}
}
}

View file

@ -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<nu_protocol::PluginSignature> {
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
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<Value, LabeledError> {
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<dyn CustomValue>,
) -> Result<(), LabeledError> {
// This is how we implement our drop behavior for DropCheck.
if let Some(drop_check) = custom_value.as_any().downcast_ref::<DropCheck>() {
if let Some(drop_check) = custom_value.as_any().downcast_ref::<DropCheckValue>() {
drop_check.notify();
}
Ok(())
}
}
impl CustomValuePlugin {
fn generate(&self, call: &EvaluatedCall, _input: &Value) -> Result<Value, LabeledError> {
Ok(CoolCustomValue::new("abc").into_value(call.head))
}
fn generate2(
&self,
engine: &EngineInterface,
call: &EvaluatedCall,
) -> Result<Value, LabeledError> {
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<Value, LabeledError> {
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<Value, LabeledError> {
Ok(DropCheck::new(call.req(0)?).into_value(call.head))
}
}
fn main() {
serve_plugin(&CustomValuePlugin, MsgPackSerializer {})
}

View file

@ -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<Value, LabeledError> {
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())
}
}

View file

@ -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<Value, LabeledError> {
SimplePluginCommand::run(&Update, plugin, engine, call, &call.req(0)?)
}
}

View file

@ -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;

View file

@ -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<Value, LabeledError> {
plugin.print_values(1, call, input)?;
Ok(Value::nothing(call.head))
}
}

View file

@ -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<Value, LabeledError> {
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))
}
}

View file

@ -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<Value, LabeledError> {
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),
})
}
}

View file

@ -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<Value, LabeledError> {
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),
}),
}
}
}

View file

@ -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<Value, LabeledError> {
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,
))
}
}

View file

@ -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<Value, LabeledError> {
if call.has_flag("cwd")? {
// Get working directory
Ok(Value::string(engine.get_current_dir()?, call.head))
} else if let Some(name) = call.opt::<String>(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,
))
}
}
}

View file

@ -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<Value, LabeledError> {
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<Value, LabeledError> {
self.print_values(1, call, input)?;
Ok(Value::nothing(call.head))
}
pub fn test2(&self, call: &EvaluatedCall, input: &Value) -> Result<Value, LabeledError> {
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<Value, LabeledError> {
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<Value, LabeledError> {
if call.has_flag("cwd")? {
// Get working directory
Ok(Value::string(engine.get_current_dir()?, call.head))
} else if let Some(name) = call.opt::<String>(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<Value, LabeledError> {
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,
))
}
}

View file

@ -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<Box<dyn PluginCommand<Plugin = Self>>> {
// 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),
]
}
}

View file

@ -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<PluginSignature> {
// 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<Value, LabeledError> {
// 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),
}),
}
}
}

View file

@ -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<Value, LabeledError> {
let preview_body: usize = call
.get_flag::<i64>("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<Value, LabeledError> {
let preview_body: usize = call
.get_flag::<i64>("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<PluginExample> {

View file

@ -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<Value, LabeledError> {
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::<String>();
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<Value, LabeledError> {
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::<String>();
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<PluginExample> {

View file

@ -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<Value, LabeledError> {
let span = input.span();
let input_string = input.coerce_str()?;
let head = call.head;
pub struct FromIni;
let ini_config: Result<ini::Ini, ini::ParseError> = 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<Value, LabeledError> {
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, ini::ParseError> = 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()),
}
}

View file

@ -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<Value, LabeledError> {
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::<String>();
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<Value, LabeledError> {
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::<String>();
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<PluginExample> {

View file

@ -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<PluginSignature> {
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
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<Value, LabeledError> {
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),
}),
}
}
}

View file

@ -2,3 +2,4 @@ mod gstat;
mod nu;
pub use gstat::GStat;
pub use nu::GStatPlugin;

View file

@ -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 {})
}

View file

@ -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<PluginSignature> {
vec![PluginSignature::build("gstat")
pub struct GStatPlugin;
impl Plugin for GStatPlugin {
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
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<Value, LabeledError> {
if name != "gstat" {
return Ok(Value::nothing(call.head));
}
let repo_path: Option<Spanned<String>> = call.opt(0)?;
// eprintln!("input value: {:#?}", &input);
let current_dir = engine.get_current_dir()?;

View file

@ -2,3 +2,4 @@ mod inc;
mod nu;
pub use inc::Inc;
pub use nu::IncPlugin;

View file

@ -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 {})
}

View file

@ -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<PluginSignature> {
vec![PluginSignature::build("inc")
pub struct IncPlugin;
impl Plugin for IncPlugin {
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
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<Value, LabeledError> {
if name != "inc" {
return Ok(Value::nothing(call.head));
}
let mut inc = self.clone();
let cell_path: Option<CellPath> = call.opt(0)?;

View file

@ -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;

View file

@ -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<PluginSignature> {
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 <file> | 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<Value, LabeledError> {
// You can use the name to identify what plugin signature was called
let path: Option<Spanned<String>> = 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<PluginExample> {
vec![PluginExample {
example: "http get https://phoronix.com | query web --query 'header' | flatten".into(),
description: "Retrieve all `<header>` 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,
}]
}

View file

@ -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<Spanned<String>>,
) -> Result<Value, LabeledError> {
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<Spanned<String>>,
) -> Result<Value, LabeledError> {
execute_json_query(name, call, input, query)
}
pub fn query_web(
&self,
_name: &str,
call: &EvaluatedCall,
input: &Value,
_rest: Option<Spanned<String>>,
) -> Result<Value, LabeledError> {
parse_selector_params(call, input)
}
pub fn query_xml(
&self,
name: &str,
call: &EvaluatedCall,
input: &Value,
query: Option<Spanned<String>>,
) -> Result<Value, LabeledError> {
execute_xpath_query(name, call, input, query)
impl Plugin for Query {
fn commands(&self) -> Vec<Box<dyn PluginCommand<Plugin = Self>>> {
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<Value, LabeledError> {
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);

View file

@ -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 <file> | 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<Value, LabeledError> {
let query: Option<Spanned<String>> = call.opt(0)?;
execute_json_query(call, input, query)
}
}
pub fn execute_json_query(
_name: &str,
call: &EvaluatedCall,
input: &Value,
query: Option<Spanned<String>>,

View file

@ -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<Value, LabeledError> {
parse_selector_params(call, input)
}
}
pub fn web_examples() -> Vec<PluginExample> {
vec![
PluginExample {
example: "http get https://phoronix.com | query web --query 'header' | flatten".into(),
description: "Retrieve all `<header>` 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,

View file

@ -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<Value, LabeledError> {
let query: Option<Spanned<String>> = call.opt(0)?;
execute_xpath_query(call, input, query)
}
}
pub fn execute_xpath_query(
_name: &str,
call: &EvaluatedCall,
input: &Value,
query: Option<Spanned<String>>,
@ -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),

View file

@ -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`

View file

@ -0,0 +1,51 @@
use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, PluginCommand};
use nu_protocol::{Category, PipelineData, PluginExample, PluginSignature, RawStream, Type, Value};
use crate::StreamExample;
/// `<list<string>> | 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<PipelineData, LabeledError> {
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,
})
}
}

View file

@ -0,0 +1,45 @@
use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, PluginCommand};
use nu_protocol::{Category, PipelineData, PluginExample, PluginSignature, SyntaxShape, Type};
use crate::StreamExample;
/// `<list> | 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<PipelineData, LabeledError> {
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)
}
}

View file

@ -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 <initial> { |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<PipelineData, LabeledError> {
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))
}
}

View file

@ -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;

View file

@ -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 <first> <last>`
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<PipelineData, LabeledError> {
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))
}
}

View file

@ -0,0 +1,91 @@
use nu_plugin::{EngineInterface, EvaluatedCall, LabeledError, PluginCommand};
use nu_protocol::{Category, PipelineData, PluginExample, PluginSignature, Span, Type, Value};
use crate::StreamExample;
/// `<list> | 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<PipelineData, LabeledError> {
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),
}
}
}

View file

@ -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<PipelineData, LabeledError> {
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<PipelineData, LabeledError> {
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<PipelineData, LabeledError> {
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<PipelineData, LabeledError> {
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<PipelineData, LabeledError> {
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))
}
}

View file

@ -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),
}
}
}

View file

@ -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<Box<dyn PluginCommand<Plugin = Self>>> {
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<Value, LabeledError> {
Err(LabeledError {
label: "No subcommand provided".into(),
msg: "add --help here to see usage".into(),
span: Some(call.head.past()),
})
}
}

View file

@ -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

View file

@ -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<PluginSignature> {
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<PipelineData, LabeledError> {
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),
}),
}
}
}

View file

@ -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());
}