mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 16:33:37 +01:00
c747ec75c9
# Description When implementing a `Command`, one must also import all the types present in the function signatures for `Command`. This makes it so that we often import the same set of types in each command implementation file. E.g., something like this: ```rust use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; use nu_protocol::{ record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, PipelineData, ShellError, Signature, Span, Type, Value, }; ``` This PR adds the `nu_engine::command_prelude` module which contains the necessary and commonly used types to implement a `Command`: ```rust // command_prelude.rs pub use crate::CallExt; pub use nu_protocol::{ ast::{Call, CellPath}, engine::{Command, EngineState, Stack}, record, Category, Example, IntoInterruptiblePipelineData, IntoPipelineData, IntoSpanned, PipelineData, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; ``` This should reduce the boilerplate needed to implement a command and also gives us a place to track the breadth of the `Command` API. I tried to be conservative with what went into the prelude modules, since it might be hard/annoying to remove items from the prelude in the future. Let me know if something should be included or excluded.
305 lines
11 KiB
Rust
305 lines
11 KiB
Rust
use crate::{lex::lex_signature, parser::parse_value, trim_quotes, TokenContents};
|
|
use nu_protocol::{engine::StateWorkingSet, ParseError, Span, SyntaxShape, Type};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum ShapeDescriptorUse {
|
|
/// Used in an argument position allowing the addition of custom completion
|
|
Argument,
|
|
/// Used to define the type of a variable or input/output types
|
|
Type,
|
|
}
|
|
|
|
/// equivalent to [`parse_shape_name`] with [`ShapeDescriptorUse::Type`] converting the
|
|
/// [`SyntaxShape`] to its [`Type`]
|
|
pub fn parse_type(working_set: &mut StateWorkingSet, bytes: &[u8], span: Span) -> Type {
|
|
parse_shape_name(working_set, bytes, span, ShapeDescriptorUse::Type).to_type()
|
|
}
|
|
|
|
/// Parse the literals of [`Type`]-like [`SyntaxShape`]s including inner types.
|
|
/// Also handles the specification of custom completions with `type@completer`.
|
|
///
|
|
/// Restrict the parsing with `use_loc`
|
|
/// Used in:
|
|
/// - [`ShapeDescriptorUse::Argument`]
|
|
/// - `: ` argument type (+completer) positions in signatures
|
|
/// - [`ShapeDescriptorUse::Type`]
|
|
/// - `type->type` input/output type pairs
|
|
/// - `let name: type` variable type infos
|
|
///
|
|
/// NOTE: Does not provide a mapping to every [`SyntaxShape`]
|
|
pub fn parse_shape_name(
|
|
working_set: &mut StateWorkingSet,
|
|
bytes: &[u8],
|
|
span: Span,
|
|
use_loc: ShapeDescriptorUse,
|
|
) -> SyntaxShape {
|
|
let result = match bytes {
|
|
b"any" => SyntaxShape::Any,
|
|
b"binary" => SyntaxShape::Binary,
|
|
b"block" => {
|
|
working_set.error(ParseError::LabeledErrorWithHelp {
|
|
error: "Blocks are not support as first-class values".into(),
|
|
label: "blocks are not supported as values".into(),
|
|
help: "Use 'closure' instead of 'block'".into(),
|
|
span,
|
|
});
|
|
SyntaxShape::Any
|
|
}
|
|
b"bool" => SyntaxShape::Boolean,
|
|
b"cell-path" => SyntaxShape::CellPath,
|
|
b"closure" => SyntaxShape::Closure(None), //FIXME: Blocks should have known output types
|
|
b"datetime" => SyntaxShape::DateTime,
|
|
b"directory" => SyntaxShape::Directory,
|
|
b"duration" => SyntaxShape::Duration,
|
|
b"error" => SyntaxShape::Error,
|
|
b"float" => SyntaxShape::Float,
|
|
b"filesize" => SyntaxShape::Filesize,
|
|
b"glob" => SyntaxShape::GlobPattern,
|
|
b"int" => SyntaxShape::Int,
|
|
_ if bytes.starts_with(b"list") => parse_list_shape(working_set, bytes, span, use_loc),
|
|
b"nothing" => SyntaxShape::Nothing,
|
|
b"number" => SyntaxShape::Number,
|
|
b"path" => SyntaxShape::Filepath,
|
|
b"range" => SyntaxShape::Range,
|
|
_ if bytes.starts_with(b"record") => {
|
|
parse_collection_shape(working_set, bytes, span, use_loc)
|
|
}
|
|
b"string" => SyntaxShape::String,
|
|
_ if bytes.starts_with(b"table") => {
|
|
parse_collection_shape(working_set, bytes, span, use_loc)
|
|
}
|
|
_ => {
|
|
if bytes.contains(&b'@') {
|
|
let mut split = bytes.splitn(2, |b| b == &b'@');
|
|
|
|
let shape_name = split
|
|
.next()
|
|
.expect("If `bytes` contains `@` splitn returns 2 slices");
|
|
let shape_span = Span::new(span.start, span.start + shape_name.len());
|
|
let shape = parse_shape_name(working_set, shape_name, shape_span, use_loc);
|
|
if use_loc != ShapeDescriptorUse::Argument {
|
|
let illegal_span = Span::new(span.start + shape_name.len(), span.end);
|
|
working_set.error(ParseError::LabeledError(
|
|
"Unexpected custom completer in type spec".into(),
|
|
"Type specifications do not support custom completers".into(),
|
|
illegal_span,
|
|
));
|
|
return shape;
|
|
}
|
|
|
|
let cmd_span = Span::new(span.start + shape_name.len() + 1, span.end);
|
|
let cmd_name = split
|
|
.next()
|
|
.expect("If `bytes` contains `@` splitn returns 2 slices");
|
|
|
|
let cmd_name = trim_quotes(cmd_name);
|
|
if cmd_name.is_empty() {
|
|
working_set.error(ParseError::Expected(
|
|
"the command name of a completion function",
|
|
cmd_span,
|
|
));
|
|
return shape;
|
|
}
|
|
|
|
if let Some(decl_id) = working_set.find_decl(cmd_name) {
|
|
return SyntaxShape::CompleterWrapper(Box::new(shape), decl_id);
|
|
} else {
|
|
working_set.error(ParseError::UnknownCommand(cmd_span));
|
|
return shape;
|
|
}
|
|
} else {
|
|
//TODO: Handle error case for unknown shapes
|
|
working_set.error(ParseError::UnknownType(span));
|
|
return SyntaxShape::Any;
|
|
}
|
|
}
|
|
};
|
|
|
|
result
|
|
}
|
|
|
|
fn parse_collection_shape(
|
|
working_set: &mut StateWorkingSet,
|
|
bytes: &[u8],
|
|
span: Span,
|
|
use_loc: ShapeDescriptorUse,
|
|
) -> SyntaxShape {
|
|
assert!(bytes.starts_with(b"record") || bytes.starts_with(b"table"));
|
|
let is_table = bytes.starts_with(b"table");
|
|
|
|
let name = if is_table { "table" } else { "record" };
|
|
let prefix = (if is_table { "table<" } else { "record<" }).as_bytes();
|
|
let prefix_len = prefix.len();
|
|
let mk_shape = |ty| -> SyntaxShape {
|
|
if is_table {
|
|
SyntaxShape::Table(ty)
|
|
} else {
|
|
SyntaxShape::Record(ty)
|
|
}
|
|
};
|
|
|
|
if bytes == name.as_bytes() {
|
|
mk_shape(vec![])
|
|
} else if bytes.starts_with(prefix) {
|
|
let Some(inner_span) = prepare_inner_span(working_set, bytes, span, prefix_len) else {
|
|
return SyntaxShape::Any;
|
|
};
|
|
|
|
// record<> or table<>
|
|
if inner_span.end - inner_span.start == 0 {
|
|
return mk_shape(vec![]);
|
|
}
|
|
let source = working_set.get_span_contents(inner_span);
|
|
let (tokens, err) = lex_signature(
|
|
source,
|
|
inner_span.start,
|
|
&[b'\n', b'\r'],
|
|
&[b':', b','],
|
|
true,
|
|
);
|
|
|
|
if let Some(err) = err {
|
|
working_set.error(err);
|
|
// lexer errors cause issues with span overflows
|
|
return mk_shape(vec![]);
|
|
}
|
|
|
|
let mut sig = vec![];
|
|
let mut idx = 0;
|
|
|
|
let key_error = |span| {
|
|
ParseError::LabeledError(
|
|
format!("`{name}` type annotations key not string"),
|
|
"must be a string".into(),
|
|
span,
|
|
)
|
|
};
|
|
|
|
while idx < tokens.len() {
|
|
let TokenContents::Item = tokens[idx].contents else {
|
|
working_set.error(key_error(tokens[idx].span));
|
|
return mk_shape(vec![]);
|
|
};
|
|
|
|
let key_bytes = working_set.get_span_contents(tokens[idx].span).to_vec();
|
|
if key_bytes.first().copied() == Some(b',') {
|
|
idx += 1;
|
|
continue;
|
|
}
|
|
|
|
let Some(key) =
|
|
parse_value(working_set, tokens[idx].span, &SyntaxShape::String).as_string()
|
|
else {
|
|
working_set.error(key_error(tokens[idx].span));
|
|
return mk_shape(vec![]);
|
|
};
|
|
|
|
// we want to allow such an annotation
|
|
// `record<name>` where the user leaves out the type
|
|
if idx + 1 == tokens.len() {
|
|
sig.push((key, SyntaxShape::Any));
|
|
break;
|
|
} else {
|
|
idx += 1;
|
|
}
|
|
|
|
let maybe_colon = working_set.get_span_contents(tokens[idx].span).to_vec();
|
|
match maybe_colon.as_slice() {
|
|
b":" => {
|
|
if idx + 1 == tokens.len() {
|
|
working_set
|
|
.error(ParseError::Expected("type after colon", tokens[idx].span));
|
|
break;
|
|
} else {
|
|
idx += 1;
|
|
}
|
|
}
|
|
// a key provided without a type
|
|
b"," => {
|
|
idx += 1;
|
|
sig.push((key, SyntaxShape::Any));
|
|
continue;
|
|
}
|
|
// a key provided without a type
|
|
_ => {
|
|
sig.push((key, SyntaxShape::Any));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
let shape_bytes = working_set.get_span_contents(tokens[idx].span).to_vec();
|
|
let shape = parse_shape_name(working_set, &shape_bytes, tokens[idx].span, use_loc);
|
|
sig.push((key, shape));
|
|
idx += 1;
|
|
}
|
|
|
|
mk_shape(sig)
|
|
} else {
|
|
working_set.error(ParseError::UnknownType(span));
|
|
|
|
SyntaxShape::Any
|
|
}
|
|
}
|
|
|
|
fn parse_list_shape(
|
|
working_set: &mut StateWorkingSet,
|
|
bytes: &[u8],
|
|
span: Span,
|
|
use_loc: ShapeDescriptorUse,
|
|
) -> SyntaxShape {
|
|
assert!(bytes.starts_with(b"list"));
|
|
|
|
if bytes == b"list" {
|
|
SyntaxShape::List(Box::new(SyntaxShape::Any))
|
|
} else if bytes.starts_with(b"list<") {
|
|
let Some(inner_span) = prepare_inner_span(working_set, bytes, span, 5) else {
|
|
return SyntaxShape::Any;
|
|
};
|
|
|
|
let inner_text = String::from_utf8_lossy(working_set.get_span_contents(inner_span));
|
|
// remove any extra whitespace, for example `list< string >` becomes `list<string>`
|
|
let inner_bytes = inner_text.trim().as_bytes().to_vec();
|
|
|
|
// list<>
|
|
if inner_bytes.is_empty() {
|
|
SyntaxShape::List(Box::new(SyntaxShape::Any))
|
|
} else {
|
|
let inner_sig = parse_shape_name(working_set, &inner_bytes, inner_span, use_loc);
|
|
|
|
SyntaxShape::List(Box::new(inner_sig))
|
|
}
|
|
} else {
|
|
working_set.error(ParseError::UnknownType(span));
|
|
|
|
SyntaxShape::List(Box::new(SyntaxShape::Any))
|
|
}
|
|
}
|
|
|
|
fn prepare_inner_span(
|
|
working_set: &mut StateWorkingSet,
|
|
bytes: &[u8],
|
|
span: Span,
|
|
prefix_len: usize,
|
|
) -> Option<Span> {
|
|
let start = span.start + prefix_len;
|
|
|
|
if bytes.ends_with(b">") {
|
|
let end = span.end - 1;
|
|
Some(Span::new(start, end))
|
|
} else if bytes.contains(&b'>') {
|
|
let angle_start = bytes.split(|it| it == &b'>').collect::<Vec<_>>()[0].len() + 1;
|
|
let span = Span::new(span.start + angle_start, span.end);
|
|
|
|
working_set.error(ParseError::LabeledError(
|
|
"Extra characters in the parameter name".into(),
|
|
"extra characters".into(),
|
|
span,
|
|
));
|
|
|
|
None
|
|
} else {
|
|
working_set.error(ParseError::Unclosed(">".into(), span));
|
|
None
|
|
}
|
|
}
|