diff --git a/crates/nu-command/src/core_commands/register.rs b/crates/nu-command/src/core_commands/register.rs index 85d843e848..ffa6f9d768 100644 --- a/crates/nu-command/src/core_commands/register.rs +++ b/crates/nu-command/src/core_commands/register.rs @@ -32,6 +32,12 @@ impl Command for Register { SyntaxShape::Any, "Block with signature description as json object", ) + .named( + "shell", + SyntaxShape::Filepath, + "path of shell used to run plugin (cmd, sh, python, etc)", + Some('s'), + ) .category(Category::Core) } diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 5238fc94bc..e338ac52f1 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -1195,7 +1195,7 @@ pub fn parse_register( .map(|expr| { let name_expr = working_set.get_span_contents(expr.span); String::from_utf8(name_expr.to_vec()) - .map_err(|_| ParseError::NonUtf8(spans[1])) + .map_err(|_| ParseError::NonUtf8(expr.span)) .and_then(|name| { canonicalize(&name).map_err(|_| ParseError::FileNotFound(name, expr.span)) }) @@ -1224,7 +1224,7 @@ pub fn parse_register( .map(|encoding| (path, encoding)) }); - // Signature is the only optional value from the call and will be used to decide if + // Signature is an optional value from the call and will be used to decide if // the plugin is called to get the signatures or to use the given signature let signature = call.positional.get(1).map(|expr| { let signature = working_set.get_span_contents(expr.span); @@ -1237,16 +1237,52 @@ pub fn parse_register( }) }); + // Shell is another optional value used as base to call shell to plugins + let shell = call.get_flag_expr("shell").map(|expr| { + let shell_expr = working_set.get_span_contents(expr.span); + + String::from_utf8(shell_expr.to_vec()) + .map_err(|_| ParseError::NonUtf8(expr.span)) + .and_then(|name| { + canonicalize(&name).map_err(|_| ParseError::FileNotFound(name, expr.span)) + }) + .and_then(|path| { + if path.exists() & path.is_file() { + Ok(path) + } else { + Err(ParseError::FileNotFound(format!("{:?}", path), expr.span)) + } + }) + }); + + let shell = match shell { + None => None, + Some(path) => match path { + Ok(path) => Some(path), + Err(err) => { + return ( + Statement::Pipeline(Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Unknown, + custom_completion: None, + }])), + Some(err), + ); + } + }, + }; + let error = match signature { Some(signature) => arguments.and_then(|(path, encoding)| { signature.map(|signature| { - let plugin_decl = PluginDeclaration::new(path, signature, encoding); + let plugin_decl = PluginDeclaration::new(path, signature, encoding, shell); working_set.add_decl(Box::new(plugin_decl)); working_set.mark_plugins_file_dirty(); }) }), None => arguments.and_then(|(path, encoding)| { - get_signature(path.as_path(), &encoding) + get_signature(path.as_path(), &encoding, &shell) .map_err(|err| { ParseError::LabeledError( "Error getting signatures".into(), @@ -1258,8 +1294,12 @@ pub fn parse_register( for signature in signatures { // create plugin command declaration (need struct impl Command) // store declaration in working set - let plugin_decl = - PluginDeclaration::new(path.clone(), signature, encoding.clone()); + let plugin_decl = PluginDeclaration::new( + path.clone(), + signature, + encoding.clone(), + shell.clone(), + ); working_set.add_decl(Box::new(plugin_decl)); } diff --git a/crates/nu-plugin/src/plugin/declaration.rs b/crates/nu-plugin/src/plugin/declaration.rs index 67ca2be1a6..a3c870f4c5 100644 --- a/crates/nu-plugin/src/plugin/declaration.rs +++ b/crates/nu-plugin/src/plugin/declaration.rs @@ -14,16 +14,23 @@ pub struct PluginDeclaration { name: String, signature: Signature, filename: PathBuf, + shell: Option, encoding: EncodingType, } impl PluginDeclaration { - pub fn new(filename: PathBuf, signature: Signature, encoding: EncodingType) -> Self { + pub fn new( + filename: PathBuf, + signature: Signature, + encoding: EncodingType, + shell: Option, + ) -> Self { Self { name: signature.name.clone(), signature, filename, encoding, + shell, } } } @@ -52,7 +59,7 @@ impl Command for PluginDeclaration { // Decode information from plugin // Create PipelineData let source_file = Path::new(&self.filename); - let mut plugin_cmd = create_command(source_file); + let mut plugin_cmd = create_command(source_file, &self.shell); let mut child = plugin_cmd.spawn().map_err(|err| { let decl = engine_state.get_decl(call.decl_id); @@ -131,7 +138,7 @@ impl Command for PluginDeclaration { Ok(pipeline_data) } - fn is_plugin(&self) -> Option<(&PathBuf, &str)> { - Some((&self.filename, self.encoding.to_str())) + fn is_plugin(&self) -> Option<(&PathBuf, &str, &Option)> { + Some((&self.filename, self.encoding.to_str(), &self.shell)) } } diff --git a/crates/nu-plugin/src/plugin/is_plugin b/crates/nu-plugin/src/plugin/is_plugin new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs index 5467d72251..91e1e1657a 100644 --- a/crates/nu-plugin/src/plugin/mod.rs +++ b/crates/nu-plugin/src/plugin/mod.rs @@ -4,7 +4,7 @@ pub use declaration::PluginDeclaration; use crate::protocol::{LabeledError, PluginCall, PluginResponse}; use crate::EncodingType; use std::io::BufReader; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::{Command as CommandSys, Stdio}; use nu_protocol::ShellError; @@ -35,10 +35,15 @@ pub trait PluginEncoder: Clone { ) -> Result; } -fn create_command(path: &Path) -> CommandSys { - let mut process = match path.extension() { - None => std::process::Command::new(path), - Some(extension) => { +fn create_command(path: &Path, shell: &Option) -> CommandSys { + let mut process = match (path.extension(), shell) { + (_, Some(shell)) => { + let mut process = std::process::Command::new(shell); + process.arg(path); + + process + } + (Some(extension), None) => { let (shell, separator) = match extension.to_str() { Some("cmd") | Some("bat") => (Some("cmd"), Some("/c")), Some("sh") => (Some("sh"), Some("-c")), @@ -63,6 +68,7 @@ fn create_command(path: &Path) -> CommandSys { _ => std::process::Command::new(path), } } + (None, None) => std::process::Command::new(path), }; // Both stdout and stdin are piped so we can receive information from the plugin @@ -71,8 +77,12 @@ fn create_command(path: &Path) -> CommandSys { process } -pub fn get_signature(path: &Path, encoding: &EncodingType) -> Result, ShellError> { - let mut plugin_cmd = create_command(path); +pub fn get_signature( + path: &Path, + encoding: &EncodingType, + shell: &Option, +) -> Result, ShellError> { + let mut plugin_cmd = create_command(path, shell); let mut child = plugin_cmd.spawn().map_err(|err| { ShellError::PluginFailedToLoad(format!("Error spawning child process: {}", err)) diff --git a/crates/nu-protocol/src/engine/command.rs b/crates/nu-protocol/src/engine/command.rs index 21b6b1a231..9615e6b5b4 100644 --- a/crates/nu-protocol/src/engine/command.rs +++ b/crates/nu-protocol/src/engine/command.rs @@ -48,8 +48,9 @@ pub trait Command: Send + Sync + CommandClone { self.name().contains(' ') } - // Is a plugin command (returns plugin's path and encoding if yes) - fn is_plugin(&self) -> Option<(&PathBuf, &str)> { + // Is a plugin command (returns plugin's path, encoding and type of shell + // if the declaration is a plugin) + fn is_plugin(&self) -> Option<(&PathBuf, &str, &Option)> { None } diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 0a56d4e0f4..9bbde51bea 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -230,12 +230,33 @@ impl EngineState { self.plugin_decls().try_for_each(|decl| { // A successful plugin registration already includes the plugin filename // No need to check the None option - let (path, encoding) = decl.is_plugin().expect("plugin should have file name"); - let file_name = path.to_str().expect("path should be a str"); + let (path, encoding, shell) = + decl.is_plugin().expect("plugin should have file name"); + let file_name = path + .to_str() + .expect("path was checked during registration as a str"); serde_json::to_string_pretty(&decl.signature()) .map(|signature| { - format!("register {} -e {} {}\n\n", file_name, encoding, signature) + // Extracting the possible path to the shell used to load the plugin + let shell_str = match shell { + Some(path) => format!( + "-s {}", + path.to_str().expect( + "shell path was checked during registration as a str" + ) + ), + None => "".into(), + }; + + // Each signature is stored in the plugin file with the required + // encoding, shell and signature + // This information will be used when loading the plugin + // information when nushell starts + format!( + "register {} -e {} {} {}\n\n", + file_name, encoding, shell_str, signature + ) }) .map_err(|err| ShellError::PluginFailedToLoad(err.to_string())) .and_then(|line| { diff --git a/crates/nu_plugin_python/plugin.py b/crates/nu_plugin_python/plugin.py index 23d7cea2cf..dceb861044 100644 --- a/crates/nu_plugin_python/plugin.py +++ b/crates/nu_plugin_python/plugin.py @@ -21,6 +21,7 @@ # native python dictionaries. The encoding and decoding process could be improved # by using libraries like pydantic and marshmallow # +# This plugin uses python3 # Note: To debug plugins write to stderr using sys.stderr.write import sys import json