Custom command attributes (#14906)

# Description
Add custom command attributes.

- Attributes are placed before a command definition and start with a `@`
character.
- Attribute invocations consist of const command call. The command's
name must start with "attr ", but this prefix is not used in the
invocation.
- A command named `attr example` is invoked as an attribute as
`@example`
-   Several built-in attribute commands are provided as part of this PR
    -   `attr example`: Attaches an example to the commands help text
        ```nushell
        # Double numbers
        @example "double an int"  { 5 | double }   --result 10
        @example "double a float" { 0.5 | double } --result 1.0
        def double []: [number -> number] {
            $in * 2
        }
        ```
    -   `attr search-terms`: Adds search terms to a command
    -   ~`attr env`: Equivalent to using `def --env`~
- ~`attr wrapped`: Equivalent to using `def --wrapped`~ shelved for
later discussion
    -   several testing related attributes in `std/testing`
- If an attribute has no internal/special purpose, it's stored as
command metadata that can be obtained with `scope commands`.
- This allows having attributes like `@test` which can be used by test
runners.
-   Used the `@example` attribute for `std` examples.
-   Updated the std tests and test runner to use `@test` attributes
-   Added completions for attributes

# User-Facing Changes
Users can add examples to their own command definitions, and add other
arbitrary attributes.

# Tests + Formatting

- 🟢 toolkit fmt
- 🟢 toolkit clippy
- 🟢 toolkit test
- 🟢 toolkit test stdlib

# After Submitting
- Add documentation about the attribute syntax and built-in attributes
- `help attributes`

---------

Co-authored-by: 132ikl <132@ikl.sh>
This commit is contained in:
Bahex
2025-02-11 15:34:51 +03:00
committed by GitHub
parent a58d9b0b3a
commit 442df9e39c
57 changed files with 2028 additions and 987 deletions

View File

@ -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<Attribute>,
pub item: Box<Expression>,
}

View File

@ -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<OutDest>, Option<OutDest>) {
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

View File

@ -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(_) => {}

View File

@ -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::*;

View File

@ -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,
}

View File

@ -310,6 +310,7 @@ fn profiler_error(msg: impl Into<String>, 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(),

View File

@ -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

View File

@ -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,
}
}
}

View File

@ -27,6 +27,7 @@ pub trait Eval {
let expr_span = expr.span(&state);
match &expr.expr {
Expr::AttributeBlock(ab) => Self::eval::<D>(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)),

View File

@ -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<dyn Command> {
pub fn into_block_command(
self,
block_id: BlockId,
attributes: Vec<(String, Value)>,
examples: Vec<CustomExample>,
) -> Box<dyn Command> {
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<Value>,
}
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<CustomExample>,
}
impl Command for BlockCommand {
@ -697,4 +729,23 @@ impl Command for BlockCommand {
fn block_id(&self) -> Option<BlockId> {
Some(self.block_id)
}
fn attributes(&self) -> Vec<(String, Value)> {
self.attributes.clone()
}
fn examples(&self) -> Vec<Example> {
self.examples
.iter()
.map(CustomExample::to_example)
.collect()
}
fn search_terms(&self) -> Vec<&str> {
self.signature
.search_terms
.iter()
.map(String::as_str)
.collect()
}
}