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")); +}