From d8847f108289ceb347f597fd7c74e316b4c4ad3f Mon Sep 17 00:00:00 2001 From: Fernando Herrera Date: Sat, 18 Dec 2021 15:52:27 +0000 Subject: [PATCH] Calling plugin without shell (#516) * calling plugin without shell * spelling error --- crates/nu-plugin/src/plugin/mod.rs | 41 ++- crates/nu-protocol/src/shell_error.rs | 6 +- crates/nu_plugin_python/plugin.py | 408 ++++++++++++++++++++++++++ 3 files changed, 440 insertions(+), 15 deletions(-) create mode 100644 crates/nu_plugin_python/plugin.py diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs index de0813381..5467d7225 100644 --- a/crates/nu-plugin/src/plugin/mod.rs +++ b/crates/nu-plugin/src/plugin/mod.rs @@ -36,17 +36,33 @@ pub trait PluginEncoder: Clone { } fn create_command(path: &Path) -> CommandSys { - //TODO. The selection of shell could be modifiable from the config file. - let mut process = if cfg!(windows) { - let mut process = CommandSys::new("cmd"); - process.arg("/c").arg(path); + let mut process = match path.extension() { + None => std::process::Command::new(path), + Some(extension) => { + let (shell, separator) = match extension.to_str() { + Some("cmd") | Some("bat") => (Some("cmd"), Some("/c")), + Some("sh") => (Some("sh"), Some("-c")), + Some("py") => (Some("python"), None), + _ => (None, None), + }; - process - } else { - let mut process = CommandSys::new("sh"); - process.arg("-c").arg(path); + match (shell, separator) { + (Some(shell), Some(separator)) => { + let mut process = std::process::Command::new(shell); + process.arg(separator); + process.arg(path); - process + process + } + (Some(shell), None) => { + let mut process = std::process::Command::new(shell); + process.arg(path); + + process + } + _ => std::process::Command::new(path), + } + } }; // Both stdout and stdin are piped so we can receive information from the plugin @@ -90,9 +106,10 @@ pub fn get_signature(path: &Path, encoding: &EncodingType) -> Result Ok(signatures), + Err(err) => Err(ShellError::PluginFailedToLoad(format!("{}", err))), + } } // The next trait and functions are part of the plugin that is being created diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 8e6c8f767..3708c1ce8 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -168,15 +168,15 @@ pub enum ShellError { #[diagnostic(code(nu::shell::file_not_found), url(docsrs))] FileNotFoundCustom(String, #[label("{0}")] Span), - #[error("Plugin failed to load")] + #[error("Plugin failed to load: {0}")] #[diagnostic(code(nu::shell::plugin_failed_to_load), url(docsrs))] PluginFailedToLoad(String), - #[error("Plugin failed to encode")] + #[error("Plugin failed to encode: {0}")] #[diagnostic(code(nu::shell::plugin_failed_to_encode), url(docsrs))] PluginFailedToEncode(String), - #[error("Plugin failed to decode")] + #[error("Plugin failed to decode: {0}")] #[diagnostic(code(nu::shell::plugin_failed_to_decode), url(docsrs))] PluginFailedToDecode(String), diff --git a/crates/nu_plugin_python/plugin.py b/crates/nu_plugin_python/plugin.py new file mode 100644 index 000000000..23d7cea2c --- /dev/null +++ b/crates/nu_plugin_python/plugin.py @@ -0,0 +1,408 @@ +# Example of using python as script to create plugins for nushell +# +# The example uses JSON encoding but it should be a similar process using +# capnp proto to move data betwee nushell and the plugin. The only difference +# would be that you need to compile the schema file in order have the objects +# that decode and encode information that is read and written to stdin and stdour +# +# To register the plugin use: +# register -e json +# +# Be carefull with the spans. Miette will crash if a span is outside the +# size of the contents vector. For this example we are using 0 and 1, which will +# point to the beginning of the contents vector. We strongly suggest using the span +# found in the plugin call head +# +# The plugin will be run using the active python implementation. If you are in +# a python environment, that is the python version that is used +# +# Note: To keep the plugin simple and without dependencies, the dictionaries that +# represent the data transferred between nushell and the plugin are kept as +# native python dictionaries. The encoding and decoding process could be improved +# by using libraries like pydantic and marshmallow +# +# Note: To debug plugins write to stderr using sys.stderr.write +import sys +import json + + +def signatures(): + """ + Multiple signatures can be sent to nushell. Each signature will be registered + as a different plugin function in nushell. + + In your plugin logic you can use the name of the signature to indicate what + operation should be done with the plugin + """ + return { + "Signature": [ + { + "name": "nu-python", + "usage": "Signature test for python", + "extra_usage": "", + "required_positional": [ + { + "name": "a", + "desc": "required integer value", + "shape": "Int", + "var_id": None, + }, + { + "name": "b", + "desc": "required string value", + "shape": "String", + "var_id": None, + }, + ], + "optional_positional": [ + { + "name": "opt", + "desc": "Optional number", + "shape": "Int", + "var_id": None, + } + ], + "rest_positional": { + "name": "rest", + "desc": "rest value string", + "shape": "String", + "var_id": None, + }, + "named": [ + { + "long": "flag", + "short": "f", + "arg": None, + "required": False, + "desc": "a flag for the signature", + "var_id": None, + }, + { + "long": "named", + "short": "n", + "arg": "String", + "required": False, + "desc": "named string", + "var_id": None, + }, + ], + "is_filter": False, + "creates_scope": False, + "category": "Experimental", + } + ] + } + + +def process_call(plugin_call): + """ + 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 pippeline + + Use this information to implement your plugin logic + """ + # Pretty printing the call to stderr + sys.stderr.write(f"{json.dumps(plugin_call, indent=4)}") + sys.stderr.write("\n") + + # Creates a Value of type List that will be encoded and sent to nushell + return { + "Value": { + "List": { + "vals": [ + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 1, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 2, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 2, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 4, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 3, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 6, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 4, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 8, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 5, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 10, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 6, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 12, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 7, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 14, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 8, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 16, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + { + "Record": { + "cols": ["one", "two", "three"], + "vals": [ + { + "Int": { + "val": 0, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 9, + "span": {"start": 0, "end": 1}, + } + }, + { + "Int": { + "val": 18, + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + }, + ], + "span": {"start": 0, "end": 1}, + } + } + } + + +def plugin(): + call_str = ",".join(sys.stdin.readlines()) + plugin_call = json.loads(call_str) + + if plugin_call == "Signature": + signature = json.dumps(signatures()) + sys.stdout.write(signature) + + elif "CallInfo" in plugin_call: + response = process_call(plugin_call) + sys.stdout.write(json.dumps(response)) + + else: + # Use this error format if you want to return an error back to nushell + error = { + "Error": { + "label": "ERROR from plugin", + "msg": "error message pointing to call head span", + "span": {"start": 0, "end": 1}, + } + } + sys.stdout.write(json.dumps(error)) + + +if __name__ == "__main__": + plugin()