forked from extern/nushell
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:
@ -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<PipelineData, ShellError> {
|
||||
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<PipelineData, ShellError> {
|
||||
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<PipelineData, ShellError> {
|
||||
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<PipelineData, ShellError> {
|
||||
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<PipelineData, ShellError> {
|
||||
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<PipelineData, ShellError> {
|
||||
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<PipelineData, ShellError> {
|
||||
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<PipelineData, ShellError> {
|
||||
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 = {
|
||||
|
Reference in New Issue
Block a user