diff --git a/crates/nu-cli/src/errors.rs b/crates/nu-cli/src/errors.rs index a7ee99216..4c04481fe 100644 --- a/crates/nu-cli/src/errors.rs +++ b/crates/nu-cli/src/errors.rs @@ -395,6 +395,15 @@ pub fn report_shell_error( 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(format!("{}", error)) + ]) + } }; // println!("DIAG"); diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index 9291a9742..91c13590f 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -6,8 +6,8 @@ use nu_protocol::{ }; use crate::{ - where_::Where, Alias, Benchmark, BuildString, Def, Do, Each, For, Git, GitCheckout, If, Length, - Let, LetEnv, ListGitBranches, Ls, Table, + where_::Where, Alias, Benchmark, BuildString, Def, Do, Each, External, For, Git, GitCheckout, + If, Length, Let, LetEnv, ListGitBranches, Ls, Table, }; pub fn create_default_context() -> Rc> { @@ -48,6 +48,8 @@ pub fn create_default_context() -> Rc> { working_set.add_decl(Box::new(Table)); + working_set.add_decl(Box::new(External)); + // This is a WIP proof of concept working_set.add_decl(Box::new(ListGitBranches)); working_set.add_decl(Box::new(Git)); diff --git a/crates/nu-command/src/lib.rs b/crates/nu-command/src/lib.rs index a508e0cb4..96f0839fb 100644 --- a/crates/nu-command/src/lib.rs +++ b/crates/nu-command/src/lib.rs @@ -14,6 +14,7 @@ mod let_; mod let_env; mod list_git_branches; mod ls; +mod run_external; mod table; mod where_; @@ -33,4 +34,5 @@ pub use let_::Let; pub use let_env::LetEnv; pub use list_git_branches::ListGitBranches; pub use ls::Ls; +pub use run_external::External; pub use table::Table; diff --git a/crates/nu-command/src/run_external.rs b/crates/nu-command/src/run_external.rs new file mode 100644 index 000000000..78ac39286 --- /dev/null +++ b/crates/nu-command/src/run_external.rs @@ -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 { + 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 { + if call.positional.len() == 0 { + return Err(ShellError::ExternalNotSupported(call.head)); + } + + Ok(Self { + name: &call.positional[0], + args: &call.positional[1..], + context: context, + }) + } + + pub fn get_name(&self) -> Result { + let value = eval_expression(self.context, self.name)?; + value.as_string() + } + + pub fn get_args(&self) -> Vec { + 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 { + 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 + } + } +} diff --git a/crates/nu-engine/src/eval.rs b/crates/nu-engine/src/eval.rs index 143379a75..8153081c1 100644 --- a/crates/nu-engine/src/eval.rs +++ b/crates/nu-engine/src/eval.rs @@ -1,8 +1,6 @@ -use std::process::Command; - use nu_protocol::ast::{Block, Call, Expr, Expression, Operator, Statement}; 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 { match op { @@ -70,33 +68,6 @@ fn eval_call(context: &EvaluationContext, call: &Call, input: Value) -> Result, - expr: &Expression, -) -> Result { - let state = context.engine_state.borrow(); - let name = state.get_span_contents(command_span); - let cmd = std::str::from_utf8(name).unwrap(); - - let args = spans - .iter() - .map(|span| { - let val = state.get_span_contents(span); - std::str::from_utf8(val).unwrap() - }) - .collect::>(); - - let output = Command::new(cmd).args(args).output().unwrap(); - - println!("{:?}", output); - let test = output.stdout; - println!("{:?}", test); - - Err(ShellError::ExternalNotSupported(expr.span)) -} - pub fn eval_expression( context: &EvaluationContext, expr: &Expression, @@ -154,8 +125,37 @@ pub fn eval_expression( } Expr::RowCondition(_, expr) => eval_expression(context, expr), Expr::Call(call) => eval_call(context, call, Value::nothing()), - Expr::ExternalCall(command_span, spans) => { - eval_external(context, command_span, spans, expr) + Expr::ExternalCall(name, args) => { + let engine_state = context.engine_state.borrow(); + + let decl_id = engine_state + .find_decl("run_external".as_bytes()) + .ok_or(ShellError::ExternalNotSupported(name.clone()))?; + + let command = engine_state.get_decl(decl_id); + let new_context = context.enter_scope(); + + let mut call = Call::new(); + call.positional = [name.clone()] + .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.clone(), + 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::BinaryOp(lhs, op, rhs) => { diff --git a/crates/nu-protocol/src/engine/evaluation_context.rs b/crates/nu-protocol/src/engine/evaluation_context.rs index 776a69fb1..3dd90d94d 100644 --- a/crates/nu-protocol/src/engine/evaluation_context.rs +++ b/crates/nu-protocol/src/engine/evaluation_context.rs @@ -104,6 +104,10 @@ impl Stack { }))) } + pub fn get_env_vars(&self) -> HashMap { + self.0.borrow().env_vars.clone() + } + pub fn print_stack(&self) { println!("===frame==="); println!("vars:"); diff --git a/crates/nu-protocol/src/shell_error.rs b/crates/nu-protocol/src/shell_error.rs index 678816b35..1f1879988 100644 --- a/crates/nu-protocol/src/shell_error.rs +++ b/crates/nu-protocol/src/shell_error.rs @@ -21,4 +21,5 @@ pub enum ShellError { AccessBeyondEndOfStream(Span), IncompatiblePathAccess(String, Span), CantFindColumn(Span), + ExternalCommand(String, Span), }