diff --git a/crates/nu-cli/src/completions/attribute_completions.rs b/crates/nu-cli/src/completions/attribute_completions.rs new file mode 100644 index 0000000000..237b5bcbb3 --- /dev/null +++ b/crates/nu-cli/src/completions/attribute_completions.rs @@ -0,0 +1,97 @@ +use super::{completion_options::NuMatcher, SemanticSuggestion}; +use crate::{ + completions::{Completer, CompletionOptions}, + SuggestionKind, +}; +use nu_protocol::{ + engine::{Stack, StateWorkingSet}, + Span, +}; +use reedline::Suggestion; + +pub struct AttributeCompletion; +pub struct AttributableCompletion; + +impl Completer for AttributeCompletion { + fn fetch( + &mut self, + working_set: &StateWorkingSet, + _stack: &Stack, + _prefix: &[u8], + span: Span, + offset: usize, + _pos: usize, + options: &CompletionOptions, + ) -> Vec { + let partial = working_set.get_span_contents(span); + let mut matcher = NuMatcher::new(String::from_utf8_lossy(partial), options.clone()); + + let attr_commands = working_set.find_commands_by_predicate( + |s| { + s.strip_prefix(b"attr ") + .map(String::from_utf8_lossy) + .is_some_and(|name| matcher.matches(&name)) + }, + true, + ); + + for (name, desc, ty) in attr_commands { + let name = name.strip_prefix(b"attr ").unwrap_or(&name); + matcher.add_semantic_suggestion(SemanticSuggestion { + suggestion: Suggestion { + value: String::from_utf8_lossy(name).into_owned(), + description: desc, + style: None, + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + append_whitespace: false, + }, + kind: Some(SuggestionKind::Command(ty)), + }); + } + + matcher.results() + } +} + +impl Completer for AttributableCompletion { + fn fetch( + &mut self, + working_set: &StateWorkingSet, + _stack: &Stack, + _prefix: &[u8], + span: Span, + offset: usize, + _pos: usize, + options: &CompletionOptions, + ) -> Vec { + let partial = working_set.get_span_contents(span); + let mut matcher = NuMatcher::new(String::from_utf8_lossy(partial), options.clone()); + + for s in ["def", "extern", "export def", "export extern"] { + let decl_id = working_set + .find_decl(s.as_bytes()) + .expect("internal error, builtin declaration not found"); + let cmd = working_set.get_decl(decl_id); + matcher.add_semantic_suggestion(SemanticSuggestion { + suggestion: Suggestion { + value: cmd.name().into(), + description: Some(cmd.description().into()), + style: None, + extra: None, + span: reedline::Span { + start: span.start - offset, + end: span.end - offset, + }, + append_whitespace: false, + }, + kind: Some(SuggestionKind::Command(cmd.command_type())), + }); + } + + matcher.results() + } +} diff --git a/crates/nu-cli/src/completions/completer.rs b/crates/nu-cli/src/completions/completer.rs index 9fe5d05001..dc89df1906 100644 --- a/crates/nu-cli/src/completions/completer.rs +++ b/crates/nu-cli/src/completions/completer.rs @@ -1,7 +1,7 @@ use crate::completions::{ - CellPathCompletion, CommandCompletion, Completer, CompletionOptions, CustomCompletion, - DirectoryCompletion, DotNuCompletion, FileCompletion, FlagCompletion, OperatorCompletion, - VariableCompletion, + AttributableCompletion, AttributeCompletion, CellPathCompletion, CommandCompletion, Completer, + CompletionOptions, CustomCompletion, DirectoryCompletion, DotNuCompletion, FileCompletion, + FlagCompletion, OperatorCompletion, VariableCompletion, }; use log::debug; use nu_color_config::{color_record_to_nustyle, lookup_ansi_color_style}; @@ -63,6 +63,15 @@ fn find_pipeline_element_by_position<'a>( .map(FindMapResult::Found) .unwrap_or_default(), Expr::Var(_) => FindMapResult::Found(expr), + Expr::AttributeBlock(ab) => ab + .attributes + .iter() + .map(|attr| &attr.expr) + .chain(Some(ab.item.as_ref())) + .find_map(|expr| expr.find_map(working_set, &closure)) + .or(Some(expr)) + .map(FindMapResult::Found) + .unwrap_or_default(), _ => FindMapResult::Continue, } } @@ -297,6 +306,29 @@ impl NuCompleter { let index = pos - span.start; let prefix = ¤t_span[..index]; + if let Expr::AttributeBlock(ab) = &element_expression.expr { + let last_attr = ab.attributes.last().expect("at least one attribute"); + if let Expr::Garbage = last_attr.expr.expr { + return self.process_completion( + &mut AttributeCompletion, + &working_set, + prefix, + new_span, + fake_offset, + pos, + ); + } else { + return self.process_completion( + &mut AttributableCompletion, + &working_set, + prefix, + new_span, + fake_offset, + pos, + ); + } + } + // Flags completion if prefix.starts_with(b"-") { // Try to complete flag internally diff --git a/crates/nu-cli/src/completions/mod.rs b/crates/nu-cli/src/completions/mod.rs index 6122657b28..5f16338bc2 100644 --- a/crates/nu-cli/src/completions/mod.rs +++ b/crates/nu-cli/src/completions/mod.rs @@ -1,3 +1,4 @@ +mod attribute_completions; mod base; mod cell_path_completions; mod command_completions; @@ -12,6 +13,7 @@ mod flag_completions; mod operator_completions; mod variable_completions; +pub use attribute_completions::{AttributableCompletion, AttributeCompletion}; pub use base::{Completer, SemanticSuggestion, SuggestionKind}; pub use cell_path_completions::CellPathCompletion; pub use command_completions::CommandCompletion; diff --git a/crates/nu-cli/src/syntax_highlight.rs b/crates/nu-cli/src/syntax_highlight.rs index b8f7abe0c5..283a06f97e 100644 --- a/crates/nu-cli/src/syntax_highlight.rs +++ b/crates/nu-cli/src/syntax_highlight.rs @@ -309,6 +309,7 @@ fn find_matching_block_end_in_expr( .unwrap_or(expression.span.start); return match &expression.expr { + // TODO: Can't these be handled with an `_ => None` branch? Refactor Expr::Bool(_) => None, Expr::Int(_) => None, Expr::Float(_) => None, @@ -335,6 +336,28 @@ fn find_matching_block_end_in_expr( Expr::Nothing => None, Expr::Garbage => None, + Expr::AttributeBlock(ab) => ab + .attributes + .iter() + .find_map(|attr| { + find_matching_block_end_in_expr( + line, + working_set, + &attr.expr, + global_span_offset, + global_cursor_offset, + ) + }) + .or_else(|| { + find_matching_block_end_in_expr( + line, + working_set, + &ab.item, + global_span_offset, + global_cursor_offset, + ) + }), + Expr::Table(table) => { if expr_last == global_cursor_offset { // cursor is at table end diff --git a/crates/nu-cli/tests/completions/mod.rs b/crates/nu-cli/tests/completions/mod.rs index fdea8b521e..3e07a6e266 100644 --- a/crates/nu-cli/tests/completions/mod.rs +++ b/crates/nu-cli/tests/completions/mod.rs @@ -1213,6 +1213,44 @@ fn flag_completions() { match_suggestions(&expected, &suggestions); } +#[test] +fn attribute_completions() { + // Create a new engine + let (_, _, engine, stack) = new_engine(); + + // Instantiate a new completer + let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); + // Test completions for the 'ls' flags + let suggestions = completer.complete("@", 1); + + // Only checking for the builtins and not the std attributes + let expected: Vec = vec!["example".into(), "search-terms".into()]; + + // Match results + match_suggestions(&expected, &suggestions); +} + +#[test] +fn attributable_completions() { + // Create a new engine + let (_, _, engine, stack) = new_engine(); + + // Instantiate a new completer + let mut completer = NuCompleter::new(Arc::new(engine), Arc::new(stack)); + // Test completions for the 'ls' flags + let suggestions = completer.complete("@example; ", 10); + + let expected: Vec = vec![ + "def".into(), + "export def".into(), + "export extern".into(), + "extern".into(), + ]; + + // Match results + match_suggestions(&expected, &suggestions); +} + #[test] fn folder_with_directorycompletions() { // Create a new engine diff --git a/crates/nu-cmd-lang/src/core_commands/attr/example.rs b/crates/nu-cmd-lang/src/core_commands/attr/example.rs new file mode 100644 index 0000000000..a12f83d746 --- /dev/null +++ b/crates/nu-cmd-lang/src/core_commands/attr/example.rs @@ -0,0 +1,159 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct AttrExample; + +impl Command for AttrExample { + fn name(&self) -> &str { + "attr example" + } + + // TODO: When const closure are available, switch to using them for the `example` argument + // rather than a block. That should remove the need for `requires_ast_for_arguments` to be true + fn signature(&self) -> Signature { + Signature::build("attr example") + .input_output_types(vec![( + Type::Nothing, + Type::Record( + [ + ("description".into(), Type::String), + ("example".into(), Type::String), + ] + .into(), + ), + )]) + .allow_variants_without_examples(true) + .required( + "description", + SyntaxShape::String, + "Description of the example.", + ) + .required( + "example", + SyntaxShape::OneOf(vec![SyntaxShape::Block, SyntaxShape::String]), + "Example code snippet.", + ) + .named( + "result", + SyntaxShape::Any, + "Expected output of example.", + None, + ) + .category(Category::Core) + } + + fn description(&self) -> &str { + "Attribute for adding examples to custom commands." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let description: Spanned = call.req(engine_state, stack, 0)?; + let result: Option = call.get_flag(engine_state, stack, "result")?; + + let example_string: Result = call.req(engine_state, stack, 1); + let example_expr = call + .positional_nth(stack, 1) + .ok_or(ShellError::MissingParameter { + param_name: "example".into(), + span: call.head, + })?; + + let working_set = StateWorkingSet::new(engine_state); + + attr_example_impl( + example_expr, + example_string, + &working_set, + call, + description, + result, + ) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + let description: Spanned = call.req_const(working_set, 0)?; + let result: Option = call.get_flag_const(working_set, "result")?; + + let example_string: Result = call.req_const(working_set, 1); + let example_expr = + call.assert_ast_call()? + .positional_nth(1) + .ok_or(ShellError::MissingParameter { + param_name: "example".into(), + span: call.head, + })?; + + attr_example_impl( + example_expr, + example_string, + working_set, + call, + description, + result, + ) + } + + fn is_const(&self) -> bool { + true + } + + fn requires_ast_for_arguments(&self) -> bool { + true + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Add examples to custom command", + example: r###"# Double numbers + @example "double an int" { 2 | double } --result 4 + @example "double a float" { 0.25 | double } --result 0.5 + def double []: [number -> number] { $in * 2 }"###, + result: None, + }] + } +} + +fn attr_example_impl( + example_expr: &nu_protocol::ast::Expression, + example_string: Result, + working_set: &StateWorkingSet<'_>, + call: &Call<'_>, + description: Spanned, + result: Option, +) -> Result { + let example_content = match example_expr.as_block() { + Some(block_id) => { + let block = working_set.get_block(block_id); + let contents = + working_set.get_span_contents(block.span.expect("a block must have a span")); + let contents = contents + .strip_prefix(b"{") + .and_then(|x| x.strip_suffix(b"}")) + .unwrap_or(contents) + .trim_ascii(); + String::from_utf8_lossy(contents).into_owned() + } + None => example_string?, + }; + + let mut rec = record! { + "description" => Value::string(description.item, description.span), + "example" => Value::string(example_content, example_expr.span), + }; + if let Some(result) = result { + rec.push("result", result); + } + + Ok(Value::record(rec, call.head).into_pipeline_data()) +} diff --git a/crates/nu-cmd-lang/src/core_commands/attr/mod.rs b/crates/nu-cmd-lang/src/core_commands/attr/mod.rs new file mode 100644 index 0000000000..0d2a0e6d3c --- /dev/null +++ b/crates/nu-cmd-lang/src/core_commands/attr/mod.rs @@ -0,0 +1,5 @@ +mod example; +mod search_terms; + +pub use example::AttrExample; +pub use search_terms::AttrSearchTerms; diff --git a/crates/nu-cmd-lang/src/core_commands/attr/search_terms.rs b/crates/nu-cmd-lang/src/core_commands/attr/search_terms.rs new file mode 100644 index 0000000000..767bbead50 --- /dev/null +++ b/crates/nu-cmd-lang/src/core_commands/attr/search_terms.rs @@ -0,0 +1,57 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct AttrSearchTerms; + +impl Command for AttrSearchTerms { + fn name(&self) -> &str { + "attr search-terms" + } + + fn signature(&self) -> Signature { + Signature::build("attr search-terms") + .input_output_type(Type::Nothing, Type::list(Type::String)) + .allow_variants_without_examples(true) + .rest("terms", SyntaxShape::String, "Search terms.") + .category(Category::Core) + } + + fn description(&self) -> &str { + "Attribute for adding search terms to custom commands." + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let args = call.rest(engine_state, stack, 0)?; + Ok(Value::list(args, call.head).into_pipeline_data()) + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + let args = call.rest_const(working_set, 0)?; + Ok(Value::list(args, call.head).into_pipeline_data()) + } + + fn is_const(&self) -> bool { + true + } + + fn examples(&self) -> Vec { + vec![Example { + description: "Add search terms to a custom command", + example: r###"# Double numbers + @search-terms multiply times + def double []: [number -> number] { $in * 2 }"###, + result: None, + }] + } +} diff --git a/crates/nu-cmd-lang/src/core_commands/mod.rs b/crates/nu-cmd-lang/src/core_commands/mod.rs index 2b865641e8..7ec42ea379 100644 --- a/crates/nu-cmd-lang/src/core_commands/mod.rs +++ b/crates/nu-cmd-lang/src/core_commands/mod.rs @@ -1,4 +1,5 @@ mod alias; +mod attr; mod break_; mod collect; mod const_; @@ -35,6 +36,7 @@ mod version; mod while_; pub use alias::Alias; +pub use attr::*; pub use break_::Break; pub use collect::Collect; pub use const_::Const; diff --git a/crates/nu-cmd-lang/src/default_context.rs b/crates/nu-cmd-lang/src/default_context.rs index 43fa0ddbf5..f98f7ee39a 100644 --- a/crates/nu-cmd-lang/src/default_context.rs +++ b/crates/nu-cmd-lang/src/default_context.rs @@ -16,6 +16,8 @@ pub fn create_default_context() -> EngineState { // Core bind_command! { Alias, + AttrExample, + AttrSearchTerms, Break, Collect, Const, diff --git a/crates/nu-engine/src/compile/expression.rs b/crates/nu-engine/src/compile/expression.rs index 0badb76b45..ac5496f289 100644 --- a/crates/nu-engine/src/compile/expression.rs +++ b/crates/nu-engine/src/compile/expression.rs @@ -72,6 +72,14 @@ pub(crate) fn compile_expression( }; match &expr.expr { + Expr::AttributeBlock(ab) => compile_expression( + working_set, + builder, + &ab.item, + redirect_modes, + in_reg, + out_reg, + ), Expr::Bool(b) => lit(builder, Literal::Bool(*b)), Expr::Int(i) => lit(builder, Literal::Int(*i)), Expr::Float(f) => lit(builder, Literal::Float(*f)), diff --git a/crates/nu-engine/src/scope.rs b/crates/nu-engine/src/scope.rs index 2e21ac3242..5109612b16 100644 --- a/crates/nu-engine/src/scope.rs +++ b/crates/nu-engine/src/scope.rs @@ -106,12 +106,27 @@ impl<'e, 's> ScopeData<'e, 's> { }) .collect(); + let attributes = decl + .attributes() + .into_iter() + .map(|(name, value)| { + Value::record( + record! { + "name" => Value::string(name, span), + "value" => value, + }, + span, + ) + }) + .collect(); + let record = record! { "name" => Value::string(String::from_utf8_lossy(command_name), span), "category" => Value::string(signature.category.to_string(), span), "signatures" => self.collect_signatures(&signature, span), "description" => Value::string(decl.description(), span), "examples" => Value::list(examples, span), + "attributes" => Value::list(attributes, span), "type" => Value::string(decl.command_type().to_string(), span), "is_sub" => Value::bool(decl.is_sub(), span), "is_const" => Value::bool(decl.is_const(), span), diff --git a/crates/nu-parser/src/flatten.rs b/crates/nu-parser/src/flatten.rs index 731ab52984..46cf901f9a 100644 --- a/crates/nu-parser/src/flatten.rs +++ b/crates/nu-parser/src/flatten.rs @@ -189,6 +189,12 @@ fn flatten_expression_into( } match &expr.expr { + Expr::AttributeBlock(ab) => { + for attr in &ab.attributes { + flatten_expression_into(working_set, &attr.expr, output); + } + flatten_expression_into(working_set, &ab.item, output); + } Expr::BinaryOp(lhs, op, rhs) => { flatten_expression_into(working_set, lhs, output); flatten_expression_into(working_set, op, output); diff --git a/crates/nu-parser/src/known_external.rs b/crates/nu-parser/src/known_external.rs index 6cf61fff91..2c341152a2 100644 --- a/crates/nu-parser/src/known_external.rs +++ b/crates/nu-parser/src/known_external.rs @@ -3,11 +3,14 @@ use nu_protocol::{ ast::{self, Expr, Expression}, engine::{self, CallImpl, CommandType, UNKNOWN_SPAN_ID}, ir::{self, DataSlice}, + CustomExample, }; #[derive(Clone)] pub struct KnownExternal { pub signature: Box, + pub attributes: Vec<(String, Value)>, + pub examples: Vec, } impl Command for KnownExternal { @@ -84,6 +87,17 @@ impl Command for KnownExternal { } } } + + fn attributes(&self) -> Vec<(String, Value)> { + self.attributes.clone() + } + + fn examples(&self) -> Vec { + self.examples + .iter() + .map(CustomExample::to_example) + .collect() + } } /// Transform the args from an `ast::Call` onto a `run-external` call diff --git a/crates/nu-parser/src/lite_parser.rs b/crates/nu-parser/src/lite_parser.rs index 1a89271af2..f199375579 100644 --- a/crates/nu-parser/src/lite_parser.rs +++ b/crates/nu-parser/src/lite_parser.rs @@ -3,7 +3,7 @@ use crate::{Token, TokenContents}; use itertools::{Either, Itertools}; -use nu_protocol::{ast::RedirectionSource, ParseError, Span}; +use nu_protocol::{ast::RedirectionSource, engine::StateWorkingSet, ParseError, Span}; use std::mem; #[derive(Debug, Clone, Copy)] @@ -65,6 +65,8 @@ pub struct LiteCommand { pub comments: Vec, pub parts: Vec, pub redirection: Option, + /// one past the end indices of attributes + pub attribute_idx: Vec, } impl LiteCommand { @@ -146,6 +148,25 @@ impl LiteCommand { ) .sorted_unstable_by_key(|a| (a.start, a.end)) } + + pub fn command_parts(&self) -> &[Span] { + let command_start = self.attribute_idx.last().copied().unwrap_or(0); + &self.parts[command_start..] + } + + pub fn has_attributes(&self) -> bool { + !self.attribute_idx.is_empty() + } + + pub fn attribute_commands(&'_ self) -> impl Iterator + '_ { + std::iter::once(0) + .chain(self.attribute_idx.iter().copied()) + .tuple_windows() + .map(|(s, e)| LiteCommand { + parts: self.parts[s..e].to_owned(), + ..Default::default() + }) + } } #[derive(Debug, Clone, Default)] @@ -188,7 +209,17 @@ fn last_non_comment_token(tokens: &[Token], cur_idx: usize) -> Option (LiteBlock, Option) { +#[derive(PartialEq, Eq)] +enum Mode { + Assignment, + Attribute, + Normal, +} + +pub fn lite_parse( + tokens: &[Token], + working_set: &StateWorkingSet, +) -> (LiteBlock, Option) { if tokens.is_empty() { return (LiteBlock::default(), None); } @@ -200,220 +231,263 @@ pub fn lite_parse(tokens: &[Token]) -> (LiteBlock, Option) { let mut last_token = TokenContents::Eol; let mut file_redirection = None; let mut curr_comment: Option> = None; - let mut is_assignment = false; + let mut mode = Mode::Normal; let mut error = None; for (idx, token) in tokens.iter().enumerate() { - if is_assignment { - match &token.contents { - // Consume until semicolon or terminating EOL. Assignments absorb pipelines and - // redirections. - TokenContents::Eol => { - // Handle `[Command] [Pipe] ([Comment] | [Eol])+ [Command]` - // - // `[Eol]` branch checks if previous token is `[Pipe]` to construct pipeline - // and so `[Comment] | [Eol]` should be ignore to make it work - let actual_token = last_non_comment_token(tokens, idx); - if actual_token != Some(TokenContents::Pipe) { - is_assignment = false; - pipeline.push(&mut command); - block.push(&mut pipeline); + match mode { + Mode::Attribute => { + match &token.contents { + // Consume until semicolon or terminating EOL. Attributes can't contain pipelines or redirections. + TokenContents::Eol | TokenContents::Semicolon => { + command.attribute_idx.push(command.parts.len()); + mode = Mode::Normal; + if matches!(last_token, TokenContents::Eol | TokenContents::Semicolon) { + // Clear out the comment as we're entering a new comment + curr_comment = None; + pipeline.push(&mut command); + block.push(&mut pipeline); + } } - - if last_token == TokenContents::Eol { - // Clear out the comment as we're entering a new comment - curr_comment = None; - } - } - TokenContents::Semicolon => { - is_assignment = false; - pipeline.push(&mut command); - block.push(&mut pipeline); - } - TokenContents::Comment => { - command.comments.push(token.span); - curr_comment = None; - } - _ => command.push(token.span), - } - } else if let Some((source, append, span)) = file_redirection.take() { - match &token.contents { - TokenContents::PipePipe => { - error = error.or(Some(ParseError::ShellOrOr(token.span))); - command.push(span); - command.push(token.span); - } - TokenContents::Item => { - let target = LiteRedirectionTarget::File { - connector: span, - file: token.span, - append, - }; - if let Err(err) = command.try_add_redirection(source, target) { - error = error.or(Some(err)); - command.push(span); - command.push(token.span) - } - } - TokenContents::AssignmentOperator => { - error = error.or(Some(ParseError::Expected("redirection target", token.span))); - command.push(span); - command.push(token.span); - } - TokenContents::OutGreaterThan - | TokenContents::OutGreaterGreaterThan - | TokenContents::ErrGreaterThan - | TokenContents::ErrGreaterGreaterThan - | TokenContents::OutErrGreaterThan - | TokenContents::OutErrGreaterGreaterThan => { - error = error.or(Some(ParseError::Expected("redirection target", token.span))); - command.push(span); - command.push(token.span); - } - TokenContents::Pipe - | TokenContents::ErrGreaterPipe - | TokenContents::OutErrGreaterPipe => { - error = error.or(Some(ParseError::Expected("redirection target", token.span))); - command.push(span); - pipeline.push(&mut command); - command.pipe = Some(token.span); - } - TokenContents::Eol => { - error = error.or(Some(ParseError::Expected("redirection target", token.span))); - command.push(span); - pipeline.push(&mut command); - } - TokenContents::Semicolon => { - error = error.or(Some(ParseError::Expected("redirection target", token.span))); - command.push(span); - pipeline.push(&mut command); - block.push(&mut pipeline); - } - TokenContents::Comment => { - error = error.or(Some(ParseError::Expected("redirection target", span))); - command.push(span); - command.comments.push(token.span); - curr_comment = None; - } - } - } else { - match &token.contents { - TokenContents::PipePipe => { - error = error.or(Some(ParseError::ShellOrOr(token.span))); - command.push(token.span); - } - TokenContents::Item => { - // This is commented out to preserve old parser behavior, - // but we should probably error here. - // - // if element.redirection.is_some() { - // error = error.or(Some(ParseError::LabeledError( - // "Unexpected positional".into(), - // "cannot add positional arguments after output redirection".into(), - // token.span, - // ))); - // } - // - // For example, this is currently allowed: ^echo thing o> out.txt extra_arg - - // If we have a comment, go ahead and attach it - if let Some(curr_comment) = curr_comment.take() { - command.comments = curr_comment; - } - command.push(token.span); - } - TokenContents::AssignmentOperator => { - // When in assignment mode, we'll just consume pipes or redirections as part of - // the command. - is_assignment = true; - if let Some(curr_comment) = curr_comment.take() { - command.comments = curr_comment; - } - command.push(token.span); - } - TokenContents::OutGreaterThan => { - error = error.or(command.check_accepts_redirection(token.span)); - file_redirection = Some((RedirectionSource::Stdout, false, token.span)); - } - TokenContents::OutGreaterGreaterThan => { - error = error.or(command.check_accepts_redirection(token.span)); - file_redirection = Some((RedirectionSource::Stdout, true, token.span)); - } - TokenContents::ErrGreaterThan => { - error = error.or(command.check_accepts_redirection(token.span)); - file_redirection = Some((RedirectionSource::Stderr, false, token.span)); - } - TokenContents::ErrGreaterGreaterThan => { - error = error.or(command.check_accepts_redirection(token.span)); - file_redirection = Some((RedirectionSource::Stderr, true, token.span)); - } - TokenContents::OutErrGreaterThan => { - error = error.or(command.check_accepts_redirection(token.span)); - file_redirection = - Some((RedirectionSource::StdoutAndStderr, false, token.span)); - } - TokenContents::OutErrGreaterGreaterThan => { - error = error.or(command.check_accepts_redirection(token.span)); - file_redirection = Some((RedirectionSource::StdoutAndStderr, true, token.span)); - } - TokenContents::ErrGreaterPipe => { - let target = LiteRedirectionTarget::Pipe { - connector: token.span, - }; - if let Err(err) = command.try_add_redirection(RedirectionSource::Stderr, target) - { - error = error.or(Some(err)); - } - pipeline.push(&mut command); - command.pipe = Some(token.span); - } - TokenContents::OutErrGreaterPipe => { - let target = LiteRedirectionTarget::Pipe { - connector: token.span, - }; - if let Err(err) = - command.try_add_redirection(RedirectionSource::StdoutAndStderr, target) - { - error = error.or(Some(err)); - } - pipeline.push(&mut command); - command.pipe = Some(token.span); - } - TokenContents::Pipe => { - pipeline.push(&mut command); - command.pipe = Some(token.span); - } - TokenContents::Eol => { - // Handle `[Command] [Pipe] ([Comment] | [Eol])+ [Command]` - // - // `[Eol]` branch checks if previous token is `[Pipe]` to construct pipeline - // and so `[Comment] | [Eol]` should be ignore to make it work - let actual_token = last_non_comment_token(tokens, idx); - if actual_token != Some(TokenContents::Pipe) { - pipeline.push(&mut command); - block.push(&mut pipeline); - } - - if last_token == TokenContents::Eol { - // Clear out the comment as we're entering a new comment - curr_comment = None; - } - } - TokenContents::Semicolon => { - pipeline.push(&mut command); - block.push(&mut pipeline); - } - TokenContents::Comment => { - // Comment is beside something - if last_token != TokenContents::Eol { + TokenContents::Comment => { command.comments.push(token.span); curr_comment = None; - } else { - // Comment precedes something - if let Some(curr_comment) = &mut curr_comment { - curr_comment.push(token.span); - } else { - curr_comment = Some(vec![token.span]); + } + _ => command.push(token.span), + } + } + Mode::Assignment => { + match &token.contents { + // Consume until semicolon or terminating EOL. Assignments absorb pipelines and + // redirections. + TokenContents::Eol => { + // Handle `[Command] [Pipe] ([Comment] | [Eol])+ [Command]` + // + // `[Eol]` branch checks if previous token is `[Pipe]` to construct pipeline + // and so `[Comment] | [Eol]` should be ignore to make it work + let actual_token = last_non_comment_token(tokens, idx); + if actual_token != Some(TokenContents::Pipe) { + mode = Mode::Normal; + pipeline.push(&mut command); + block.push(&mut pipeline); + } + + if last_token == TokenContents::Eol { + // Clear out the comment as we're entering a new comment + curr_comment = None; + } + } + TokenContents::Semicolon => { + mode = Mode::Normal; + pipeline.push(&mut command); + block.push(&mut pipeline); + } + TokenContents::Comment => { + command.comments.push(token.span); + curr_comment = None; + } + _ => command.push(token.span), + } + } + Mode::Normal => { + if let Some((source, append, span)) = file_redirection.take() { + match &token.contents { + TokenContents::PipePipe => { + error = error.or(Some(ParseError::ShellOrOr(token.span))); + command.push(span); + command.push(token.span); + } + TokenContents::Item => { + let target = LiteRedirectionTarget::File { + connector: span, + file: token.span, + append, + }; + if let Err(err) = command.try_add_redirection(source, target) { + error = error.or(Some(err)); + command.push(span); + command.push(token.span) + } + } + TokenContents::AssignmentOperator => { + error = error + .or(Some(ParseError::Expected("redirection target", token.span))); + command.push(span); + command.push(token.span); + } + TokenContents::OutGreaterThan + | TokenContents::OutGreaterGreaterThan + | TokenContents::ErrGreaterThan + | TokenContents::ErrGreaterGreaterThan + | TokenContents::OutErrGreaterThan + | TokenContents::OutErrGreaterGreaterThan => { + error = error + .or(Some(ParseError::Expected("redirection target", token.span))); + command.push(span); + command.push(token.span); + } + TokenContents::Pipe + | TokenContents::ErrGreaterPipe + | TokenContents::OutErrGreaterPipe => { + error = error + .or(Some(ParseError::Expected("redirection target", token.span))); + command.push(span); + pipeline.push(&mut command); + command.pipe = Some(token.span); + } + TokenContents::Eol => { + error = error + .or(Some(ParseError::Expected("redirection target", token.span))); + command.push(span); + pipeline.push(&mut command); + } + TokenContents::Semicolon => { + error = error + .or(Some(ParseError::Expected("redirection target", token.span))); + command.push(span); + pipeline.push(&mut command); + block.push(&mut pipeline); + } + TokenContents::Comment => { + error = + error.or(Some(ParseError::Expected("redirection target", span))); + command.push(span); + command.comments.push(token.span); + curr_comment = None; + } + } + } else { + match &token.contents { + TokenContents::PipePipe => { + error = error.or(Some(ParseError::ShellOrOr(token.span))); + command.push(token.span); + } + TokenContents::Item => { + // FIXME: This is commented out to preserve old parser behavior, + // but we should probably error here. + // + // if element.redirection.is_some() { + // error = error.or(Some(ParseError::LabeledError( + // "Unexpected positional".into(), + // "cannot add positional arguments after output redirection".into(), + // token.span, + // ))); + // } + // + // For example, this is currently allowed: ^echo thing o> out.txt extra_arg + + if working_set.get_span_contents(token.span).starts_with(b"@") { + if matches!( + last_token, + TokenContents::Eol | TokenContents::Semicolon + ) { + mode = Mode::Attribute; + } + command.push(token.span); + } else { + // If we have a comment, go ahead and attach it + if let Some(curr_comment) = curr_comment.take() { + command.comments = curr_comment; + } + command.push(token.span); + } + } + TokenContents::AssignmentOperator => { + // When in assignment mode, we'll just consume pipes or redirections as part of + // the command. + mode = Mode::Assignment; + if let Some(curr_comment) = curr_comment.take() { + command.comments = curr_comment; + } + command.push(token.span); + } + TokenContents::OutGreaterThan => { + error = error.or(command.check_accepts_redirection(token.span)); + file_redirection = Some((RedirectionSource::Stdout, false, token.span)); + } + TokenContents::OutGreaterGreaterThan => { + error = error.or(command.check_accepts_redirection(token.span)); + file_redirection = Some((RedirectionSource::Stdout, true, token.span)); + } + TokenContents::ErrGreaterThan => { + error = error.or(command.check_accepts_redirection(token.span)); + file_redirection = Some((RedirectionSource::Stderr, false, token.span)); + } + TokenContents::ErrGreaterGreaterThan => { + error = error.or(command.check_accepts_redirection(token.span)); + file_redirection = Some((RedirectionSource::Stderr, true, token.span)); + } + TokenContents::OutErrGreaterThan => { + error = error.or(command.check_accepts_redirection(token.span)); + file_redirection = + Some((RedirectionSource::StdoutAndStderr, false, token.span)); + } + TokenContents::OutErrGreaterGreaterThan => { + error = error.or(command.check_accepts_redirection(token.span)); + file_redirection = + Some((RedirectionSource::StdoutAndStderr, true, token.span)); + } + TokenContents::ErrGreaterPipe => { + let target = LiteRedirectionTarget::Pipe { + connector: token.span, + }; + if let Err(err) = + command.try_add_redirection(RedirectionSource::Stderr, target) + { + error = error.or(Some(err)); + } + pipeline.push(&mut command); + command.pipe = Some(token.span); + } + TokenContents::OutErrGreaterPipe => { + let target = LiteRedirectionTarget::Pipe { + connector: token.span, + }; + if let Err(err) = command + .try_add_redirection(RedirectionSource::StdoutAndStderr, target) + { + error = error.or(Some(err)); + } + pipeline.push(&mut command); + command.pipe = Some(token.span); + } + TokenContents::Pipe => { + pipeline.push(&mut command); + command.pipe = Some(token.span); + } + TokenContents::Eol => { + // Handle `[Command] [Pipe] ([Comment] | [Eol])+ [Command]` + // + // `[Eol]` branch checks if previous token is `[Pipe]` to construct pipeline + // and so `[Comment] | [Eol]` should be ignore to make it work + let actual_token = last_non_comment_token(tokens, idx); + if actual_token != Some(TokenContents::Pipe) { + pipeline.push(&mut command); + block.push(&mut pipeline); + } + + if last_token == TokenContents::Eol { + // Clear out the comment as we're entering a new comment + curr_comment = None; + } + } + TokenContents::Semicolon => { + pipeline.push(&mut command); + block.push(&mut pipeline); + } + TokenContents::Comment => { + // Comment is beside something + if last_token != TokenContents::Eol { + command.comments.push(token.span); + curr_comment = None; + } else { + // Comment precedes something + if let Some(curr_comment) = &mut curr_comment { + curr_comment.push(token.span); + } else { + curr_comment = Some(vec![token.span]); + } + } } } } @@ -428,6 +502,10 @@ pub fn lite_parse(tokens: &[Token]) -> (LiteBlock, Option) { error = error.or(Some(ParseError::Expected("redirection target", span))); } + if let Mode::Attribute = mode { + command.attribute_idx.push(command.parts.len()); + } + pipeline.push(&mut command); block.push(&mut pipeline); diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index cbd26e1a1f..7396b4ea93 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -1,7 +1,7 @@ use crate::{ exportable::Exportable, parse_block, - parser::{parse_redirection, redirecting_builtin_error}, + parser::{parse_attribute, parse_redirection, redirecting_builtin_error}, type_check::{check_block_input_output, type_compatible}, }; use itertools::Itertools; @@ -9,14 +9,14 @@ use log::trace; use nu_path::canonicalize_with; use nu_protocol::{ ast::{ - Argument, Block, Call, Expr, Expression, ImportPattern, ImportPatternHead, + Argument, AttributeBlock, Block, Call, Expr, Expression, ImportPattern, ImportPatternHead, ImportPatternMember, Pipeline, PipelineElement, }, engine::{StateWorkingSet, DEFAULT_OVERLAY_NAME}, eval_const::eval_constant, parser_path::ParserPath, - Alias, BlockId, DeclId, Module, ModuleId, ParseError, PositionalArg, ResolvedImportPattern, - Span, Spanned, SyntaxShape, Type, Value, VarId, + Alias, BlockId, CustomExample, DeclId, FromValue, Module, ModuleId, ParseError, PositionalArg, + ResolvedImportPattern, ShellError, Span, Spanned, SyntaxShape, Type, Value, VarId, }; use std::{ collections::{HashMap, HashSet}, @@ -363,16 +363,166 @@ fn verify_not_reserved_variable_name(working_set: &mut StateWorkingSet, name: &s } } +// This is meant for parsing attribute blocks without an accompanying `def` or `extern`. It's +// necessary to provide consistent syntax highlighting, completions, and helpful errors +// +// There is no need to run the const evaluation here +pub fn parse_attribute_block( + working_set: &mut StateWorkingSet, + lite_command: &LiteCommand, +) -> Pipeline { + let attributes = lite_command + .attribute_commands() + .map(|cmd| parse_attribute(working_set, &cmd).0) + .collect::>(); + + let last_attr_span = attributes + .last() + .expect("Attribute block must contain at least one attribute") + .expr + .span; + + working_set.error(ParseError::AttributeRequiresDefinition(last_attr_span)); + let cmd_span = if lite_command.command_parts().is_empty() { + last_attr_span.past() + } else { + Span::concat(lite_command.command_parts()) + }; + let cmd_expr = garbage(working_set, cmd_span); + let ty = cmd_expr.ty.clone(); + + let attr_block_span = Span::merge_many( + attributes + .first() + .map(|x| x.expr.span) + .into_iter() + .chain(Some(cmd_span)), + ); + + Pipeline::from_vec(vec![Expression::new( + working_set, + Expr::AttributeBlock(AttributeBlock { + attributes, + item: Box::new(cmd_expr), + }), + attr_block_span, + ty, + )]) +} + // Returns also the parsed command name and ID pub fn parse_def( working_set: &mut StateWorkingSet, lite_command: &LiteCommand, module_name: Option<&[u8]>, ) -> (Pipeline, Option<(Vec, DeclId)>) { - let spans = &lite_command.parts[..]; + let mut attributes = vec![]; + let mut attribute_vals = vec![]; + + for attr_cmd in lite_command.attribute_commands() { + let (attr, name) = parse_attribute(working_set, &attr_cmd); + if let Some(name) = name { + let val = eval_constant(working_set, &attr.expr); + match val { + Ok(val) => attribute_vals.push((name, val)), + Err(e) => working_set.error(e.wrap(working_set, attr.expr.span)), + } + } + attributes.push(attr); + } + + let (expr, decl) = parse_def_inner(working_set, attribute_vals, lite_command, module_name); + + let ty = expr.ty.clone(); + + let attr_block_span = Span::merge_many( + attributes + .first() + .map(|x| x.expr.span) + .into_iter() + .chain(Some(expr.span)), + ); + + let expr = if attributes.is_empty() { + expr + } else { + Expression::new( + working_set, + Expr::AttributeBlock(AttributeBlock { + attributes, + item: Box::new(expr), + }), + attr_block_span, + ty, + ) + }; + + (Pipeline::from_vec(vec![expr]), decl) +} + +pub fn parse_extern( + working_set: &mut StateWorkingSet, + lite_command: &LiteCommand, + module_name: Option<&[u8]>, +) -> Pipeline { + let mut attributes = vec![]; + let mut attribute_vals = vec![]; + + for attr_cmd in lite_command.attribute_commands() { + let (attr, name) = parse_attribute(working_set, &attr_cmd); + if let Some(name) = name { + let val = eval_constant(working_set, &attr.expr); + match val { + Ok(val) => attribute_vals.push((name, val)), + Err(e) => working_set.error(e.wrap(working_set, attr.expr.span)), + } + } + attributes.push(attr); + } + + let expr = parse_extern_inner(working_set, attribute_vals, lite_command, module_name); + + let ty = expr.ty.clone(); + + let attr_block_span = Span::merge_many( + attributes + .first() + .map(|x| x.expr.span) + .into_iter() + .chain(Some(expr.span)), + ); + + let expr = if attributes.is_empty() { + expr + } else { + Expression::new( + working_set, + Expr::AttributeBlock(AttributeBlock { + attributes, + item: Box::new(expr), + }), + attr_block_span, + ty, + ) + }; + + Pipeline::from_vec(vec![expr]) +} + +// Returns also the parsed command name and ID +fn parse_def_inner( + working_set: &mut StateWorkingSet, + attributes: Vec<(String, Value)>, + lite_command: &LiteCommand, + module_name: Option<&[u8]>, +) -> (Expression, Option<(Vec, DeclId)>) { + let spans = lite_command.command_parts(); let (desc, extra_desc) = working_set.build_desc(&lite_command.comments); + let (attribute_vals, examples, search_terms) = + handle_special_attributes(attributes, working_set); + // Checking that the function is used with the correct name // Maybe this is not necessary but it is a sanity check // Note: "export def" is treated the same as "def" @@ -390,11 +540,11 @@ pub fn parse_def( "internal error: Wrong call name for def function".into(), Span::concat(spans), )); - return (garbage_pipeline(working_set, spans), None); + return (garbage(working_set, Span::concat(spans)), None); } if let Some(redirection) = lite_command.redirection.as_ref() { working_set.error(redirecting_builtin_error("def", redirection)); - return (garbage_pipeline(working_set, spans), None); + return (garbage(working_set, Span::concat(spans)), None); } // Parsing the spans and checking that they match the register signature @@ -406,7 +556,7 @@ pub fn parse_def( "internal error: def declaration not found".into(), Span::concat(spans), )); - return (garbage_pipeline(working_set, spans), None); + return (garbage(working_set, Span::concat(spans)), None); } Some(decl_id) => { working_set.enter_scope(); @@ -430,7 +580,7 @@ pub fn parse_def( String::from_utf8_lossy(&def_call).as_ref(), ) { working_set.error(err); - return (garbage_pipeline(working_set, spans), None); + return (garbage(working_set, Span::concat(spans)), None); } } @@ -474,17 +624,12 @@ pub fn parse_def( working_set.parse_errors.append(&mut new_errors); let Ok(is_help) = has_flag_const(working_set, &call, "help") else { - return (garbage_pipeline(working_set, spans), None); + return (garbage(working_set, Span::concat(spans)), None); }; if starting_error_count != working_set.parse_errors.len() || is_help { return ( - Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - output, - )]), + Expression::new(working_set, Expr::Call(call), call_span, output), None, ); } @@ -494,10 +639,10 @@ pub fn parse_def( }; let Ok(has_env) = has_flag_const(working_set, &call, "env") else { - return (garbage_pipeline(working_set, spans), None); + return (garbage(working_set, Span::concat(spans)), None); }; let Ok(has_wrapped) = has_flag_const(working_set, &call, "wrapped") else { - return (garbage_pipeline(working_set, spans), None); + return (garbage(working_set, Span::concat(spans)), None); }; // All positional arguments must be in the call positional vector by this point @@ -517,12 +662,7 @@ pub fn parse_def( name_expr_span, )); return ( - Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - Type::Any, - )]), + Expression::new(working_set, Expr::Call(call), call_span, Type::Any), None, ); } @@ -534,7 +674,7 @@ pub fn parse_def( "Could not get string from string expression".into(), name_expr.span, )); - return (garbage_pipeline(working_set, spans), None); + return (garbage(working_set, Span::concat(spans)), None); }; let mut result = None; @@ -567,12 +707,7 @@ pub fn parse_def( format!("...rest-like positional argument used in 'def --wrapped' supports only strings. Change the type annotation of ...{} to 'string'.", &rest.name))); return ( - Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - Type::Any, - )]), + Expression::new(working_set, Expr::Call(call), call_span, Type::Any), result, ); } @@ -581,12 +716,7 @@ pub fn parse_def( working_set.error(ParseError::MissingPositional("...rest-like positional argument".to_string(), name_expr.span, "def --wrapped must have a ...rest-like positional argument. Add '...rest: string' to the command's signature.".to_string())); return ( - Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - Type::Any, - )]), + Expression::new(working_set, Expr::Call(call), call_span, Type::Any), result, ); } @@ -602,8 +732,11 @@ pub fn parse_def( signature.description = desc; signature.extra_description = extra_desc; signature.allows_unknown_args = has_wrapped; + signature.search_terms = search_terms; - *declaration = signature.clone().into_block_command(block_id); + *declaration = signature + .clone() + .into_block_command(block_id, attribute_vals, examples); let block = working_set.get_block_mut(block_id); block.signature = signature; @@ -637,25 +770,25 @@ pub fn parse_def( working_set.merge_predecl(name.as_bytes()); ( - Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - Type::Any, - )]), + Expression::new(working_set, Expr::Call(call), call_span, Type::Any), result, ) } -pub fn parse_extern( +fn parse_extern_inner( working_set: &mut StateWorkingSet, + attributes: Vec<(String, Value)>, lite_command: &LiteCommand, module_name: Option<&[u8]>, -) -> Pipeline { - let spans = &lite_command.parts; +) -> Expression { + let spans = lite_command.command_parts(); + let concat_span = Span::concat(spans); let (description, extra_description) = working_set.build_desc(&lite_command.comments); + let (attribute_vals, examples, search_terms) = + handle_special_attributes(attributes, working_set); + // Checking that the function is used with the correct name // Maybe this is not necessary but it is a sanity check @@ -672,11 +805,11 @@ pub fn parse_extern( "internal error: Wrong call name for extern command".into(), Span::concat(spans), )); - return garbage_pipeline(working_set, spans); + return garbage(working_set, concat_span); } if let Some(redirection) = lite_command.redirection.as_ref() { working_set.error(redirecting_builtin_error("extern", redirection)); - return garbage_pipeline(working_set, spans); + return garbage(working_set, concat_span); } // Parsing the spans and checking that they match the register signature @@ -688,7 +821,7 @@ pub fn parse_extern( "internal error: def declaration not found".into(), Span::concat(spans), )); - return garbage_pipeline(working_set, spans); + return garbage(working_set, concat_span); } Some(decl_id) => { working_set.enter_scope(); @@ -702,7 +835,7 @@ pub fn parse_extern( String::from_utf8_lossy(&extern_call).as_ref(), ) { working_set.error(err); - return garbage_pipeline(working_set, spans); + return garbage(working_set, concat_span); } } @@ -736,12 +869,7 @@ pub fn parse_extern( "main".to_string(), name_expr_span, )); - return Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - Type::Any, - )]); + return Expression::new(working_set, Expr::Call(call), call_span, Type::Any); } } @@ -761,6 +889,7 @@ pub fn parse_extern( signature.name = external_name; signature.description = description; signature.extra_description = extra_description; + signature.search_terms = search_terms; signature.allows_unknown_args = true; if let Some(block_id) = body.and_then(|x| x.as_block()) { @@ -770,7 +899,11 @@ pub fn parse_extern( name_expr.span, )); } else { - *declaration = signature.clone().into_block_command(block_id); + *declaration = signature.clone().into_block_command( + block_id, + attribute_vals, + examples, + ); working_set.get_block_mut(block_id).signature = signature; } @@ -785,7 +918,11 @@ pub fn parse_extern( ); } - let decl = KnownExternal { signature }; + let decl = KnownExternal { + signature, + attributes: attribute_vals, + examples, + }; *declaration = Box::new(decl); } @@ -807,12 +944,54 @@ pub fn parse_extern( } } - Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - Type::Any, - )]) + Expression::new(working_set, Expr::Call(call), call_span, Type::Any) +} + +fn handle_special_attributes( + attributes: Vec<(String, Value)>, + working_set: &mut StateWorkingSet<'_>, +) -> (Vec<(String, Value)>, Vec, Vec) { + let mut attribute_vals = vec![]; + let mut examples = vec![]; + let mut search_terms = vec![]; + + for (name, value) in attributes { + let val_span = value.span(); + match name.as_str() { + "example" => match CustomExample::from_value(value) { + Ok(example) => examples.push(example), + Err(_) => { + let e = ShellError::GenericError { + error: "nu::shell::invalid_example".into(), + msg: "Value couldn't be converted to an example".into(), + span: Some(val_span), + help: Some("Is `attr example` shadowed?".into()), + inner: vec![], + }; + working_set.error(e.wrap(working_set, val_span)); + } + }, + "search-terms" => match >::from_value(value) { + Ok(mut terms) => { + search_terms.append(&mut terms); + } + Err(_) => { + let e = ShellError::GenericError { + error: "nu::shell::invalid_search_terms".into(), + msg: "Value couldn't be converted to search-terms".into(), + span: Some(val_span), + help: Some("Is `attr search-terms` shadowed?".into()), + inner: vec![], + }; + working_set.error(e.wrap(working_set, val_span)); + } + }, + _ => { + attribute_vals.push((name, value)); + } + } + } + (attribute_vals, examples, search_terms) } fn check_alias_name<'a>(working_set: &mut StateWorkingSet, spans: &'a [Span]) -> Option<&'a Span> { @@ -1126,8 +1305,9 @@ pub fn parse_export_in_block( ) -> Pipeline { let call_span = Span::concat(&lite_command.parts); - let full_name = if lite_command.parts.len() > 1 { - let sub = working_set.get_span_contents(lite_command.parts[1]); + let parts = lite_command.command_parts(); + let full_name = if parts.len() > 1 { + let sub = working_set.get_span_contents(parts[1]); match sub { b"alias" => "export alias", b"def" => "export def", @@ -1146,64 +1326,62 @@ pub fn parse_export_in_block( return garbage_pipeline(working_set, &lite_command.parts); } - if let Some(decl_id) = working_set.find_decl(full_name.as_bytes()) { - let starting_error_count = working_set.parse_errors.len(); - let ParsedInternalCall { call, output, .. } = parse_internal_call( - working_set, - if full_name == "export" { - lite_command.parts[0] - } else { - Span::concat(&lite_command.parts[0..2]) - }, - if full_name == "export" { - &lite_command.parts[1..] - } else { - &lite_command.parts[2..] - }, - decl_id, - ); - // don't need errors generated by parse_internal_call - // further error will be generated by detail `parse_xxx` function. - working_set.parse_errors.truncate(starting_error_count); + // No need to care for this when attributes are present, parse_attribute_block will throw the + // necessary error + if !lite_command.has_attributes() { + if let Some(decl_id) = working_set.find_decl(full_name.as_bytes()) { + let starting_error_count = working_set.parse_errors.len(); + let ParsedInternalCall { call, output, .. } = parse_internal_call( + working_set, + if full_name == "export" { + parts[0] + } else { + Span::concat(&parts[0..2]) + }, + if full_name == "export" { + &parts[1..] + } else { + &parts[2..] + }, + decl_id, + ); + // don't need errors generated by parse_internal_call + // further error will be generated by detail `parse_xxx` function. + working_set.parse_errors.truncate(starting_error_count); - let decl = working_set.get_decl(decl_id); - check_call(working_set, call_span, &decl.signature(), &call); - let Ok(is_help) = has_flag_const(working_set, &call, "help") else { + let decl = working_set.get_decl(decl_id); + check_call(working_set, call_span, &decl.signature(), &call); + let Ok(is_help) = has_flag_const(working_set, &call, "help") else { + return garbage_pipeline(working_set, &lite_command.parts); + }; + + if starting_error_count != working_set.parse_errors.len() || is_help { + return Pipeline::from_vec(vec![Expression::new( + working_set, + Expr::Call(call), + call_span, + output, + )]); + } + } else { + working_set.error(ParseError::UnknownState( + format!("internal error: '{full_name}' declaration not found",), + Span::concat(&lite_command.parts), + )); return garbage_pipeline(working_set, &lite_command.parts); }; - - if starting_error_count != working_set.parse_errors.len() || is_help { - return Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - call_span, - output, - )]); - } - } else { - working_set.error(ParseError::UnknownState( - format!("internal error: '{full_name}' declaration not found",), - Span::concat(&lite_command.parts), - )); - return garbage_pipeline(working_set, &lite_command.parts); - }; - - if full_name == "export" { - // export by itself is meaningless - working_set.error(ParseError::UnexpectedKeyword( - "export".into(), - lite_command.parts[0], - )); - return garbage_pipeline(working_set, &lite_command.parts); } match full_name { - "export alias" => parse_alias(working_set, lite_command, None), + // `parse_def` and `parse_extern` work both with and without attributes "export def" => parse_def(working_set, lite_command, None).0, + "export extern" => parse_extern(working_set, lite_command, None), + // Other definitions can't have attributes, so we handle attributes here with parse_attribute_block + _ if lite_command.has_attributes() => parse_attribute_block(working_set, lite_command), + "export alias" => parse_alias(working_set, lite_command, None), "export const" => parse_const(working_set, &lite_command.parts[1..]).0, "export use" => parse_use(working_set, lite_command, None).0, "export module" => parse_module(working_set, lite_command, None).0, - "export extern" => parse_extern(working_set, lite_command, None), _ => { working_set.error(ParseError::UnexpectedKeyword( full_name.into(), @@ -1222,7 +1400,7 @@ pub fn parse_export_in_module( module_name: &[u8], parent_module: &mut Module, ) -> (Pipeline, Vec) { - let spans = &lite_command.parts[..]; + let spans = lite_command.command_parts(); let export_span = if let Some(sp) = spans.first() { if working_set.get_span_contents(*sp) != b"export" { @@ -1259,18 +1437,15 @@ pub fn parse_export_in_module( parser_info: HashMap::new(), }); + let mut out_pipeline = None; + let exportables = if let Some(kw_span) = spans.get(1) { let kw_name = working_set.get_span_contents(*kw_span); match kw_name { + // `parse_def` and `parse_extern` work both with and without attributes b"def" => { - let lite_command = LiteCommand { - comments: lite_command.comments.clone(), - parts: spans[1..].to_vec(), - pipe: lite_command.pipe, - redirection: lite_command.redirection.clone(), - }; - let (pipeline, cmd_result) = - parse_def(working_set, &lite_command, Some(module_name)); + let (mut pipeline, cmd_result) = + parse_def(working_set, lite_command, Some(module_name)); let mut result = vec![]; @@ -1292,30 +1467,37 @@ pub fn parse_export_in_module( }; // Trying to warp the 'def' call into the 'export def' in a very clumsy way - if let Some(Expr::Call(def_call)) = pipeline.elements.first().map(|e| &e.expr.expr) - { - call.clone_from(def_call); - call.head = Span::concat(&spans[0..=1]); - call.decl_id = export_def_decl_id; - } else { + // TODO: Rather than this, handle `export def` correctly in `parse_def` + 'warp: { + match pipeline.elements.first_mut().map(|e| &mut e.expr.expr) { + Some(Expr::Call(def_call)) => { + def_call.head = Span::concat(&spans[0..=1]); + def_call.decl_id = export_def_decl_id; + break 'warp; + } + Some(Expr::AttributeBlock(ab)) => { + if let Expr::Call(def_call) = &mut ab.item.expr { + def_call.head = Span::concat(&spans[0..=1]); + def_call.decl_id = export_def_decl_id; + break 'warp; + } + } + _ => {} + }; + working_set.error(ParseError::InternalError( "unexpected output from parsing a definition".into(), Span::concat(&spans[1..]), )); - }; + } + out_pipeline = Some(pipeline); result } b"extern" => { - let lite_command = LiteCommand { - comments: lite_command.comments.clone(), - parts: spans[1..].to_vec(), - pipe: lite_command.pipe, - redirection: lite_command.redirection.clone(), - }; let extern_name = [b"export ", kw_name].concat(); - let pipeline = parse_extern(working_set, &lite_command, Some(module_name)); + let mut pipeline = parse_extern(working_set, lite_command, Some(module_name)); let export_def_decl_id = if let Some(id) = working_set.find_decl(&extern_name) { id @@ -1327,18 +1509,30 @@ pub fn parse_export_in_module( return (garbage_pipeline(working_set, spans), vec![]); }; - // Trying to warp the 'def' call into the 'export def' in a very clumsy way - if let Some(Expr::Call(def_call)) = pipeline.elements.first().map(|e| &e.expr.expr) - { - call.clone_from(def_call); - call.head = Span::concat(&spans[0..=1]); - call.decl_id = export_def_decl_id; - } else { + // Trying to warp the 'extern' call into the 'export extern' in a very clumsy way + // TODO: Rather than this, handle `export extern` correctly in `parse_extern` + 'warp: { + match pipeline.elements.first_mut().map(|e| &mut e.expr.expr) { + Some(Expr::Call(def_call)) => { + def_call.head = Span::concat(&spans[0..=1]); + def_call.decl_id = export_def_decl_id; + break 'warp; + } + Some(Expr::AttributeBlock(ab)) => { + if let Expr::Call(def_call) = &mut ab.item.expr { + def_call.head = Span::concat(&spans[0..=1]); + def_call.decl_id = export_def_decl_id; + break 'warp; + } + } + _ => {} + }; + working_set.error(ParseError::InternalError( "unexpected output from parsing a definition".into(), Span::concat(&spans[1..]), )); - }; + } let mut result = vec![]; @@ -1360,14 +1554,21 @@ pub fn parse_export_in_module( )); } + out_pipeline = Some(pipeline); result } + // Other definitions can't have attributes, so we handle attributes here with parse_attribute_block + _ if lite_command.has_attributes() => { + out_pipeline = Some(parse_attribute_block(working_set, lite_command)); + vec![] + } b"alias" => { let lite_command = LiteCommand { comments: lite_command.comments.clone(), parts: spans[1..].to_vec(), pipe: lite_command.pipe, redirection: lite_command.redirection.clone(), + attribute_idx: vec![], }; let pipeline = parse_alias(working_set, &lite_command, Some(module_name)); @@ -1425,6 +1626,7 @@ pub fn parse_export_in_module( parts: spans[1..].to_vec(), pipe: lite_command.pipe, redirection: lite_command.redirection.clone(), + attribute_idx: vec![], }; let (pipeline, exportables) = parse_use(working_set, &lite_command, Some(parent_module)); @@ -1580,15 +1782,13 @@ pub fn parse_export_in_module( vec![] }; - ( - Pipeline::from_vec(vec![Expression::new( - working_set, - Expr::Call(call), - Span::concat(spans), - Type::Any, - )]), - exportables, - ) + let out_pipeline = out_pipeline.unwrap_or(Pipeline::from_vec(vec![Expression::new( + working_set, + Expr::Call(call), + Span::concat(spans), + Type::Any, + )])); + (out_pipeline, exportables) } pub fn parse_export_env( @@ -1724,14 +1924,14 @@ pub fn parse_module_block( let module_comments = collect_first_comments(&output); - let (output, err) = lite_parse(&output); + let (output, err) = lite_parse(&output, working_set); if let Some(err) = err { working_set.error(err) } for pipeline in &output.block { if pipeline.commands.len() == 1 { - parse_def_predecl(working_set, &pipeline.commands[0].parts); + parse_def_predecl(working_set, pipeline.commands[0].command_parts()); } } @@ -1744,9 +1944,14 @@ pub fn parse_module_block( if pipeline.commands.len() == 1 { let command = &pipeline.commands[0]; - let name = working_set.get_span_contents(command.parts[0]); + let name = command + .command_parts() + .first() + .map(|s| working_set.get_span_contents(*s)) + .unwrap_or(b""); match name { + // `parse_def` and `parse_extern` work both with and without attributes b"def" => { block.pipelines.push( parse_def( @@ -1757,33 +1962,10 @@ pub fn parse_module_block( .0, ) } - b"const" => block - .pipelines - .push(parse_const(working_set, &command.parts).0), b"extern" => block .pipelines .push(parse_extern(working_set, command, None)), - b"alias" => { - block.pipelines.push(parse_alias( - working_set, - command, - None, // using aliases named as the module locally is OK - )) - } - b"use" => { - let (pipeline, _) = parse_use(working_set, command, Some(&mut module)); - - block.pipelines.push(pipeline) - } - b"module" => { - let (pipeline, _) = parse_module( - working_set, - command, - None, // using modules named as the module locally is OK - ); - - block.pipelines.push(pipeline) - } + // `parse_export_in_module` also handles attributes by itself b"export" => { let (pipe, exportables) = parse_export_in_module(working_set, command, module_name, &mut module); @@ -1864,6 +2046,34 @@ pub fn parse_module_block( block.pipelines.push(pipe) } + // Other definitions can't have attributes, so we handle attributes here with parse_attribute_block + _ if command.has_attributes() => block + .pipelines + .push(parse_attribute_block(working_set, command)), + b"const" => block + .pipelines + .push(parse_const(working_set, &command.parts).0), + b"alias" => { + block.pipelines.push(parse_alias( + working_set, + command, + None, // using aliases named as the module locally is OK + )) + } + b"use" => { + let (pipeline, _) = parse_use(working_set, command, Some(&mut module)); + + block.pipelines.push(pipeline) + } + b"module" => { + let (pipeline, _) = parse_module( + working_set, + command, + None, // using modules named as the module locally is OK + ); + + block.pipelines.push(pipeline) + } b"export-env" => { let (pipe, maybe_env_block) = parse_export_env(working_set, &command.parts); diff --git a/crates/nu-parser/src/parse_patterns.rs b/crates/nu-parser/src/parse_patterns.rs index 05193c6696..4c856a06a5 100644 --- a/crates/nu-parser/src/parse_patterns.rs +++ b/crates/nu-parser/src/parse_patterns.rs @@ -99,7 +99,7 @@ pub fn parse_list_pattern(working_set: &mut StateWorkingSet, span: Span) -> Matc working_set.error(err); } - let (output, err) = lite_parse(&output); + let (output, err) = lite_parse(&output, working_set); if let Some(err) = err { working_set.error(err); } diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index 4c90338d75..d2d3e5c6e1 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -14,8 +14,8 @@ use log::trace; use nu_engine::DIR_VAR_PARSER_INFO; use nu_protocol::{ ast::*, engine::StateWorkingSet, eval_const::eval_constant, BlockId, DeclId, DidYouMean, - FilesizeUnit, Flag, ParseError, PositionalArg, Signature, Span, Spanned, SyntaxShape, Type, - Value, VarId, ENV_VARIABLE_ID, IN_VARIABLE_ID, + FilesizeUnit, Flag, ParseError, PositionalArg, ShellError, Signature, Span, Spanned, + SyntaxShape, Type, Value, VarId, ENV_VARIABLE_ID, IN_VARIABLE_ID, }; use std::{ collections::{HashMap, HashSet}, @@ -1283,51 +1283,7 @@ pub fn parse_call(working_set: &mut StateWorkingSet, spans: &[Span], head: Span) return garbage(working_set, head); } - let mut pos = 0; - let cmd_start = pos; - let mut name_spans = vec![]; - let mut name = vec![]; - - for word_span in spans[cmd_start..].iter() { - // Find the longest group of words that could form a command - - name_spans.push(*word_span); - - let name_part = working_set.get_span_contents(*word_span); - if name.is_empty() { - name.extend(name_part); - } else { - name.push(b' '); - name.extend(name_part); - } - - pos += 1; - } - - let mut maybe_decl_id = working_set.find_decl(&name); - - while maybe_decl_id.is_none() { - // Find the longest command match - if name_spans.len() <= 1 { - // Keep the first word even if it does not match -- could be external command - break; - } - - name_spans.pop(); - pos -= 1; - - let mut name = vec![]; - for name_span in &name_spans { - let name_part = working_set.get_span_contents(*name_span); - if name.is_empty() { - name.extend(name_part); - } else { - name.push(b' '); - name.extend(name_part); - } - } - maybe_decl_id = working_set.find_decl(&name); - } + let (cmd_start, pos, _name, maybe_decl_id) = find_longest_decl(working_set, spans); if let Some(decl_id) = maybe_decl_id { // Before the internal parsing we check if there is no let or alias declarations @@ -1424,6 +1380,172 @@ pub fn parse_call(working_set: &mut StateWorkingSet, spans: &[Span], head: Span) } } +pub fn find_longest_decl( + working_set: &mut StateWorkingSet<'_>, + spans: &[Span], +) -> ( + usize, + usize, + Vec, + Option>, +) { + find_longest_decl_with_prefix(working_set, spans, b"") +} + +pub fn find_longest_decl_with_prefix( + working_set: &mut StateWorkingSet<'_>, + spans: &[Span], + prefix: &[u8], +) -> ( + usize, + usize, + Vec, + Option>, +) { + let mut pos = 0; + let cmd_start = pos; + let mut name_spans = vec![]; + let mut name = vec![]; + name.extend(prefix); + + for word_span in spans[cmd_start..].iter() { + // Find the longest group of words that could form a command + + name_spans.push(*word_span); + + let name_part = working_set.get_span_contents(*word_span); + if name.is_empty() { + name.extend(name_part); + } else { + name.push(b' '); + name.extend(name_part); + } + + pos += 1; + } + + let mut maybe_decl_id = working_set.find_decl(&name); + + while maybe_decl_id.is_none() { + // Find the longest command match + if name_spans.len() <= 1 { + // Keep the first word even if it does not match -- could be external command + break; + } + + name_spans.pop(); + pos -= 1; + + // TODO: Refactor to avoid recreating name with an inner loop. + name.clear(); + name.extend(prefix); + for name_span in &name_spans { + let name_part = working_set.get_span_contents(*name_span); + if name.is_empty() { + name.extend(name_part); + } else { + name.push(b' '); + name.extend(name_part); + } + } + maybe_decl_id = working_set.find_decl(&name); + } + (cmd_start, pos, name, maybe_decl_id) +} + +pub fn parse_attribute( + working_set: &mut StateWorkingSet, + lite_command: &LiteCommand, +) -> (Attribute, Option) { + let _ = lite_command + .parts + .first() + .filter(|s| working_set.get_span_contents(**s).starts_with(b"@")) + .expect("Attributes always start with an `@`"); + + assert!( + lite_command.attribute_idx.is_empty(), + "attributes can't have attributes" + ); + + let mut spans = lite_command.parts.clone(); + if let Some(first) = spans.first_mut() { + first.start += 1; + } + let spans = spans.as_slice(); + let attr_span = Span::concat(spans); + + let (cmd_start, cmd_end, mut name, decl_id) = + find_longest_decl_with_prefix(working_set, spans, b"attr"); + + debug_assert!(name.starts_with(b"attr ")); + let _ = name.drain(..(b"attr ".len())); + + let name_span = Span::concat(&spans[cmd_start..cmd_end]); + + let Ok(name) = String::from_utf8(name) else { + working_set.error(ParseError::NonUtf8(name_span)); + return ( + Attribute { + expr: garbage(working_set, attr_span), + }, + None, + ); + }; + + let Some(decl_id) = decl_id else { + working_set.error(ParseError::UnknownCommand(name_span)); + return ( + Attribute { + expr: garbage(working_set, attr_span), + }, + None, + ); + }; + + let decl = working_set.get_decl(decl_id); + + let parsed_call = match decl.as_alias() { + // TODO: Once `const def` is available, we should either disallow aliases as attributes OR + // allow them but rather than using the aliases' name, use the name of the aliased command + Some(alias) => match &alias.clone().wrapped_call { + Expression { + expr: Expr::ExternalCall(..), + .. + } => { + let shell_error = ShellError::NotAConstCommand { span: name_span }; + working_set.error(shell_error.wrap(working_set, attr_span)); + return ( + Attribute { + expr: garbage(working_set, Span::concat(spans)), + }, + None, + ); + } + _ => { + trace!("parsing: alias of internal call"); + parse_internal_call(working_set, name_span, &spans[cmd_end..], decl_id) + } + }, + None => { + trace!("parsing: internal call"); + parse_internal_call(working_set, name_span, &spans[cmd_end..], decl_id) + } + }; + + ( + Attribute { + expr: Expression::new( + working_set, + Expr::Call(parsed_call.call), + Span::concat(spans), + parsed_call.output, + ), + }, + Some(name), + ) +} + pub fn parse_binary(working_set: &mut StateWorkingSet, span: Span) -> Expression { trace!("parsing: binary"); let contents = working_set.get_span_contents(span); @@ -4166,7 +4288,7 @@ pub fn parse_list_expression( working_set.error(err) } - let (mut output, err) = lite_parse(&output); + let (mut output, err) = lite_parse(&output, working_set); if let Some(err) = err { working_set.error(err) } @@ -5728,11 +5850,20 @@ pub fn parse_builtin_commands( } trace!("parsing: checking for keywords"); - let name = working_set.get_span_contents(lite_command.parts[0]); + let name = lite_command + .command_parts() + .first() + .map(|s| working_set.get_span_contents(*s)) + .unwrap_or(b""); match name { + // `parse_def` and `parse_extern` work both with and without attributes b"def" => parse_def(working_set, lite_command, None).0, b"extern" => parse_extern(working_set, lite_command, None), + // `parse_export_in_block` also handles attributes by itself + b"export" => parse_export_in_block(working_set, lite_command), + // Other definitions can't have attributes, so we handle attributes here with parse_attribute_block + _ if lite_command.has_attributes() => parse_attribute_block(working_set, lite_command), b"let" => parse_let( working_set, &lite_command @@ -5761,7 +5892,6 @@ pub fn parse_builtin_commands( parse_keyword(working_set, lite_command) } b"source" | b"source-env" => parse_source(working_set, lite_command), - b"export" => parse_export_in_block(working_set, lite_command), b"hide" => parse_hide(working_set, lite_command), b"where" => parse_where(working_set, lite_command), // Only "plugin use" is a keyword @@ -6154,7 +6284,7 @@ pub fn parse_block( scoped: bool, is_subexpression: bool, ) -> Block { - let (lite_block, err) = lite_parse(tokens); + let (lite_block, err) = lite_parse(tokens, working_set); if let Some(err) = err { working_set.error(err); } @@ -6169,7 +6299,7 @@ pub fn parse_block( // that share the same block can see each other for pipeline in &lite_block.block { if pipeline.commands.len() == 1 { - parse_def_predecl(working_set, &pipeline.commands[0].parts) + parse_def_predecl(working_set, pipeline.commands[0].command_parts()) } } @@ -6354,6 +6484,9 @@ pub fn discover_captures_in_expr( output: &mut Vec<(VarId, Span)>, ) -> Result<(), ParseError> { match &expr.expr { + Expr::AttributeBlock(ab) => { + discover_captures_in_expr(working_set, &ab.item, seen, seen_blocks, output)?; + } Expr::BinaryOp(lhs, _, rhs) => { discover_captures_in_expr(working_set, lhs, seen, seen_blocks, output)?; discover_captures_in_expr(working_set, rhs, seen, seen_blocks, output)?; diff --git a/crates/nu-parser/tests/test_parser.rs b/crates/nu-parser/tests/test_parser.rs index 75d97e84a6..8d3249ade2 100644 --- a/crates/nu-parser/tests/test_parser.rs +++ b/crates/nu-parser/tests/test_parser.rs @@ -1,109 +1,12 @@ use nu_parser::*; use nu_protocol::{ ast::{Argument, Expr, Expression, ExternalArgument, PathMember, Range}, - engine::{Call, Command, EngineState, Stack, StateWorkingSet}, - Category, DeclId, ParseError, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, + engine::{Command, EngineState, Stack, StateWorkingSet}, + DeclId, ParseError, Signature, Span, SyntaxShape, Type, }; use rstest::rstest; -#[cfg(test)] -#[derive(Clone)] -pub struct Let; - -#[cfg(test)] -impl Command for Let { - fn name(&self) -> &str { - "let" - } - - fn description(&self) -> &str { - "Create a variable and give it a value." - } - - fn signature(&self) -> nu_protocol::Signature { - Signature::build("let") - .required("var_name", SyntaxShape::VarWithOptType, "variable name") - .required( - "initial_value", - SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)), - "equals sign followed by value", - ) - } - - fn run( - &self, - _engine_state: &EngineState, - _stack: &mut Stack, - _call: &Call, - _input: PipelineData, - ) -> Result { - todo!() - } -} - -#[cfg(test)] -#[derive(Clone)] -pub struct Mut; - -#[cfg(test)] -impl Command for Mut { - fn name(&self) -> &str { - "mut" - } - - fn description(&self) -> &str { - "Mock mut command." - } - - fn signature(&self) -> nu_protocol::Signature { - Signature::build("mut") - .required("var_name", SyntaxShape::VarWithOptType, "variable name") - .required( - "initial_value", - SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)), - "equals sign followed by value", - ) - } - - fn run( - &self, - _engine_state: &EngineState, - _stack: &mut Stack, - _call: &Call, - _input: PipelineData, - ) -> Result { - todo!() - } -} - -#[derive(Clone)] -pub struct ToCustom; - -impl Command for ToCustom { - fn name(&self) -> &str { - "to-custom" - } - - fn description(&self) -> &str { - "Mock converter command." - } - - fn signature(&self) -> nu_protocol::Signature { - Signature::build(self.name()) - .input_output_type(Type::Any, Type::Custom("custom".into())) - .category(Category::Custom("custom".into())) - } - - fn run( - &self, - _engine_state: &EngineState, - _stack: &mut Stack, - _call: &Call, - _input: PipelineData, - ) -> Result { - todo!() - } -} +use mock::{Alias, AttrEcho, Def, Let, Mut, ToCustom}; fn test_int( test_tag: &str, // name of sub-test @@ -758,6 +661,129 @@ pub fn parse_call_missing_req_flag() { )); } +#[test] +pub fn parse_attribute_block_check_spans() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + let source = br#" + @foo a 1 2 + @bar b 3 4 + echo baz + "#; + let block = parse(&mut working_set, None, source, true); + + // There SHOULD be errors here, we're using nonexistent commands + assert!(!working_set.parse_errors.is_empty()); + + assert_eq!(block.len(), 1); + + let pipeline = &block.pipelines[0]; + assert_eq!(pipeline.len(), 1); + let element = &pipeline.elements[0]; + assert!(element.redirection.is_none()); + + let Expr::AttributeBlock(ab) = &element.expr.expr else { + panic!("Couldn't parse attribute block"); + }; + + assert_eq!( + working_set.get_span_contents(ab.attributes[0].expr.span), + b"foo a 1 2" + ); + assert_eq!( + working_set.get_span_contents(ab.attributes[1].expr.span), + b"bar b 3 4" + ); + assert_eq!(working_set.get_span_contents(ab.item.span), b"echo baz"); +} + +#[test] +pub fn parse_attributes_check_values() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + working_set.add_decl(Box::new(Def)); + working_set.add_decl(Box::new(AttrEcho)); + + let source = br#" + @echo "hello world" + @echo 42 + def foo [] {} + "#; + let _ = parse(&mut working_set, None, source, false); + + assert!(working_set.parse_errors.is_empty()); + + let decl_id = working_set.find_decl(b"foo").unwrap(); + let cmd = working_set.get_decl(decl_id); + let attributes = cmd.attributes(); + + let (name, val) = &attributes[0]; + assert_eq!(name, "echo"); + assert_eq!(val.as_str(), Ok("hello world")); + + let (name, val) = &attributes[1]; + assert_eq!(name, "echo"); + assert_eq!(val.as_int(), Ok(42)); +} + +#[test] +pub fn parse_attributes_alias() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + working_set.add_decl(Box::new(Def)); + working_set.add_decl(Box::new(Alias)); + working_set.add_decl(Box::new(AttrEcho)); + + let source = br#" + alias "attr test" = attr echo + + @test null + def foo [] {} + "#; + let _ = parse(&mut working_set, None, source, false); + + assert!(working_set.parse_errors.is_empty()); + + let decl_id = working_set.find_decl(b"foo").unwrap(); + let cmd = working_set.get_decl(decl_id); + let attributes = cmd.attributes(); + + let (name, val) = &attributes[0]; + assert_eq!(name, "test"); + assert!(val.is_nothing()); +} + +#[test] +pub fn parse_attributes_external_alias() { + let engine_state = EngineState::new(); + let mut working_set = StateWorkingSet::new(&engine_state); + + working_set.add_decl(Box::new(Def)); + working_set.add_decl(Box::new(Alias)); + working_set.add_decl(Box::new(AttrEcho)); + + let source = br#" + alias "attr test" = ^echo + + @test null + def foo [] {} + "#; + let _ = parse(&mut working_set, None, source, false); + + assert!(!working_set.parse_errors.is_empty()); + + let ParseError::LabeledError(shell_error, parse_error, _span) = &working_set.parse_errors[0] + else { + panic!("Expected LabeledError"); + }; + + assert!(shell_error.contains("nu::shell::not_a_const_command")); + assert!(parse_error.contains("Encountered error during parse-time evaluation")); +} + fn test_external_call(input: &str, tag: &str, f: impl FnOnce(&Expression, &[ExternalArgument])) { let engine_state = EngineState::new(); let mut working_set = StateWorkingSet::new(&engine_state); @@ -1943,10 +1969,78 @@ mod range { } #[cfg(test)] -mod input_types { +mod mock { use super::*; - use nu_protocol::{ast::Argument, engine::Call, Category, PipelineData, ShellError, Type}; + use nu_engine::CallExt; + use nu_protocol::{ + engine::Call, Category, IntoPipelineData, PipelineData, ShellError, Type, Value, + }; + #[derive(Clone)] + pub struct Let; + + impl Command for Let { + fn name(&self) -> &str { + "let" + } + + fn description(&self) -> &str { + "Create a variable and give it a value." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("let") + .required("var_name", SyntaxShape::VarWithOptType, "variable name") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)), + "equals sign followed by value", + ) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + todo!() + } + } + + #[derive(Clone)] + pub struct Mut; + + impl Command for Mut { + fn name(&self) -> &str { + "mut" + } + + fn description(&self) -> &str { + "Mock mut command." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("mut") + .required("var_name", SyntaxShape::VarWithOptType, "variable name") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::MathExpression)), + "equals sign followed by value", + ) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + todo!() + } + } #[derive(Clone)] pub struct LsTest; @@ -2006,6 +2100,87 @@ mod input_types { } } + #[derive(Clone)] + pub struct Alias; + + impl Command for Alias { + fn name(&self) -> &str { + "alias" + } + + fn description(&self) -> &str { + "Mock alias command." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build("alias") + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .required("name", SyntaxShape::String, "Name of the alias.") + .required( + "initial_value", + SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::Expression)), + "Equals sign followed by value.", + ) + .category(Category::Core) + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + todo!() + } + } + + #[derive(Clone)] + pub struct AttrEcho; + + impl Command for AttrEcho { + fn name(&self) -> &str { + "attr echo" + } + + fn signature(&self) -> Signature { + Signature::build("attr echo").required( + "value", + SyntaxShape::Any, + "Value to store as an attribute", + ) + } + + fn description(&self) -> &str { + "Add an arbitrary value as an attribute to a command" + } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let value: Value = call.req(engine_state, stack, 0)?; + Ok(value.into_pipeline_data()) + } + + fn is_const(&self) -> bool { + true + } + + fn run_const( + &self, + working_set: &StateWorkingSet, + call: &Call, + _input: PipelineData, + ) -> Result { + let value: Value = call.req_const(working_set, 0)?; + Ok(value.into_pipeline_data()) + } + } + #[derive(Clone)] pub struct GroupBy; @@ -2255,6 +2430,13 @@ mod input_types { todo!() } } +} + +#[cfg(test)] +mod input_types { + use super::*; + use mock::*; + use nu_protocol::ast::Argument; fn add_declarations(engine_state: &mut EngineState) { let delta = { diff --git a/crates/nu-protocol/src/ast/attribute.rs b/crates/nu-protocol/src/ast/attribute.rs new file mode 100644 index 0000000000..85b89256ee --- /dev/null +++ b/crates/nu-protocol/src/ast/attribute.rs @@ -0,0 +1,13 @@ +use super::Expression; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Attribute { + pub expr: Expression, +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct AttributeBlock { + pub attributes: Vec, + pub item: Box, +} diff --git a/crates/nu-protocol/src/ast/expr.rs b/crates/nu-protocol/src/ast/expr.rs index 62a2f3ad37..1f9579de20 100644 --- a/crates/nu-protocol/src/ast/expr.rs +++ b/crates/nu-protocol/src/ast/expr.rs @@ -2,8 +2,8 @@ use chrono::FixedOffset; use serde::{Deserialize, Serialize}; use super::{ - Call, CellPath, Expression, ExternalArgument, FullCellPath, Keyword, MatchPattern, Operator, - Range, Table, ValueWithUnit, + AttributeBlock, Call, CellPath, Expression, ExternalArgument, FullCellPath, Keyword, + MatchPattern, Operator, Range, Table, ValueWithUnit, }; use crate::{ ast::ImportPattern, engine::StateWorkingSet, BlockId, ModuleId, OutDest, Signature, Span, VarId, @@ -12,6 +12,7 @@ use crate::{ /// An [`Expression`] AST node #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum Expr { + AttributeBlock(AttributeBlock), Bool(bool), Int(i64), Float(f64), @@ -67,6 +68,7 @@ impl Expr { working_set: &StateWorkingSet, ) -> (Option, Option) { match self { + Expr::AttributeBlock(ab) => ab.item.expr.pipe_redirection(working_set), Expr::Call(call) => working_set.get_decl(call.decl_id).pipe_redirection(), Expr::Collect(_, _) => { // A collect expression always has default redirection, it's just going to collect diff --git a/crates/nu-protocol/src/ast/expression.rs b/crates/nu-protocol/src/ast/expression.rs index e1a761ed2f..7d08f62533 100644 --- a/crates/nu-protocol/src/ast/expression.rs +++ b/crates/nu-protocol/src/ast/expression.rs @@ -104,6 +104,7 @@ impl Expression { pub fn has_in_variable(&self, working_set: &StateWorkingSet) -> bool { match &self.expr { + Expr::AttributeBlock(ab) => ab.item.has_in_variable(working_set), Expr::BinaryOp(left, _, right) => { left.has_in_variable(working_set) || right.has_in_variable(working_set) } @@ -280,6 +281,7 @@ impl Expression { self.span = new_span; } match &mut self.expr { + Expr::AttributeBlock(ab) => ab.item.replace_span(working_set, replaced, new_span), Expr::BinaryOp(left, _, right) => { left.replace_span(working_set, replaced, new_span); right.replace_span(working_set, replaced, new_span); @@ -428,6 +430,7 @@ impl Expression { pub fn replace_in_variable(&mut self, working_set: &mut StateWorkingSet, new_var_id: VarId) { match &mut self.expr { + Expr::AttributeBlock(ab) => ab.item.replace_in_variable(working_set, new_var_id), Expr::Bool(_) => {} Expr::Int(_) => {} Expr::Float(_) => {} diff --git a/crates/nu-protocol/src/ast/mod.rs b/crates/nu-protocol/src/ast/mod.rs index 0138431ec6..950c9a7c81 100644 --- a/crates/nu-protocol/src/ast/mod.rs +++ b/crates/nu-protocol/src/ast/mod.rs @@ -1,4 +1,5 @@ //! Types representing parsed Nushell code (the Abstract Syntax Tree) +mod attribute; mod block; mod call; mod cell_path; @@ -15,6 +16,7 @@ mod traverse; mod unit; mod value_with_unit; +pub use attribute::*; pub use block::*; pub use call::*; pub use cell_path::*; diff --git a/crates/nu-protocol/src/ast/traverse.rs b/crates/nu-protocol/src/ast/traverse.rs index 2c9e6dbaa1..814e8ded7b 100644 --- a/crates/nu-protocol/src/ast/traverse.rs +++ b/crates/nu-protocol/src/ast/traverse.rs @@ -177,6 +177,12 @@ impl Traverse for Expression { Expr::StringInterpolation(vec) | Expr::GlobInterpolation(vec, _) => { vec.iter().flat_map(recur).collect() } + Expr::AttributeBlock(ab) => ab + .attributes + .iter() + .flat_map(|attr| recur(&attr.expr)) + .chain(recur(&ab.item)) + .collect(), _ => Vec::new(), } @@ -233,6 +239,11 @@ impl Traverse for Expression { Expr::StringInterpolation(vec) | Expr::GlobInterpolation(vec, _) => { vec.iter().find_map(recur) } + Expr::AttributeBlock(ab) => ab + .attributes + .iter() + .find_map(|attr| recur(&attr.expr)) + .or_else(|| recur(&ab.item)), _ => None, } diff --git a/crates/nu-protocol/src/debugger/profiler.rs b/crates/nu-protocol/src/debugger/profiler.rs index 6ac71f996f..6f8df16905 100644 --- a/crates/nu-protocol/src/debugger/profiler.rs +++ b/crates/nu-protocol/src/debugger/profiler.rs @@ -310,6 +310,7 @@ fn profiler_error(msg: impl Into, span: Span) -> ShellError { fn expr_to_string(engine_state: &EngineState, expr: &Expr) -> String { match expr { + Expr::AttributeBlock(ab) => expr_to_string(engine_state, &ab.item.expr), Expr::Binary(_) => "binary".to_string(), Expr::BinaryOp(_, _, _) => "binary operation".to_string(), Expr::Block(_) => "block".to_string(), diff --git a/crates/nu-protocol/src/engine/command.rs b/crates/nu-protocol/src/engine/command.rs index be80a18bc4..d10a6210a9 100644 --- a/crates/nu-protocol/src/engine/command.rs +++ b/crates/nu-protocol/src/engine/command.rs @@ -1,5 +1,7 @@ use super::{EngineState, Stack, StateWorkingSet}; -use crate::{engine::Call, Alias, BlockId, Example, OutDest, PipelineData, ShellError, Signature}; +use crate::{ + engine::Call, Alias, BlockId, Example, OutDest, PipelineData, ShellError, Signature, Value, +}; use std::fmt::Display; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -73,6 +75,10 @@ pub trait Command: Send + Sync + CommandClone { vec![] } + fn attributes(&self) -> Vec<(String, Value)> { + vec![] + } + // Whether can run in const evaluation in the parser fn is_const(&self) -> bool { false diff --git a/crates/nu-protocol/src/errors/parse_error.rs b/crates/nu-protocol/src/errors/parse_error.rs index f7de9f4890..2f9b7b3ed4 100644 --- a/crates/nu-protocol/src/errors/parse_error.rs +++ b/crates/nu-protocol/src/errors/parse_error.rs @@ -543,6 +543,13 @@ pub enum ParseError { help("try assigning to a variable or a cell path of a variable") )] AssignmentRequiresVar(#[label("needs to be a variable")] Span), + + #[error("Attributes must be followed by a definition.")] + #[diagnostic( + code(nu::parser::attribute_requires_definition), + help("try following this line with a `def` or `extern` definition") + )] + AttributeRequiresDefinition(#[label("must be followed by a definition")] Span), } impl ParseError { @@ -634,6 +641,7 @@ impl ParseError { ParseError::ExtraTokensAfterClosingDelimiter(s) => *s, ParseError::AssignmentRequiresVar(s) => *s, ParseError::AssignmentRequiresMutableVar(s) => *s, + ParseError::AttributeRequiresDefinition(s) => *s, } } } diff --git a/crates/nu-protocol/src/eval_base.rs b/crates/nu-protocol/src/eval_base.rs index 65f4dd7da0..0e7916bd3c 100644 --- a/crates/nu-protocol/src/eval_base.rs +++ b/crates/nu-protocol/src/eval_base.rs @@ -27,6 +27,7 @@ pub trait Eval { let expr_span = expr.span(&state); match &expr.expr { + Expr::AttributeBlock(ab) => Self::eval::(state, mut_state, &ab.item), Expr::Bool(b) => Ok(Value::bool(*b, expr_span)), Expr::Int(i) => Ok(Value::int(*i, expr_span)), Expr::Float(f) => Ok(Value::float(*f, expr_span)), diff --git a/crates/nu-protocol/src/signature.rs b/crates/nu-protocol/src/signature.rs index 549e23743f..bc975848f8 100644 --- a/crates/nu-protocol/src/signature.rs +++ b/crates/nu-protocol/src/signature.rs @@ -1,10 +1,16 @@ use crate::{ engine::{Call, Command, CommandType, EngineState, Stack}, - BlockId, PipelineData, ShellError, SyntaxShape, Type, Value, VarId, + BlockId, Example, PipelineData, ShellError, SyntaxShape, Type, Value, VarId, }; +use nu_derive_value::FromValue; use serde::{Deserialize, Serialize}; use std::fmt::Write; +// Make nu_protocol available in this namespace, consumers of this crate will +// have this without such an export. +// The `FromValue` derive macro fully qualifies paths to "nu_protocol". +use crate as nu_protocol; + /// The signature definition of a named flag that either accepts a value or acts as a toggle flag #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Flag { @@ -560,10 +566,17 @@ impl Signature { } /// Combines a signature and a block into a runnable block - pub fn into_block_command(self, block_id: BlockId) -> Box { + pub fn into_block_command( + self, + block_id: BlockId, + attributes: Vec<(String, Value)>, + examples: Vec, + ) -> Box { Box::new(BlockCommand { signature: self, block_id, + attributes, + examples, }) } @@ -651,10 +664,29 @@ fn get_positional_short_name(arg: &PositionalArg, is_required: bool) -> String { } } +#[derive(Clone, FromValue)] +pub struct CustomExample { + pub example: String, + pub description: String, + pub result: Option, +} + +impl CustomExample { + pub fn to_example(&self) -> Example<'_> { + Example { + example: self.example.as_str(), + description: self.description.as_str(), + result: self.result.clone(), + } + } +} + #[derive(Clone)] struct BlockCommand { signature: Signature, block_id: BlockId, + attributes: Vec<(String, Value)>, + examples: Vec, } impl Command for BlockCommand { @@ -697,4 +729,23 @@ impl Command for BlockCommand { fn block_id(&self) -> Option { Some(self.block_id) } + + fn attributes(&self) -> Vec<(String, Value)> { + self.attributes.clone() + } + + fn examples(&self) -> Vec { + self.examples + .iter() + .map(CustomExample::to_example) + .collect() + } + + fn search_terms(&self) -> Vec<&str> { + self.signature + .search_terms + .iter() + .map(String::as_str) + .collect() + } } diff --git a/crates/nu-std/src/lib.rs b/crates/nu-std/src/lib.rs index 58dc72b12d..cf14140ca1 100644 --- a/crates/nu-std/src/lib.rs +++ b/crates/nu-std/src/lib.rs @@ -58,6 +58,11 @@ pub fn load_standard_library( ("mod.nu", "std/util", include_str!("../std/util/mod.nu")), ("mod.nu", "std/xml", include_str!("../std/xml/mod.nu")), ("mod.nu", "std/config", include_str!("../std/config/mod.nu")), + ( + "mod.nu", + "std/testing", + include_str!("../std/testing/mod.nu"), + ), ]; for (filename, std_subdir_name, content) in std_submodules.drain(..) { diff --git a/crates/nu-std/std/assert/mod.nu b/crates/nu-std/std/assert/mod.nu index 19ca65dd64..8d10e1e34b 100644 --- a/crates/nu-std/std/assert/mod.nu +++ b/crates/nu-std/std/assert/mod.nu @@ -7,32 +7,16 @@ # Universal assert command # # If the condition is not true, it generates an error. -# -# # Example -# -# ```nushell -# >_ assert (3 == 3) -# >_ assert (42 == 3) -# Error: -# × Assertion failed: -# ╭─[myscript.nu:11:1] -# 11 │ assert (3 == 3) -# 12 │ assert (42 == 3) -# · ───┬──── -# · ╰── It is not true. -# 13 │ -# ╰──── -# ``` -# -# The --error-label flag can be used if you want to create a custom assert command: -# ``` -# def "assert even" [number: int] { -# assert ($number mod 2 == 0) --error-label { -# text: $"($number) is not an even number", -# span: (metadata $number).span, -# } -# } -# ``` +@example "This assert passes" { assert (3 == 3) } +@example "This assert fails" { assert (42 == 3) } +@example "The --error-label flag can be used if you want to create a custom assert command:" { + def "assert even" [number: int] { + assert ($number mod 2 == 0) --error-label { + text: $"($number) is not an even number", + span: (metadata $number).span, + } + } +} export def main [ condition: bool, # Condition, which should be true message?: string, # Optional error message @@ -52,32 +36,16 @@ export def main [ # Negative assertion # # If the condition is not false, it generates an error. -# -# # Examples -# -# >_ assert (42 == 3) -# >_ assert (3 == 3) -# Error: -# × Assertion failed: -# ╭─[myscript.nu:11:1] -# 11 │ assert (42 == 3) -# 12 │ assert (3 == 3) -# · ───┬──── -# · ╰── It is not false. -# 13 │ -# ╰──── -# -# -# The --error-label flag can be used if you want to create a custom assert command: -# ``` -# def "assert not even" [number: int] { -# assert not ($number mod 2 == 0) --error-label { -# span: (metadata $number).span, -# text: $"($number) is an even number", -# } -# } -# ``` -# +@example "This assert passes" { assert (42 == 3) } +@example "This assert fails" { assert (3 == 3) } +@example "The --error-label flag can be used if you want to create a custom assert command:" { + def "assert not even" [number: int] { + assert not ($number mod 2 == 0) --error-label { + span: (metadata $number).span, + text: $"($number) is an even number", + } + } +} export def not [ condition: bool, # Condition, which should be false message?: string, # Optional error message @@ -95,14 +63,12 @@ export def not [ } } + # Assert that executing the code generates an error # # For more documentation see the assert command -# -# # Examples -# -# > assert error {|| missing_command} # passes -# > assert error {|| 12} # fails +@example "This assert passes" { assert error {|| missing_command} } +@example "This assert fails" { assert error {|| 12} } export def error [ code: closure, message?: string @@ -120,12 +86,9 @@ export def error [ # Assert $left == $right # # For more documentation see the assert command -# -# # Examples -# -# > assert equal 1 1 # passes -# > assert equal (0.1 + 0.2) 0.3 -# > assert equal 1 2 # fails +@example "This assert passes" { assert equal 1 1 } +@example "This assert passes" { assert equal (0.1 + 0.2) 0.3 } +@example "This assert fails" { assert equal 1 2 } export def equal [left: any, right: any, message?: string] { main ($left == $right) $message --error-label { span: { @@ -143,12 +106,9 @@ export def equal [left: any, right: any, message?: string] { # Assert $left != $right # # For more documentation see the assert command -# -# # Examples -# -# > assert not equal 1 2 # passes -# > assert not equal 1 "apple" # passes -# > assert not equal 7 7 # fails +@example "This assert passes" { assert not equal 1 2 } +@example "This assert passes" { assert not equal 1 "apple" } +@example "This assert fails" { assert not equal 7 7 } export def "not equal" [left: any, right: any, message?: string] { main ($left != $right) $message --error-label { span: { @@ -162,12 +122,9 @@ export def "not equal" [left: any, right: any, message?: string] { # Assert $left <= $right # # For more documentation see the assert command -# -# # Examples -# -# > assert less or equal 1 2 # passes -# > assert less or equal 1 1 # passes -# > assert less or equal 1 0 # fails +@example "This assert passes" { assert less or equal 1 2 } +@example "This assert passes" { assert less or equal 1 1 } +@example "This assert fails" { assert less or equal 1 0 } export def "less or equal" [left: any, right: any, message?: string] { main ($left <= $right) $message --error-label { span: { @@ -185,11 +142,8 @@ export def "less or equal" [left: any, right: any, message?: string] { # Assert $left < $right # # For more documentation see the assert command -# -# # Examples -# -# > assert less 1 2 # passes -# > assert less 1 1 # fails +@example "This assert passes" { assert less 1 2 } +@example "This assert fails" { assert less 1 1 } export def less [left: any, right: any, message?: string] { main ($left < $right) $message --error-label { span: { @@ -207,11 +161,8 @@ export def less [left: any, right: any, message?: string] { # Assert $left > $right # # For more documentation see the assert command -# -# # Examples -# -# > assert greater 2 1 # passes -# > assert greater 2 2 # fails +@example "This assert passes" { assert greater 2 1 } +@example "This assert fails" { assert greater 2 2 } export def greater [left: any, right: any, message?: string] { main ($left > $right) $message --error-label { span: { @@ -229,12 +180,9 @@ export def greater [left: any, right: any, message?: string] { # Assert $left >= $right # # For more documentation see the assert command -# -# # Examples -# -# > assert greater or equal 2 1 # passes -# > assert greater or equal 2 2 # passes -# > assert greater or equal 1 2 # fails +@example "This assert passes" { assert greater or equal 2 1 } +@example "This assert passes" { assert greater or equal 2 2 } +@example "This assert fails" { assert greater or equal 1 2 } export def "greater or equal" [left: any, right: any, message?: string] { main ($left >= $right) $message --error-label { span: { @@ -253,11 +201,8 @@ alias "core length" = length # Assert length of $left is $right # # For more documentation see the assert command -# -# # Examples -# -# > assert length [0, 0] 2 # passes -# > assert length [0] 3 # fails +@example "This assert passes" { assert length [0, 0] 2 } +@example "This assert fails" { assert length [0] 3 } export def length [left: list, right: int, message?: string] { main (($left | core length) == $right) $message --error-label { span: { @@ -277,11 +222,8 @@ alias "core str contains" = str contains # Assert that ($left | str contains $right) # # For more documentation see the assert command -# -# # Examples -# -# > assert str contains "arst" "rs" # passes -# > assert str contains "arst" "k" # fails +@example "This assert passes" { assert str contains "arst" "rs" } +@example "This assert fails" { assert str contains "arst" "k" } export def "str contains" [left: string, right: string, message?: string] { main ($left | core str contains $right) $message --error-label { span: { diff --git a/crates/nu-std/std/bench/mod.nu b/crates/nu-std/std/bench/mod.nu index 25994ded58..a32e041ac1 100644 --- a/crates/nu-std/std/bench/mod.nu +++ b/crates/nu-std/std/bench/mod.nu @@ -1,45 +1,34 @@ # run a piece of `nushell` code multiple times and measure the time of execution. # # this command returns a benchmark report of the following form: -# ``` -# record< -# mean: duration -# std: duration -# times: list -# > -# ``` # # > **Note** # > `std bench --pretty` will return a `string`. -# -# # Examples -# measure the performance of simple addition -# > std bench { 1 + 2 } -n 10 | table -e -# ╭───────┬────────────────────╮ -# │ mean │ 4µs 956ns │ -# │ std │ 4µs 831ns │ -# │ │ ╭───┬────────────╮ │ -# │ times │ │ 0 │ 19µs 402ns │ │ -# │ │ │ 1 │ 4µs 322ns │ │ -# │ │ │ 2 │ 3µs 352ns │ │ -# │ │ │ 3 │ 2µs 966ns │ │ -# │ │ │ 4 │ 3µs │ │ -# │ │ │ 5 │ 3µs 86ns │ │ -# │ │ │ 6 │ 3µs 84ns │ │ -# │ │ │ 7 │ 3µs 604ns │ │ -# │ │ │ 8 │ 3µs 98ns │ │ -# │ │ │ 9 │ 3µs 653ns │ │ -# │ │ ╰───┴────────────╯ │ -# ╰───────┴────────────────────╯ -# -# get a pretty benchmark report -# > std bench { 1 + 2 } --pretty -# 3µs 125ns +/- 2µs 408ns +@example "measure the performance of simple addition" { bench { 1 + 2 } -n 10 } --result { + mean: (4µs + 956ns) + std: (4µs + 831ns) + times: [ + (19µs + 402ns) + ( 4µs + 322ns) + ( 3µs + 352ns) + ( 2µs + 966ns) + ( 3µs ) + ( 3µs + 86ns) + ( 3µs + 84ns) + ( 3µs + 604ns) + ( 3µs + 98ns) + ( 3µs + 653ns) + ] +} +@example "get a pretty benchmark report" { bench { 1 + 2 } --pretty } --result "3µs 125ns +/- 2µs 408ns" export def main [ code: closure # the piece of `nushell` code to measure the performance of --rounds (-n): int = 50 # the number of benchmark rounds (hopefully the more rounds the less variance) --verbose (-v) # be more verbose (namely prints the progress) --pretty # shows the results in human-readable format: " +/- " +]: [ + nothing -> record> + nothing -> string ] { let times: list = ( seq 1 $rounds | each {|i| diff --git a/crates/nu-std/std/dt/mod.nu b/crates/nu-std/std/dt/mod.nu index 682fa9587c..95a2a9cb48 100644 --- a/crates/nu-std/std/dt/mod.nu +++ b/crates/nu-std/std/dt/mod.nu @@ -89,23 +89,23 @@ def borrow-second [from: record, current: record] { } # Subtract later from earlier datetime and return the unit differences as a record -# Example: -# > dt datetime-diff 2023-05-07T04:08:45+12:00 2019-05-10T09:59:12-07:00 -# ╭─────────────┬────╮ -# │ year │ 3 │ -# │ month │ 11 │ -# │ day │ 26 │ -# │ hour │ 23 │ -# │ minute │ 9 │ -# │ second │ 33 │ -# │ millisecond │ 0 │ -# │ microsecond │ 0 │ -# │ nanosecond │ 0 │ -# ╰─────────────┴────╯ +@example "Get the difference between two dates" { + dt datetime-diff 2023-05-07T04:08:45+12:00 2019-05-10T09:59:12-07:00 +} --result { + year: 3, + month: 11, + day: 26, + hour: 23, + minute: 9, + second: 33, + millisecond: 0, + microsecond: 0, + nanosecond: 0, +} export def datetime-diff [ - later: datetime, # a later datetime - earlier: datetime # earlier (starting) datetime - ] { + later: datetime, # a later datetime + earlier: datetime # earlier (starting) datetime +]: [nothing -> record] { if $earlier > $later { let start = (metadata $later).span.start let end = (metadata $earlier).span.end @@ -162,10 +162,10 @@ export def datetime-diff [ } # Convert record from datetime-diff into humanized string -# Example: -# > dt pretty-print-duration (dt datetime-diff 2023-05-07T04:08:45+12:00 2019-05-10T09:59:12+12:00) -# 3yrs 11months 27days 18hrs 9mins 33secs -export def pretty-print-duration [dur: record] { +@example "Format the difference between two dates into a human readable string" { + dt pretty-print-duration (dt datetime-diff 2023-05-07T04:08:45+12:00 2019-05-10T09:59:12+12:00) +} --result "3yrs 11months 27days 18hrs 9mins 33secs" +export def pretty-print-duration [dur: record]: [nothing -> string] { mut result = "" if $dur.year != 0 { if $dur.year > 1 { diff --git a/crates/nu-std/std/iter/mod.nu b/crates/nu-std/std/iter/mod.nu index df5e03bd7b..d96852882c 100644 --- a/crates/nu-std/std/iter/mod.nu +++ b/crates/nu-std/std/iter/mod.nu @@ -14,21 +14,12 @@ # > The closure also has to be valid for the types it receives # > These will be flagged as errors later as closure annotations # > are implemented -# -# # Example -# ``` -# use std ["assert equal" "iter find"] -# -# let haystack = ["shell", "abc", "around", "nushell", "std"] -# -# let found = ($haystack | iter find {|e| $e starts-with "a" }) -# let not_found = ($haystack | iter find {|e| $e mod 2 == 0}) -# -# assert equal $found "abc" -# assert equal $not_found null -# ``` -export def find [ # -> any | null - fn: closure # the closure used to perform the search +@example "Find an element starting with 'a'" { + ["shell", "abc", "around", "nushell", "std"] | iter find {|e| $e starts-with "a" } +} --result "abc" +@example "Try to find an element starting with 'a'" { ["shell", "abc", "around", "nushell", "std"] | iter find {|e| $e mod 2 == 0} } --result null +export def find [ + fn: closure # the closure used to perform the search ] { filter {|e| try {do $fn $e} } | try { first } } @@ -38,23 +29,14 @@ export def find [ # -> any | null # # # Invariant # > The closure has to return a bool -# -# # Example -# ```nu -# use std ["assert equal" "iter find-index"] -# -# let res = ( -# ["iter", "abc", "shell", "around", "nushell", "std"] -# | iter find-index {|x| $x starts-with 's'} -# ) -# assert equal $res 2 -# -# let is_even = {|x| $x mod 2 == 0} -# let res = ([3 5 13 91] | iter find-index $is_even) -# assert equal $res -1 -# ``` -export def find-index [ # -> int - fn: closure # the closure used to perform the search +@example "Find the index of an element starting with 's'" { + ["iter", "abc", "shell", "around", "nushell", "std"] | iter find-index {|x| $x starts-with 's'} +} --result 2 +@example "Try to find the index of an element starting with 's'" { + [3 5 13 91] | iter find-index {|x| $x mod 2 == 0} +} --result -1 +export def find-index [ + fn: closure # the closure used to perform the search ] { enumerate | find {|e| $e.item | do $fn $e.item } @@ -63,16 +45,11 @@ export def find-index [ # -> int # Returns a new list with the separator between adjacent # items of the original list -# -# # Example -# ``` -# use std ["assert equal" "iter intersperse"] -# -# let res = ([1 2 3 4] | iter intersperse 0) -# assert equal $res [1 0 2 0 3 0 4] -# ``` -export def intersperse [ # -> list - separator: any # the separator to be used +@example "Intersperse the list with `0`" { + [1 2 3 4] | iter intersperse 0 +} --result [1 0 2 0 3 0 4] +export def intersperse [ + separator: any # the separator to be used ] { reduce --fold [] {|e, acc| $acc ++ [$e, $separator] @@ -89,20 +66,12 @@ export def intersperse [ # -> list # being the list element in the current iteration and the second # the internal state. # The internal state is also provided as pipeline input. -# -# # Example -# ``` -# use std ["assert equal" "iter scan"] -# let scanned = ([1 2 3] | iter scan 0 {|x, y| $x + $y}) -# -# assert equal $scanned [0, 1, 3, 6] -# -# # use the --noinit(-n) flag to remove the initial value from -# # the final result -# let scanned = ([1 2 3] | iter scan 0 {|x, y| $x + $y} -n) -# -# assert equal $scanned [1, 3, 6] -# ``` +@example "Get a running sum of the input list" { + [1 2 3] | iter scan 0 {|x, y| $x + $y} +} --result [0, 1, 3, 6] +@example "use the `--noinit(-n)` flag to remove the initial value from the final result" { + [1 2 3] | iter scan 0 {|x, y| $x + $y} -n +} --result [1, 3, 6] export def scan [ # -> list init: any # initial value to seed the initial state fn: closure # the closure to perform the scan @@ -118,16 +87,10 @@ export def scan [ # -> list # Returns a list of values for which the supplied closure does not # return `null` or an error. It is equivalent to # `$in | each $fn | filter $fn` -# -# # Example -# ```nu -# use std ["assert equal" "iter filter-map"] -# -# let res = ([2 5 "4" 7] | iter filter-map {|e| $e ** 2}) -# -# assert equal $res [4 25 49] -# ``` -export def filter-map [ # -> list +@example "Get the squares of elements that can be squared" { + [2 5 "4" 7] | iter filter-map {|e| $e ** 2} +} --result [4, 25, 49] +export def filter-map [ fn: closure # the closure to apply to the input ] { each {|$e| @@ -143,16 +106,9 @@ export def filter-map [ # -> list } # Maps a closure to each nested structure and flattens the result -# -# # Example -# ```nu -# use std ["assert equal" "iter flat-map"] -# -# let res = ( -# [[1 2 3] [2 3 4] [5 6 7]] | iter flat-map {|e| $e | math sum} -# ) -# assert equal $res [6 9 18] -# ``` +@example "Get the sums of list elements" { + [[1 2 3] [2 3 4] [5 6 7]] | iter flat-map {|e| $e | math sum} +} --result [6, 9, 18] export def flat-map [ # -> list fn: closure # the closure to map to the nested structures ] { @@ -160,18 +116,10 @@ export def flat-map [ # -> list } # Zips two structures and applies a closure to each of the zips -# -# # Example -# ```nu -# use std ["assert equal" "iter iter zip-with"] -# -# let res = ( -# [1 2 3] | iter zip-with [2 3 4] {|a, b| $a + $b } -# ) -# -# assert equal $res [3 5 7] -# ``` -export def zip-with [ # -> list +@example "Add two lists element-wise" { + [1 2 3] | iter zip-with [2 3 4] {|a, b| $a + $b } +} --result [3, 5, 7] +export def zip-with [ # -> list other: any # the structure to zip with fn: closure # the closure to apply to the zips ] { @@ -182,20 +130,9 @@ export def zip-with [ # -> list } # Zips two lists and returns a record with the first list as headers -# -# # Example -# ```nu -# use std ["assert equal" "iter iter zip-into-record"] -# -# let res = ( -# [1 2 3] | iter zip-into-record [2 3 4] -# ) -# -# assert equal $res [ -# [1 2 3]; -# [2 3 4] -# ] -# ``` +@example "Create record from two lists" { + [1 2 3] | iter zip-into-record [2 3 4] +} --result [{1: 2, 2: 3, 3: 4}] export def zip-into-record [ # -> table other: list # the values to zip with ] { diff --git a/crates/nu-std/std/mod.nu b/crates/nu-std/std/mod.nu index 89454ee1f3..3251b8cc61 100644 --- a/crates/nu-std/std/mod.nu +++ b/crates/nu-std/std/mod.nu @@ -15,6 +15,7 @@ export module std/log export module std/math export module std/xml export module std/config +export module std/testing # Load main dirs command and all subcommands export use std/dirs main diff --git a/crates/nu-std/std/testing/mod.nu b/crates/nu-std/std/testing/mod.nu new file mode 100644 index 0000000000..576d76d80e --- /dev/null +++ b/crates/nu-std/std/testing/mod.nu @@ -0,0 +1,17 @@ +# Mark command as a test +export alias "attr test" = echo + +# Mark a test command to be ignored +export alias "attr ignore" = echo + +# Mark a command to be run before each test +export alias "attr before-each" = echo + +# Mark a command to be run once before all tests +export alias "attr before-all" = echo + +# Mark a command to be run after each test +export alias "attr after-each" = echo + +# Mark a command to be run once after all tests +export alias "attr after-all" = echo diff --git a/crates/nu-std/std/util/mod.nu b/crates/nu-std/std/util/mod.nu index 3a7e6c0dcd..5b196c0892 100644 --- a/crates/nu-std/std/util/mod.nu +++ b/crates/nu-std/std/util/mod.nu @@ -1,29 +1,15 @@ # Add the given paths to the PATH. -# -# # Example -# - adding some dummy paths to an empty PATH -# ```nushell -# >_ with-env { PATH: [] } { -# std path add "foo" -# std path add "bar" "baz" -# std path add "fooo" --append -# -# assert equal $env.PATH ["bar" "baz" "foo" "fooo"] -# -# print (std path add "returned" --ret) -# } -# ╭───┬──────────╮ -# │ 0 │ returned │ -# │ 1 │ bar │ -# │ 2 │ baz │ -# │ 3 │ foo │ -# │ 4 │ fooo │ -# ╰───┴──────────╯ -# ``` -# - adding paths based on the operating system -# ```nushell -# >_ std path add {linux: "foo", windows: "bar", darwin: "baz"} -# ``` +@example "adding some dummy paths to an empty PATH" { + with-env { PATH: [] } { + path add "foo" + path add "bar" "baz" + path add "fooo" --append + path add "returned" --ret + } +} --result [returned bar baz foo fooo] +@example "adding paths based on the operating system" { + path add {linux: "foo", windows: "bar", darwin: "baz"} +} export def --env "path add" [ --ret (-r) # return $env.PATH, useful in pipelines to avoid scoping. --append (-a) # append to $env.PATH instead of prepending to. @@ -82,11 +68,9 @@ export def ellie [] { } # repeat anything a bunch of times, yielding a list of *n* times the input -# -# # Examples -# repeat a string -# > "foo" | std repeat 3 | str join -# "foofoofoo" +@example "repeat a string" { + "foo" | std repeat 3 | str join +} --result "foofoofoo" export def repeat [ n: int # the number of repetitions, must be positive ]: any -> list { @@ -118,10 +102,9 @@ export const null_device = if $nu.os-info.name == "windows" { } # return a null device file. -# -# # Examples -# run a command and ignore it's stderr output -# > cat xxx.txt e> (null-device) +@example "run a command and ignore it's stderr output" { + cat xxx.txt e> (null-device) +} export def null-device []: nothing -> path { $null_device } diff --git a/crates/nu-std/testing.nu b/crates/nu-std/testing.nu index 2b55646df7..44d1867afe 100644 --- a/crates/nu-std/testing.nu +++ b/crates/nu-std/testing.nu @@ -14,43 +14,29 @@ def "nu-complete threads" [] { # test and test-skip annotations may be used multiple times throughout the module as the function names are stored in a list # Other annotations should only be used once within a module file # If you find yourself in need of multiple before- or after- functions it's a sign your test suite probably needs redesign -def valid-annotations [] { - { - "#[test]": "test", - "#[ignore]": "test-skip", - "#[before-each]": "before-each" - "#[before-all]": "before-all" - "#[after-each]": "after-each" - "#[after-all]": "after-all" - } +const valid_annotations = { + "test": "test", + "ignore": "test-skip", + "before-each": "before-each" + "before-all": "before-all" + "after-each": "after-each" + "after-all": "after-all" } # Returns a table containing the list of function names together with their annotations (comments above the declaration) def get-annotated [ file: path ]: nothing -> table { - let raw_file = ( - open $file - | lines - | enumerate - | flatten - ) - - $raw_file - | where item starts-with def and index > 0 - | insert annotation {|x| - $raw_file - | get ($x.index - 1) - | get item - | str trim - } - | where annotation in (valid-annotations|columns) - | reject index - | update item { - split column --collapse-empty ' ' - | get column2.0 - } - | rename function_name + ^$nu.current-exe -c $' + source `($file)` + scope commands + | select name attributes + | where attributes != [] + | to nuon + ' + | from nuon + | update attributes { get name | each {|x| $valid_annotations | get -i $x } | first } + | rename function_name annotation } # Takes table of function names and their annotations such as the one returned by get-annotated @@ -72,10 +58,6 @@ def create-test-record []: nothing -> record @@ -17,7 +18,7 @@ def before-each [] { } } -#[test] +@test def xml_xaccess [] { let sample_xml = $in.sample_xml @@ -28,7 +29,7 @@ def xml_xaccess [] { assert equal ($sample_xml | xaccess [* * * {|e| $e.attributes != {}}]) [[tag, attributes, content]; [c, {a: b}, []]] } -#[test] +@test def xml_xupdate [] { let sample_xml = $in.sample_xml @@ -37,7 +38,7 @@ def xml_xupdate [] { assert equal ($sample_xml | xupdate [* * * {|e| $e.attributes != {}}] {|x| $x | update content ['xml']}) {tag: a, attributes: {}, content: [[tag, attributes, content]; [b, {}, [[tag, attributes, content]; [c, {a: b}, [xml]]]], [c, {}, []], [d, {}, [[tag, attributes, content]; [e, {}, [[tag, attributes, content]; [null, null, z]]], [e, {}, [[tag, attributes, content]; [null, null, x]]]]]]} } -#[test] +@test def xml_xinsert [] { let sample_xml = $in.sample_xml diff --git a/crates/nuon/src/from.rs b/crates/nuon/src/from.rs index 0c94434a1a..04fec72e3f 100644 --- a/crates/nuon/src/from.rs +++ b/crates/nuon/src/from.rs @@ -118,6 +118,12 @@ fn convert_to_value( original_text: &str, ) -> Result { match expr.expr { + Expr::AttributeBlock(..) => Err(ShellError::OutsideSpannedLabeledError { + src: original_text.to_string(), + error: "Error when loading".into(), + msg: "attributes not supported in nuon".into(), + span: expr.span, + }), Expr::BinaryOp(..) => Err(ShellError::OutsideSpannedLabeledError { src: original_text.to_string(), error: "Error when loading".into(),