Plugin option for shell (#517)

* calling plugin without shell

* spelling error

* option on register to select a shell
This commit is contained in:
Fernando Herrera 2021-12-18 18:13:56 +00:00 committed by GitHub
parent b3b328d19d
commit 8933dde324
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 108 additions and 22 deletions

View File

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

View File

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

View File

@ -14,16 +14,23 @@ pub struct PluginDeclaration {
name: String,
signature: Signature,
filename: PathBuf,
shell: Option<PathBuf>,
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<PathBuf>,
) -> 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<PathBuf>)> {
Some((&self.filename, self.encoding.to_str(), &self.shell))
}
}

View File

View File

@ -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<PluginResponse, ShellError>;
}
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<PathBuf>) -> 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<Vec<Signature>, ShellError> {
let mut plugin_cmd = create_command(path);
pub fn get_signature(
path: &Path,
encoding: &EncodingType,
shell: &Option<PathBuf>,
) -> Result<Vec<Signature>, 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))

View File

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

View File

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

View File

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