From 0dc4b2b686a2c9af0a97d5aa8b0c13ef3446b1da Mon Sep 17 00:00:00 2001 From: Yehuda Katz Date: Thu, 15 Aug 2019 15:18:18 -0700 Subject: [PATCH] Add support for external escape valve (`^dir`) This commit makes it possible to force nu to treat a command as an external command by prefixing it with `^`. For example `^dir` will force `dir` to run an external command, even if `dir` is also a registered nu command. This ensures that users don't need to leave nu just because we happened to use a command they need. This commit adds a new token type for external commands, which, among other things, makes it pretty straight forward to syntax highlight external commands uniquely, and generally to treat them as special. --- src/cli.rs | 79 +++++++++++++++---------- src/errors.rs | 35 +++++++++++ src/evaluate/evaluator.rs | 11 ++++ src/object/meta.rs | 19 ++++++ src/parser/hir.rs | 25 ++++++-- src/parser/hir/baseline_parse.rs | 2 + src/parser/hir/baseline_parse_tokens.rs | 5 +- src/parser/hir/external_command.rs | 21 +++++++ src/parser/parse/parser.rs | 34 +++++++---- src/parser/parse/token_tree.rs | 20 +++++++ src/parser/parse/token_tree_builder.rs | 7 +++ src/parser/parse/tokens.rs | 2 + src/shell/helper.rs | 4 ++ tests/command_cd_tests.rs | 2 +- tests/external_tests.rs | 12 ++++ 15 files changed, 228 insertions(+), 50 deletions(-) create mode 100644 src/parser/hir/external_command.rs create mode 100644 tests/external_tests.rs diff --git a/src/cli.rs b/src/cli.rs index ae14596ce..e1776b4c3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -11,7 +11,7 @@ crate use crate::errors::ShellError; use crate::git::current_branch; use crate::object::Value; use crate::parser::registry::Signature; -use crate::parser::{hir, Pipeline, PipelineElement, TokenNode}; +use crate::parser::{hir, CallNode, Pipeline, PipelineElement, TokenNode}; use crate::prelude::*; use log::{debug, trace}; @@ -158,7 +158,6 @@ pub async fn cli() -> Result<(), Box> { command("cd", Box::new(cd::cd)), command("size", Box::new(size::size)), command("from-yaml", Box::new(from_yaml::from_yaml)), - //command("enter", Box::new(enter::enter)), command("nth", Box::new(nth::nth)), command("n", Box::new(next::next)), command("p", Box::new(prev::prev)), @@ -201,7 +200,6 @@ pub async fn cli() -> Result<(), Box> { let _ = load_plugins(&mut context); let config = Config::builder().color_mode(ColorMode::Forced).build(); - //let h = crate::shell::Helper::new(context.clone_commands()); let mut rl: Editor<_> = Editor::with_config(config); #[cfg(windows)] @@ -209,7 +207,6 @@ pub async fn cli() -> Result<(), Box> { let _ = ansi_term::enable_ansi_support(); } - //rl.set_helper(Some(h)); let _ = rl.load_history("history.txt"); let ctrl_c = Arc::new(AtomicBool::new(false)); @@ -477,11 +474,21 @@ fn classify_command( let call = command.call(); match call { + // If the command starts with `^`, treat it as an external command no matter what + call if call.head().is_external() => { + let name_span = call.head().expect_external(); + let name = name_span.slice(source); + + Ok(external_command(call, source, name.tagged(name_span))) + } + + // Otherwise, if the command is a bare word, we'll need to triage it call if call.head().is_bare() => { let head = call.head(); let name = head.source(source); match context.has_command(name) { + // if the command is in the registry, it's an internal command true => { let command = context.get_command(name); let config = command.signature(); @@ -496,37 +503,45 @@ fn classify_command( args, })) } - false => { - let arg_list_strings: Vec> = match call.children() { - //Some(args) => args.iter().map(|i| i.as_external_arg(source)).collect(), - Some(args) => args - .iter() - .filter_map(|i| match i { - TokenNode::Whitespace(_) => None, - other => Some(Tagged::from_simple_spanned_item( - other.as_external_arg(source), - other.span(), - )), - }) - .collect(), - None => vec![], - }; - Ok(ClassifiedCommand::External(ExternalCommand { - name: name.to_string(), - name_span: head.span().clone(), - args: arg_list_strings, - })) - } + // otherwise, it's an external command + false => Ok(external_command(call, source, name.tagged(head.span()))), } } - call => Err(ShellError::diagnostic( - language_reporting::Diagnostic::new( - language_reporting::Severity::Error, - "Invalid command", - ) - .with_label(language_reporting::Label::new_primary(call.head().span())), - )), + // If the command is something else (like a number or a variable), that is currently unsupported. + // We might support `$somevar` as a curried command in the future. + call => Err(ShellError::invalid_command(call.head().span())), } } + +// Classify this command as an external command, which doesn't give special meaning +// to nu syntactic constructs, and passes all arguments to the external command as +// strings. +fn external_command( + call: &Tagged, + source: &Text, + name: Tagged<&str>, +) -> ClassifiedCommand { + let arg_list_strings: Vec> = match call.children() { + Some(args) => args + .iter() + .filter_map(|i| match i { + TokenNode::Whitespace(_) => None, + other => Some(Tagged::from_simple_spanned_item( + other.as_external_arg(source), + other.span(), + )), + }) + .collect(), + None => vec![], + }; + + let (name, tag) = name.into_parts(); + + ClassifiedCommand::External(ExternalCommand { + name: name.to_string(), + name_span: tag.span, + args: arg_list_strings, + }) +} diff --git a/src/errors.rs b/src/errors.rs index 18aa39bd5..ad22a2673 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -77,6 +77,20 @@ impl ShellError { .start() } + crate fn syntax_error(problem: Tagged>) -> ShellError { + ProximateShellError::SyntaxError { + problem: problem.map(|p| p.into()), + } + .start() + } + + crate fn invalid_command(problem: impl Into) -> ShellError { + ProximateShellError::InvalidCommand { + command: problem.into(), + } + .start() + } + crate fn coerce_error( left: Tagged>, right: Tagged>, @@ -130,6 +144,10 @@ impl ShellError { ProximateShellError::String(StringError { title, .. }) => { Diagnostic::new(Severity::Error, title) } + ProximateShellError::InvalidCommand { command } => { + Diagnostic::new(Severity::Error, "Invalid command") + .with_label(Label::new_primary(command.span)) + } ProximateShellError::ArgumentError { command, error, @@ -188,6 +206,15 @@ impl ShellError { } => Diagnostic::new(Severity::Error, "Type Error") .with_label(Label::new_primary(span).with_message(expected)), + ProximateShellError::SyntaxError { + problem: + Tagged { + tag: Tag { span, .. }, + .. + }, + } => Diagnostic::new(Severity::Error, "Syntax Error") + .with_label(Label::new_primary(span).with_message("Unexpected external command")), + ProximateShellError::MissingProperty { subpath, expr } => { let subpath = subpath.into_label(); let expr = expr.into_label(); @@ -258,6 +285,12 @@ impl ShellError { #[derive(Debug, Eq, PartialEq, Clone, Ord, PartialOrd, Serialize, Deserialize)] pub enum ProximateShellError { String(StringError), + SyntaxError { + problem: Tagged, + }, + InvalidCommand { + command: Tag, + }, TypeError { expected: String, actual: Tagged>, @@ -339,7 +372,9 @@ impl std::fmt::Display for ShellError { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match &self.error { ProximateShellError::String(s) => write!(f, "{}", &s.title), + ProximateShellError::InvalidCommand { .. } => write!(f, "InvalidCommand"), ProximateShellError::TypeError { .. } => write!(f, "TypeError"), + ProximateShellError::SyntaxError { .. } => write!(f, "SyntaxError"), ProximateShellError::MissingProperty { .. } => write!(f, "MissingProperty"), ProximateShellError::ArgumentError { .. } => write!(f, "ArgumentError"), ProximateShellError::Diagnostic(_) => write!(f, ""), diff --git a/src/evaluate/evaluator.rs b/src/evaluate/evaluator.rs index 0a73667d2..b58eac220 100644 --- a/src/evaluate/evaluator.rs +++ b/src/evaluate/evaluator.rs @@ -41,6 +41,7 @@ crate fn evaluate_baseline_expr( RawExpression::Literal(literal) => Ok(evaluate_literal(expr.copy_span(*literal), source)), RawExpression::Synthetic(hir::Synthetic::String(s)) => Ok(Value::string(s).tagged_unknown()), RawExpression::Variable(var) => evaluate_reference(var, scope, source), + RawExpression::ExternalCommand(external) => evaluate_external(external, scope, source), RawExpression::Binary(binary) => { let left = evaluate_baseline_expr(binary.left(), registry, scope, source)?; let right = evaluate_baseline_expr(binary.right(), registry, scope, source)?; @@ -127,3 +128,13 @@ fn evaluate_reference( .unwrap_or_else(|| Value::nothing().simple_spanned(span))), } } + +fn evaluate_external( + external: &hir::ExternalCommand, + _scope: &Scope, + _source: &Text, +) -> Result, ShellError> { + Err(ShellError::syntax_error( + "Unexpected external command".tagged(external.name()), + )) +} diff --git a/src/object/meta.rs b/src/object/meta.rs index bd43180b6..d743e71d3 100644 --- a/src/object/meta.rs +++ b/src/object/meta.rs @@ -120,6 +120,10 @@ impl Tagged { pub fn item(&self) -> &T { &self.item } + + pub fn into_parts(self) -> (T, Tag) { + (self.item, self.tag) + } } impl From<&Tagged> for Span { @@ -178,6 +182,21 @@ pub struct Tag { pub span: Span, } +impl From for Tag { + fn from(span: Span) -> Self { + Tag { origin: None, span } + } +} + +impl From<&Span> for Tag { + fn from(span: &Span) -> Self { + Tag { + origin: None, + span: *span, + } + } +} + impl Tag { pub fn unknown_origin(span: Span) -> Tag { Tag { origin: None, span } diff --git a/src/parser/hir.rs b/src/parser/hir.rs index 27d626145..e323812d1 100644 --- a/src/parser/hir.rs +++ b/src/parser/hir.rs @@ -1,10 +1,10 @@ crate mod baseline_parse; crate mod baseline_parse_tokens; crate mod binary; +crate mod external_command; crate mod named; crate mod path; -use crate::evaluate::Scope; use crate::parser::{registry, Unit}; use crate::prelude::*; use derive_new::new; @@ -12,11 +12,14 @@ use getset::Getters; use serde::{Deserialize, Serialize}; use std::fmt; -crate use baseline_parse::{baseline_parse_single_token, baseline_parse_token_as_string}; -crate use baseline_parse_tokens::{baseline_parse_next_expr, SyntaxType, TokensIterator}; -crate use binary::Binary; -crate use named::NamedArguments; -crate use path::Path; +use crate::evaluate::Scope; + +crate use self::baseline_parse::{baseline_parse_single_token, baseline_parse_token_as_string}; +crate use self::baseline_parse_tokens::{baseline_parse_next_expr, SyntaxType, TokensIterator}; +crate use self::binary::Binary; +crate use self::external_command::ExternalCommand; +crate use self::named::NamedArguments; +crate use self::path::Path; pub fn path(head: impl Into, tail: Vec>>) -> Path { Path::new( @@ -78,6 +81,7 @@ pub enum RawExpression { Block(Vec), List(Vec), Path(Box), + ExternalCommand(ExternalCommand), #[allow(unused)] Boolean(bool), @@ -107,6 +111,7 @@ impl RawExpression { RawExpression::Block(..) => "block", RawExpression::Path(..) => "path", RawExpression::Boolean(..) => "boolean", + RawExpression::ExternalCommand(..) => "external", } } } @@ -147,6 +152,13 @@ impl Expression { ) } + crate fn external_command(inner: impl Into, outer: impl Into) -> Expression { + Tagged::from_simple_spanned_item( + RawExpression::ExternalCommand(ExternalCommand::new(inner.into())), + outer.into(), + ) + } + crate fn it_variable(inner: impl Into, outer: impl Into) -> Expression { Tagged::from_simple_spanned_item( RawExpression::Variable(Variable::It(inner.into())), @@ -163,6 +175,7 @@ impl ToDebug for Expression { RawExpression::Variable(Variable::It(_)) => write!(f, "$it"), RawExpression::Variable(Variable::Other(s)) => write!(f, "${}", s.slice(source)), RawExpression::Binary(b) => write!(f, "{}", b.debug(source)), + RawExpression::ExternalCommand(c) => write!(f, "^{}", c.name().slice(source)), RawExpression::Block(exprs) => { write!(f, "{{ ")?; diff --git a/src/parser/hir/baseline_parse.rs b/src/parser/hir/baseline_parse.rs index 8a6f82e65..681347064 100644 --- a/src/parser/hir/baseline_parse.rs +++ b/src/parser/hir/baseline_parse.rs @@ -10,6 +10,7 @@ pub fn baseline_parse_single_token(token: &Token, source: &Text) -> hir::Express hir::Expression::it_variable(span, token.span()) } RawToken::Variable(span) => hir::Expression::variable(span, token.span()), + RawToken::External(span) => hir::Expression::external_command(span, token.span()), RawToken::Bare => hir::Expression::bare(token.span()), } } @@ -19,6 +20,7 @@ pub fn baseline_parse_token_as_string(token: &Token, source: &Text) -> hir::Expr RawToken::Variable(span) if span.slice(source) == "it" => { hir::Expression::it_variable(span, token.span()) } + RawToken::External(span) => hir::Expression::external_command(span, token.span()), RawToken::Variable(span) => hir::Expression::variable(span, token.span()), RawToken::Integer(_) => hir::Expression::bare(token.span()), RawToken::Size(_, _) => hir::Expression::bare(token.span()), diff --git a/src/parser/hir/baseline_parse_tokens.rs b/src/parser/hir/baseline_parse_tokens.rs index 4f0765401..d9891b2fc 100644 --- a/src/parser/hir/baseline_parse_tokens.rs +++ b/src/parser/hir/baseline_parse_tokens.rs @@ -235,7 +235,10 @@ pub fn baseline_parse_path( TokenNode::Token(token) => match token.item() { RawToken::Bare => token.span().slice(source), RawToken::String(span) => span.slice(source), - RawToken::Integer(_) | RawToken::Size(..) | RawToken::Variable(_) => { + RawToken::Integer(_) + | RawToken::Size(..) + | RawToken::Variable(_) + | RawToken::External(_) => { return Err(ShellError::type_error( "String", token.type_name().simple_spanned(part), diff --git a/src/parser/hir/external_command.rs b/src/parser/hir/external_command.rs new file mode 100644 index 000000000..fab218f8d --- /dev/null +++ b/src/parser/hir/external_command.rs @@ -0,0 +1,21 @@ +use crate::prelude::*; +use derive_new::new; +use getset::Getters; +use serde::{Deserialize, Serialize}; +use std::fmt; + +#[derive( + Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Getters, Serialize, Deserialize, new, +)] +#[get = "crate"] +pub struct ExternalCommand { + name: Span, +} + +impl ToDebug for ExternalCommand { + fn fmt_debug(&self, f: &mut fmt::Formatter, source: &str) -> fmt::Result { + write!(f, "{}", self.name.slice(source))?; + + Ok(()) + } +} diff --git a/src/parser/parse/parser.rs b/src/parser/parse/parser.rs index 802acf75d..e54cc0f59 100644 --- a/src/parser/parse/parser.rs +++ b/src/parser/parse/parser.rs @@ -81,15 +81,6 @@ pub fn raw_integer(input: NomSpan) -> IResult> { )) }) } -/* -pub fn integer(input: NomSpan) -> IResult { - trace_step(input, "integer", move |input| { - let (input, int) = raw_integer(input)?; - - Ok((input, TokenTreeBuilder::spanned_int(*int, int.span()))) - }) -} -*/ pub fn operator(input: NomSpan) -> IResult { trace_step(input, "operator", |input| { @@ -138,6 +129,20 @@ pub fn string(input: NomSpan) -> IResult { }) } +pub fn external(input: NomSpan) -> IResult { + trace_step(input, "external", move |input| { + let start = input.offset; + let (input, _) = tag("^")(input)?; + let (input, bare) = take_while(is_bare_char)(input)?; + let end = input.offset; + + Ok(( + input, + TokenTreeBuilder::spanned_external(bare, (start, end)), + )) + }) +} + pub fn bare(input: NomSpan) -> IResult { trace_step(input, "bare", move |input| { let start = input.offset; @@ -268,7 +273,8 @@ pub fn size(input: NomSpan) -> IResult { pub fn leaf(input: NomSpan) -> IResult { trace_step(input, "leaf", move |input| { - let (input, node) = alt((size, string, operator, flag, shorthand, var, bare))(input)?; + let (input, node) = + alt((size, string, operator, flag, shorthand, var, external, bare))(input)?; Ok((input, node)) }) @@ -736,6 +742,14 @@ mod tests { } } + #[test] + fn test_external() { + assert_leaf! { + parsers [ external ] + "^ls" -> 0..3 { External(span(1, 3)) } + } + } + #[test] fn test_delimited_paren() { assert_eq!( diff --git a/src/parser/parse/token_tree.rs b/src/parser/parse/token_tree.rs index 6e7af2fa8..620eab659 100644 --- a/src/parser/parse/token_tree.rs +++ b/src/parser/parse/token_tree.rs @@ -137,6 +137,26 @@ impl TokenNode { } } + pub fn is_external(&self) -> bool { + match self { + TokenNode::Token(Tagged { + item: RawToken::External(..), + .. + }) => true, + _ => false, + } + } + + pub fn expect_external(&self) -> Span { + match self { + TokenNode::Token(Tagged { + item: RawToken::External(span), + .. + }) => *span, + _ => panic!("Only call expect_external if you checked is_external first"), + } + } + crate fn as_flag(&self, value: &str, source: &Text) -> Option> { match self { TokenNode::Flag( diff --git a/src/parser/parse/token_tree_builder.rs b/src/parser/parse/token_tree_builder.rs index cd56caea5..a30b3d74d 100644 --- a/src/parser/parse/token_tree_builder.rs +++ b/src/parser/parse/token_tree_builder.rs @@ -152,6 +152,13 @@ impl TokenTreeBuilder { )) } + pub fn spanned_external(input: impl Into, span: impl Into) -> TokenNode { + TokenNode::Token(Tagged::from_simple_spanned_item( + RawToken::External(input.into()), + span.into(), + )) + } + pub fn int(input: impl Into) -> CurriedToken { let int = input.into(); diff --git a/src/parser/parse/tokens.rs b/src/parser/parse/tokens.rs index 670edbf83..2970948a7 100644 --- a/src/parser/parse/tokens.rs +++ b/src/parser/parse/tokens.rs @@ -8,6 +8,7 @@ pub enum RawToken { Size(i64, Unit), String(Span), Variable(Span), + External(Span), Bare, } @@ -18,6 +19,7 @@ impl RawToken { RawToken::Size(..) => "Size", RawToken::String(_) => "String", RawToken::Variable(_) => "Variable", + RawToken::External(_) => "External", RawToken::Bare => "String", } } diff --git a/src/shell/helper.rs b/src/shell/helper.rs index 7b6599da1..6c161289b 100644 --- a/src/shell/helper.rs +++ b/src/shell/helper.rs @@ -135,6 +135,10 @@ fn paint_token_node(token_node: &TokenNode, line: &str) -> String { item: RawToken::Bare, .. }) => Color::Green.normal().paint(token_node.span().slice(line)), + TokenNode::Token(Tagged { + item: RawToken::External(..), + .. + }) => Color::Cyan.bold().paint(token_node.span().slice(line)), }; styled.to_string() diff --git a/tests/command_cd_tests.rs b/tests/command_cd_tests.rs index c0de7cb06..670eb5f87 100644 --- a/tests/command_cd_tests.rs +++ b/tests/command_cd_tests.rs @@ -8,4 +8,4 @@ fn cd_directory_not_found() { assert!(output.contains("dir_that_does_not_exist")); assert!(output.contains("directory not found")); -} \ No newline at end of file +} diff --git a/tests/external_tests.rs b/tests/external_tests.rs new file mode 100644 index 000000000..5db04dc27 --- /dev/null +++ b/tests/external_tests.rs @@ -0,0 +1,12 @@ +mod helpers; + +use helpers::in_directory as cwd; + +#[test] +fn external_command() { + // Echo should exist on all currently supported platforms. A better approach might + // be to generate a dummy executable as part of the tests with known semantics. + nu!(output, cwd("tests/fixtures"), "echo 1"); + + assert!(output.contains("1")); +}