Skip to content
This repository has been archived by the owner on Feb 8, 2022. It is now read-only.

Externals proposal #50

Merged
merged 7 commits into from
Sep 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/nu-cli/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(error.to_string())
])
}
};

// println!("DIAG");
Expand Down
6 changes: 4 additions & 2 deletions crates/nu-command/src/default_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RefCell<EngineState>> {
Expand Down Expand Up @@ -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(External));

// This is a WIP proof of concept
working_set.add_decl(Box::new(ListGitBranches));
working_set.add_decl(Box::new(Git));
Expand Down
2 changes: 2 additions & 0 deletions crates/nu-command/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod let_;
mod let_env;
mod list_git_branches;
mod ls;
mod run_external;
mod table;
mod where_;

Expand All @@ -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;
123 changes: 123 additions & 0 deletions crates/nu-command/src/run_external.rs
Original file line number Diff line number Diff line change
@@ -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
}
}
}
35 changes: 33 additions & 2 deletions crates/nu-engine/src/eval.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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<Operator, ShellError> {
match op {
Expand Down Expand Up @@ -125,7 +125,38 @@ pub fn eval_expression(
}
Expr::RowCondition(_, expr) => eval_expression(context, expr),
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::BinaryOp(lhs, op, rhs) => {
let op_span = op.span;
Expand Down
2 changes: 1 addition & 1 deletion crates/nu-protocol/src/engine/engine_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ impl EngineState {
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]
}

Expand Down
4 changes: 4 additions & 0 deletions crates/nu-protocol/src/engine/evaluation_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
println!("===frame===");
println!("vars:");
Expand Down
1 change: 1 addition & 0 deletions crates/nu-protocol/src/shell_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ pub enum ShellError {
AccessBeyondEndOfStream(Span),
IncompatiblePathAccess(String, Span),
CantFindColumn(Span),
ExternalCommand(String, Span),
}