From fac2f43aa49377488d3dd4247f24534772ebdc8c Mon Sep 17 00:00:00 2001 From: Devyn Cairns Date: Thu, 18 Apr 2024 23:53:30 -0700 Subject: [PATCH] Add an example Nushell plugin written in Nushell itself (#12574) # Description As suggested by @fdncred. It's neat that this is possible, but the particularly useful part of this is that we can actually test it because it doesn't have any external dependencies, unlike the python plugin. Right now this just implements exactly the same behavior as the python plugin, but we could have it exercise a few more things. Also fixes a couple of bugs: - `.nu` plugins were not run with `nu --stdin`, so they couldn't take input. - `register` couldn't be called if `--no-config-file` was set, because it would error on trying to update the plugin file. # User-Facing Changes - `nu_plugin_nu_example` plugin added. - `register` now works in `--no-config-file` mode. # Tests + Formatting Tests added for `nu_plugin_nu_example`. - :green_circle: `toolkit fmt` - :green_circle: `toolkit clippy` - :green_circle: `toolkit test` - :green_circle: `toolkit test stdlib` # After Submitting - [ ] Add the version bump to the release script just like for python --- crates/nu-plugin/src/plugin/mod.rs | 5 +- crates/nu-protocol/src/engine/engine_state.rs | 4 +- .../nu_plugin_nu_example.nu | 260 ++++++++++++++++++ tests/plugins/mod.rs | 1 + tests/plugins/nu_plugin_nu_example.rs | 26 ++ 5 files changed, 294 insertions(+), 2 deletions(-) create mode 100755 crates/nu_plugin_nu_example/nu_plugin_nu_example.nu create mode 100644 tests/plugins/nu_plugin_nu_example.rs diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs index 3366688a11..0345873404 100644 --- a/crates/nu-plugin/src/plugin/mod.rs +++ b/crates/nu-plugin/src/plugin/mod.rs @@ -113,7 +113,10 @@ fn create_command(path: &Path, mut shell: Option<&Path>, mode: &CommunicationMod Some(Path::new("sh")) } } - Some("nu") => Some(Path::new("nu")), + Some("nu") => { + shell_args.push("--stdin"); + Some(Path::new("nu")) + } Some("py") => Some(Path::new("python")), Some("rb") => Some(Path::new("ruby")), Some("jar") => { diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 17a4ccb5eb..eca752f1e2 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -269,7 +269,9 @@ impl EngineState { #[cfg(feature = "plugin")] if delta.plugins_changed { // Update the plugin file with the new signatures. - self.update_plugin_file()?; + if self.plugin_signatures.is_some() { + self.update_plugin_file()?; + } } Ok(()) diff --git a/crates/nu_plugin_nu_example/nu_plugin_nu_example.nu b/crates/nu_plugin_nu_example/nu_plugin_nu_example.nu new file mode 100755 index 0000000000..0411147e64 --- /dev/null +++ b/crates/nu_plugin_nu_example/nu_plugin_nu_example.nu @@ -0,0 +1,260 @@ +#!/usr/bin/env -S nu --stdin +# Example of using a Nushell script as a Nushell plugin +# +# This is a port of the nu_plugin_python_example plugin to Nushell itself. There is probably not +# really any reason to write a Nushell plugin in Nushell, but this is a fun proof of concept, and +# it also allows us to test the plugin interface with something manually implemented in a scripting +# language without adding any extra dependencies to our tests. + +const NUSHELL_VERSION = "0.92.3" + +def main [--stdio] { + if ($stdio) { + start_plugin + } else { + print -e "Run me from inside nushell!" + exit 1 + } +} + +const SIGNATURES = [ + { + sig: { + name: nu_plugin_nu_example, + usage: "Signature test for Nushell plugin in Nushell", + extra_usage: "", + required_positional: [ + [ + name, + desc, + shape + ]; + [ + a, + "required integer value", + Int + ], + [ + b, + "required string value", + String + ] + ], + optional_positional: [ + [ + name, + desc, + shape + ]; + [ + opt, + "Optional number", + Int + ] + ], + rest_positional: { + name: rest, + desc: "rest value string", + shape: String + }, + named: [ + [ + long, + short, + arg, + required, + desc + ]; + [ + help, + h, + null, + false, + "Display the help message for this command" + ], + [ + flag, + f, + null, + false, + "a flag for the signature" + ], + [ + named, + n, + String, + false, + "named string" + ] + ], + input_output_types: [ + [Any, Any] + ], + allow_variants_without_examples: true, + search_terms: [ + Example + ], + is_filter: false, + creates_scope: false, + allows_unknown_args: false, + category: Experimental + }, + examples: [] + } +] + +def process_call [ + id: int, + plugin_call: record< + name: string, + call: record< + head: record, + positional: list, + named: list, + >, + input: any + > +] { + # plugin_call is a dictionary with the information from the call + # It should contain: + # - The name of the call + # - The call data which includes the positional and named values + # - The input from the pipeline + + # Use this information to implement your plugin logic + + # Print the call to stderr, in raw nuon and as a table + $plugin_call | to nuon --raw | print -e + $plugin_call | table -e | print -e + + # Get the span from the call + let span = $plugin_call.call.head + + # Create a Value of type List that will be encoded and sent to Nushell + let value = { + Value: { + List: { + vals: (0..9 | each { |x| + { + Record: { + val: ( + [one two three] | + zip (0..2 | each { |y| + { + Int: { + val: ($x * $y), + span: $span, + } + } + }) | + each { into record } | + transpose --as-record --header-row + ), + span: $span + } + } + }), + span: $span + } + } + } + + write_response $id { PipelineData: $value } +} + +def tell_nushell_encoding [] { + print -n "\u{0004}json" +} + +def tell_nushell_hello [] { + # A `Hello` message is required at startup to inform nushell of the protocol capabilities and + # compatibility of the plugin. The version specified should be the version of nushell that this + # plugin was tested and developed against. + let hello = { + Hello: { + protocol: "nu-plugin", # always this value + version: $NUSHELL_VERSION, + features: [] + } + } + $hello | to json --raw | print +} + +def write_response [id: int, response: record] { + # Use this format to send a response to a plugin call. The ID of the plugin call is required. + let wrapped_response = { + CallResponse: [ + $id, + $response, + ] + } + $wrapped_response | to json --raw | print +} + +def write_error [id: int, text: string, span?: record] { + # Use this error format to send errors to nushell in response to a plugin call. The ID of the + # plugin call is required. + let error = if ($span | is-not-empty) { + { + Error: { + msg: "ERROR from plugin", + labels: [ + { + text: $text, + span: $span, + } + ], + } + } + } else { + { + Error: { + msg: "ERROR from plugin", + help: $text, + } + } + } + write_response $id $error +} + +def handle_input []: any -> nothing { + match $in { + { Hello: $hello } => { + if ($hello.version != $NUSHELL_VERSION) { + exit 1 + } + } + "Goodbye" => { + exit 0 + } + { Call: [$id, $plugin_call] } => { + match $plugin_call { + "Signature" => { + write_response $id { Signature: $SIGNATURES } + } + { Run: $call_info } => { + process_call $id $call_info + } + _ => { + write_error $id $"Operation not supported: ($plugin_call | to json --raw)" + } + } + } + $other => { + print -e $"Unknown message: ($other | to json --raw)" + exit 1 + } + } +} + +def start_plugin [] { + lines | + prepend (do { + # This is a hack so that we do this first, but we can also take input as a stream + tell_nushell_encoding + tell_nushell_hello + [] + }) | + each { from json | handle_input } | + ignore +} diff --git a/tests/plugins/mod.rs b/tests/plugins/mod.rs index f52006d3ad..7092b3a50d 100644 --- a/tests/plugins/mod.rs +++ b/tests/plugins/mod.rs @@ -3,6 +3,7 @@ mod core_inc; mod custom_values; mod env; mod formats; +mod nu_plugin_nu_example; mod register; mod stream; mod stress_internals; diff --git a/tests/plugins/nu_plugin_nu_example.rs b/tests/plugins/nu_plugin_nu_example.rs new file mode 100644 index 0000000000..aa807b874e --- /dev/null +++ b/tests/plugins/nu_plugin_nu_example.rs @@ -0,0 +1,26 @@ +use nu_test_support::nu; + +#[test] +fn register() { + let out = nu!("register crates/nu_plugin_nu_example/nu_plugin_nu_example.nu"); + assert!(out.status.success()); + assert!(out.out.trim().is_empty()); + assert!(out.err.trim().is_empty()); +} + +#[test] +fn call() { + let out = nu!(r#" + register crates/nu_plugin_nu_example/nu_plugin_nu_example.nu + nu_plugin_nu_example 4242 teststring + "#); + assert!(out.status.success()); + + assert!(out.err.contains("name: nu_plugin_nu_example")); + assert!(out.err.contains("4242")); + assert!(out.err.contains("teststring")); + + assert!(out.out.contains("one")); + assert!(out.out.contains("two")); + assert!(out.out.contains("three")); +}