mirror of
https://github.com/nushell/nushell.git
synced 2025-06-30 22:50:14 +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:
@ -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<u8>,
|
||||
Option<nu_protocol::Id<nu_protocol::marker::Decl>>,
|
||||
) {
|
||||
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<u8>,
|
||||
Option<nu_protocol::Id<nu_protocol::marker::Decl>>,
|
||||
) {
|
||||
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<String>) {
|
||||
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)?;
|
||||
|
Reference in New Issue
Block a user