Merge pull request #50 from elferherrera/externals

Externals proposal
This commit is contained in:
JT 2021-09-20 09:37:12 +12:00 committed by GitHub
commit cbe85cbeaf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 177 additions and 5 deletions

View file

@ -395,6 +395,15 @@ pub fn report_shell_error(
Label::primary(diag_file_id, diag_range).with_message("cannot find column") Label::primary(diag_file_id, diag_range).with_message("cannot find column")
]) ])
} }
ShellError::ExternalCommand(error, span) => {
let (diag_file_id, diag_range) = convert_span_to_diag(working_set, span)?;
Diagnostic::error()
.with_message("External command")
.with_labels(vec![
Label::primary(diag_file_id, diag_range).with_message(error.to_string())
])
}
}; };
// println!("DIAG"); // println!("DIAG");

View file

@ -6,8 +6,8 @@ use nu_protocol::{
}; };
use crate::{ use crate::{
where_::Where, Alias, Benchmark, BuildString, Def, Do, Each, For, Git, GitCheckout, If, Length, where_::Where, Alias, Benchmark, BuildString, Def, Do, Each, External, For, Git, GitCheckout,
Let, LetEnv, ListGitBranches, Ls, Table, If, Length, Let, LetEnv, ListGitBranches, Ls, Table,
}; };
pub fn create_default_context() -> Rc<RefCell<EngineState>> { pub fn create_default_context() -> Rc<RefCell<EngineState>> {
@ -48,6 +48,8 @@ pub fn create_default_context() -> Rc<RefCell<EngineState>> {
working_set.add_decl(Box::new(Table)); working_set.add_decl(Box::new(Table));
working_set.add_decl(Box::new(External));
// This is a WIP proof of concept // This is a WIP proof of concept
working_set.add_decl(Box::new(ListGitBranches)); working_set.add_decl(Box::new(ListGitBranches));
working_set.add_decl(Box::new(Git)); working_set.add_decl(Box::new(Git));

View file

@ -14,6 +14,7 @@ mod let_;
mod let_env; mod let_env;
mod list_git_branches; mod list_git_branches;
mod ls; mod ls;
mod run_external;
mod table; mod table;
mod where_; mod where_;
@ -33,4 +34,5 @@ pub use let_::Let;
pub use let_env::LetEnv; pub use let_env::LetEnv;
pub use list_git_branches::ListGitBranches; pub use list_git_branches::ListGitBranches;
pub use ls::Ls; pub use ls::Ls;
pub use run_external::External;
pub use table::Table; pub use table::Table;

View file

@ -0,0 +1,123 @@
use std::env;
use std::process::Command as CommandSys;
use nu_protocol::{
ast::{Call, Expression},
engine::{Command, EvaluationContext},
ShellError, Signature, SyntaxShape, Value,
};
use nu_engine::eval_expression;
pub struct External;
impl Command for External {
fn name(&self) -> &str {
"run_external"
}
fn usage(&self) -> &str {
"Runs external command"
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("run_external").rest("rest", SyntaxShape::Any, "external command to run")
}
fn run(
&self,
context: &EvaluationContext,
call: &Call,
input: Value,
) -> Result<Value, ShellError> {
let command = ExternalCommand::try_new(call, context)?;
command.run_with_input(input)
}
}
pub struct ExternalCommand<'call, 'contex> {
pub name: &'call Expression,
pub args: &'call [Expression],
pub context: &'contex EvaluationContext,
}
impl<'call, 'contex> ExternalCommand<'call, 'contex> {
pub fn try_new(
call: &'call Call,
context: &'contex EvaluationContext,
) -> Result<Self, ShellError> {
if call.positional.is_empty() {
return Err(ShellError::ExternalNotSupported(call.head));
}
Ok(Self {
name: &call.positional[0],
args: &call.positional[1..],
context,
})
}
pub fn get_name(&self) -> Result<String, ShellError> {
let value = eval_expression(self.context, self.name)?;
value.as_string()
}
pub fn get_args(&self) -> Vec<String> {
self.args
.iter()
.filter_map(|expr| eval_expression(self.context, expr).ok())
.filter_map(|value| value.as_string().ok())
.collect()
}
pub fn run_with_input(&self, _input: Value) -> Result<Value, ShellError> {
let mut process = self.create_command();
// TODO. We don't have a way to know the current directory
// This should be information from the EvaluationContex or EngineState
let path = env::current_dir().unwrap();
process.current_dir(path);
let envs = self.context.stack.get_env_vars();
process.envs(envs);
match process.spawn() {
Err(err) => Err(ShellError::ExternalCommand(
format!("{}", err),
self.name.span,
)),
Ok(mut child) => match child.wait() {
Err(err) => Err(ShellError::ExternalCommand(
format!("{}", err),
self.name.span,
)),
Ok(_) => Ok(Value::nothing()),
},
}
}
fn create_command(&self) -> CommandSys {
// in all the other cases shell out
if cfg!(windows) {
//TODO. This should be modifiable from the config file.
// We could give the option to call from powershell
// for minimal builds cwd is unused
let mut process = CommandSys::new("cmd");
process.arg("/c");
process.arg(&self.get_name().unwrap());
for arg in self.get_args() {
// Clean the args before we use them:
// https://stackoverflow.com/questions/1200235/how-to-pass-a-quoted-pipe-character-to-cmd-exe
// cmd.exe needs to have a caret to escape a pipe
let arg = arg.replace("|", "^|");
process.arg(&arg);
}
process
} else {
let cmd_with_args = vec![self.get_name().unwrap(), self.get_args().join(" ")].join(" ");
let mut process = CommandSys::new("sh");
process.arg("-c").arg(cmd_with_args);
process
}
}
}

View file

@ -1,6 +1,6 @@
use nu_protocol::ast::{Block, Call, Expr, Expression, Operator, Statement}; use nu_protocol::ast::{Block, Call, Expr, Expression, Operator, Statement};
use nu_protocol::engine::EvaluationContext; use nu_protocol::engine::EvaluationContext;
use nu_protocol::{Range, ShellError, Span, Value}; use nu_protocol::{Range, ShellError, Span, Type, Value};
pub fn eval_operator(op: &Expression) -> Result<Operator, ShellError> { pub fn eval_operator(op: &Expression) -> Result<Operator, ShellError> {
match op { match op {
@ -125,7 +125,38 @@ pub fn eval_expression(
} }
Expr::RowCondition(_, expr) => eval_expression(context, expr), Expr::RowCondition(_, expr) => eval_expression(context, expr),
Expr::Call(call) => eval_call(context, call, Value::nothing()), Expr::Call(call) => eval_call(context, call, Value::nothing()),
Expr::ExternalCall(_, _) => Err(ShellError::ExternalNotSupported(expr.span)), Expr::ExternalCall(name, args) => {
let engine_state = context.engine_state.borrow();
let decl_id = engine_state
.find_decl("run_external".as_bytes())
.ok_or_else(|| ShellError::ExternalNotSupported(*name))?;
let command = engine_state.get_decl(decl_id);
let new_context = context.enter_scope();
let mut call = Call::new();
call.positional = [*name]
.iter()
.chain(args.iter())
.map(|span| {
let contents = engine_state.get_span_contents(span);
let val = String::from_utf8_lossy(contents);
Expression {
expr: Expr::String(val.into()),
span: *span,
ty: Type::String,
custom_completion: None,
}
})
.collect();
let value = Value::Nothing {
span: Span::new(0, 1),
};
command.run(&new_context, &call, value)
}
Expr::Operator(_) => Ok(Value::Nothing { span: expr.span }), Expr::Operator(_) => Ok(Value::Nothing { span: expr.span }),
Expr::BinaryOp(lhs, op, rhs) => { Expr::BinaryOp(lhs, op, rhs) => {
let op_span = op.span; let op_span = op.span;

View file

@ -137,7 +137,7 @@ impl EngineState {
output output
} }
pub fn get_span_contents(&self, span: Span) -> &[u8] { pub fn get_span_contents(&self, span: &Span) -> &[u8] {
&self.file_contents[span.start..span.end] &self.file_contents[span.start..span.end]
} }

View file

@ -104,6 +104,10 @@ impl Stack {
}))) })))
} }
pub fn get_env_vars(&self) -> HashMap<String, String> {
self.0.borrow().env_vars.clone()
}
pub fn print_stack(&self) { pub fn print_stack(&self) {
println!("===frame==="); println!("===frame===");
println!("vars:"); println!("vars:");

View file

@ -21,4 +21,5 @@ pub enum ShellError {
AccessBeyondEndOfStream(Span), AccessBeyondEndOfStream(Span),
IncompatiblePathAccess(String, Span), IncompatiblePathAccess(String, Span),
CantFindColumn(Span), CantFindColumn(Span),
ExternalCommand(String, Span),
} }