mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 20:47:44 +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:
159
crates/nu-cmd-lang/src/core_commands/attr/example.rs
Normal file
159
crates/nu-cmd-lang/src/core_commands/attr/example.rs
Normal file
@ -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<PipelineData, ShellError> {
|
||||
let description: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||
let result: Option<Value> = call.get_flag(engine_state, stack, "result")?;
|
||||
|
||||
let example_string: Result<String, _> = 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<PipelineData, ShellError> {
|
||||
let description: Spanned<String> = call.req_const(working_set, 0)?;
|
||||
let result: Option<Value> = call.get_flag_const(working_set, "result")?;
|
||||
|
||||
let example_string: Result<String, _> = 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<Example> {
|
||||
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<String, ShellError>,
|
||||
working_set: &StateWorkingSet<'_>,
|
||||
call: &Call<'_>,
|
||||
description: Spanned<String>,
|
||||
result: Option<Value>,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
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())
|
||||
}
|
5
crates/nu-cmd-lang/src/core_commands/attr/mod.rs
Normal file
5
crates/nu-cmd-lang/src/core_commands/attr/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod example;
|
||||
mod search_terms;
|
||||
|
||||
pub use example::AttrExample;
|
||||
pub use search_terms::AttrSearchTerms;
|
57
crates/nu-cmd-lang/src/core_commands/attr/search_terms.rs
Normal file
57
crates/nu-cmd-lang/src/core_commands/attr/search_terms.rs
Normal file
@ -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<PipelineData, ShellError> {
|
||||
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<PipelineData, ShellError> {
|
||||
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<Example> {
|
||||
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,
|
||||
}]
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
Reference in New Issue
Block a user