mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 14:36:08 +02:00
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:
13
crates/nu-protocol/src/ast/attribute.rs
Normal file
13
crates/nu-protocol/src/ast/attribute.rs
Normal 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>,
|
||||
}
|
@ -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
|
||||
|
@ -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(_) => {}
|
||||
|
@ -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::*;
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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(),
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)),
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user