forked from extern/nushell
Merge pull request #292 from nushell/external-escape-valve
Add support for external escape valve (`^dir`)
This commit is contained in:
commit
3b0c9ebf28
79
src/cli.rs
79
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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<dyn Error>> {
|
||||
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<Tagged<String>> = 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<CallNode>,
|
||||
source: &Text,
|
||||
name: Tagged<&str>,
|
||||
) -> ClassifiedCommand {
|
||||
let arg_list_strings: Vec<Tagged<String>> = 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,
|
||||
})
|
||||
}
|
||||
|
@ -77,6 +77,20 @@ impl ShellError {
|
||||
.start()
|
||||
}
|
||||
|
||||
crate fn syntax_error(problem: Tagged<impl Into<String>>) -> ShellError {
|
||||
ProximateShellError::SyntaxError {
|
||||
problem: problem.map(|p| p.into()),
|
||||
}
|
||||
.start()
|
||||
}
|
||||
|
||||
crate fn invalid_command(problem: impl Into<Tag>) -> ShellError {
|
||||
ProximateShellError::InvalidCommand {
|
||||
command: problem.into(),
|
||||
}
|
||||
.start()
|
||||
}
|
||||
|
||||
crate fn coerce_error(
|
||||
left: Tagged<impl Into<String>>,
|
||||
right: Tagged<impl Into<String>>,
|
||||
@ -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<String>,
|
||||
},
|
||||
InvalidCommand {
|
||||
command: Tag,
|
||||
},
|
||||
TypeError {
|
||||
expected: String,
|
||||
actual: Tagged<Option<String>>,
|
||||
@ -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, "<diagnostic>"),
|
||||
|
@ -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<Tagged<Value>, ShellError> {
|
||||
Err(ShellError::syntax_error(
|
||||
"Unexpected external command".tagged(external.name()),
|
||||
))
|
||||
}
|
||||
|
@ -120,6 +120,10 @@ impl<T> Tagged<T> {
|
||||
pub fn item(&self) -> &T {
|
||||
&self.item
|
||||
}
|
||||
|
||||
pub fn into_parts(self) -> (T, Tag) {
|
||||
(self.item, self.tag)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<&Tagged<T>> for Span {
|
||||
@ -178,6 +182,21 @@ pub struct Tag {
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
impl From<Span> 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 }
|
||||
|
@ -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<Expression>, tail: Vec<Tagged<impl Into<String>>>) -> Path {
|
||||
Path::new(
|
||||
@ -78,6 +81,7 @@ pub enum RawExpression {
|
||||
Block(Vec<Expression>),
|
||||
List(Vec<Expression>),
|
||||
Path(Box<Path>),
|
||||
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<Span>, outer: impl Into<Span>) -> Expression {
|
||||
Tagged::from_simple_spanned_item(
|
||||
RawExpression::ExternalCommand(ExternalCommand::new(inner.into())),
|
||||
outer.into(),
|
||||
)
|
||||
}
|
||||
|
||||
crate fn it_variable(inner: impl Into<Span>, outer: impl Into<Span>) -> 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, "{{ ")?;
|
||||
|
||||
|
@ -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()),
|
||||
|
@ -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),
|
||||
|
21
src/parser/hir/external_command.rs
Normal file
21
src/parser/hir/external_command.rs
Normal file
@ -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(())
|
||||
}
|
||||
}
|
@ -81,15 +81,6 @@ pub fn raw_integer(input: NomSpan) -> IResult<NomSpan, Tagged<i64>> {
|
||||
))
|
||||
})
|
||||
}
|
||||
/*
|
||||
pub fn integer(input: NomSpan) -> IResult<NomSpan, TokenNode> {
|
||||
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<NomSpan, TokenNode> {
|
||||
trace_step(input, "operator", |input| {
|
||||
@ -138,6 +129,20 @@ pub fn string(input: NomSpan) -> IResult<NomSpan, TokenNode> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn external(input: NomSpan) -> IResult<NomSpan, TokenNode> {
|
||||
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<NomSpan, TokenNode> {
|
||||
trace_step(input, "bare", move |input| {
|
||||
let start = input.offset;
|
||||
@ -268,7 +273,8 @@ pub fn size(input: NomSpan) -> IResult<NomSpan, TokenNode> {
|
||||
|
||||
pub fn leaf(input: NomSpan) -> IResult<NomSpan, TokenNode> {
|
||||
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!(
|
||||
|
@ -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<Tagged<Flag>> {
|
||||
match self {
|
||||
TokenNode::Flag(
|
||||
|
@ -152,6 +152,13 @@ impl TokenTreeBuilder {
|
||||
))
|
||||
}
|
||||
|
||||
pub fn spanned_external(input: impl Into<Span>, span: impl Into<Span>) -> TokenNode {
|
||||
TokenNode::Token(Tagged::from_simple_spanned_item(
|
||||
RawToken::External(input.into()),
|
||||
span.into(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn int(input: impl Into<i64>) -> CurriedToken {
|
||||
let int = input.into();
|
||||
|
||||
|
@ -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",
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -8,4 +8,4 @@ fn cd_directory_not_found() {
|
||||
|
||||
assert!(output.contains("dir_that_does_not_exist"));
|
||||
assert!(output.contains("directory not found"));
|
||||
}
|
||||
}
|
||||
|
12
tests/external_tests.rs
Normal file
12
tests/external_tests.rs
Normal file
@ -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"));
|
||||
}
|
Loading…
Reference in New Issue
Block a user