forked from extern/nushell
allow tables to have annotations (#9613)
# Description follow up to #8529 and #8914 this works very similarly to record annotations, only difference being that ```sh table<name: string> ^^^^ ^^^^^^ | | | represents the type of the items in that column | represents the column name ``` more info on the syntax can be found [here](https://github.com/nushell/nushell/pull/8914#issue-1672113520) # User-Facing Changes **[BREAKING CHANGE]** this change adds a field to `SyntaxShape::Table` so any plugins that used it will have to update and include the field. though if you are unsure of the type the table expects, `SyntaxShape::Table(vec![])` will suffice
This commit is contained in:
parent
440a0e960a
commit
8e38596bc9
@ -25,7 +25,7 @@ impl Command for DropDuplicates {
|
|||||||
Signature::build(self.name())
|
Signature::build(self.name())
|
||||||
.optional(
|
.optional(
|
||||||
"subset",
|
"subset",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"subset of columns to drop duplicates",
|
"subset of columns to drop duplicates",
|
||||||
)
|
)
|
||||||
.switch("maintain", "maintain order", Some('m'))
|
.switch("maintain", "maintain order", Some('m'))
|
||||||
|
@ -24,7 +24,7 @@ impl Command for DropNulls {
|
|||||||
Signature::build(self.name())
|
Signature::build(self.name())
|
||||||
.optional(
|
.optional(
|
||||||
"subset",
|
"subset",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"subset of columns to drop nulls",
|
"subset of columns to drop nulls",
|
||||||
)
|
)
|
||||||
.input_output_type(
|
.input_output_type(
|
||||||
|
@ -26,13 +26,13 @@ impl Command for MeltDF {
|
|||||||
Signature::build(self.name())
|
Signature::build(self.name())
|
||||||
.required_named(
|
.required_named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"column names for melting",
|
"column names for melting",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
.required_named(
|
.required_named(
|
||||||
"values",
|
"values",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"column names used as value columns",
|
"column names used as value columns",
|
||||||
Some('v'),
|
Some('v'),
|
||||||
)
|
)
|
||||||
|
@ -35,7 +35,7 @@ impl Command for Summary {
|
|||||||
)
|
)
|
||||||
.named(
|
.named(
|
||||||
"quantiles",
|
"quantiles",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"provide optional quantiles",
|
"provide optional quantiles",
|
||||||
Some('q'),
|
Some('q'),
|
||||||
)
|
)
|
||||||
|
@ -32,6 +32,9 @@ impl Command for BytesIndexOf {
|
|||||||
.input_output_types(vec![
|
.input_output_types(vec![
|
||||||
(Type::Binary, Type::Int),
|
(Type::Binary, Type::Int),
|
||||||
(Type::Binary, Type::List(Box::new(Type::Int))),
|
(Type::Binary, Type::List(Box::new(Type::Int))),
|
||||||
|
// FIXME: this shouldn't be needed, cell paths should work with the two
|
||||||
|
// above
|
||||||
|
(Type::Table(vec![]), Type::Table(vec![])),
|
||||||
])
|
])
|
||||||
.required(
|
.required(
|
||||||
"pattern",
|
"pattern",
|
||||||
|
@ -26,7 +26,7 @@ impl Command for UpdateCells {
|
|||||||
)
|
)
|
||||||
.named(
|
.named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"list of columns to update",
|
"list of columns to update",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
|
@ -19,6 +19,7 @@ impl Command for SubCommand {
|
|||||||
.input_output_types(vec![
|
.input_output_types(vec![
|
||||||
(Type::String, Type::Number),
|
(Type::String, Type::Number),
|
||||||
(Type::Bool, Type::Number),
|
(Type::Bool, Type::Number),
|
||||||
|
(Type::Table(vec![]), Type::Table(vec![])),
|
||||||
])
|
])
|
||||||
.rest(
|
.rest(
|
||||||
"rest",
|
"rest",
|
||||||
|
@ -23,6 +23,7 @@ impl Command for SubCommand {
|
|||||||
// TODO: --convert option should be implemented as `format duration`
|
// TODO: --convert option should be implemented as `format duration`
|
||||||
(Type::String, Type::String),
|
(Type::String, Type::String),
|
||||||
(Type::Duration, Type::String),
|
(Type::Duration, Type::String),
|
||||||
|
(Type::Table(vec![]), Type::Table(vec![])),
|
||||||
])
|
])
|
||||||
.named(
|
.named(
|
||||||
"convert",
|
"convert",
|
||||||
|
@ -38,7 +38,7 @@ impl Command for SubCommand {
|
|||||||
])
|
])
|
||||||
.named(
|
.named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"For a record or table input, convert strings in the given columns to their basename",
|
"For a record or table input, convert strings in the given columns to their basename",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
|
@ -35,7 +35,7 @@ impl Command for SubCommand {
|
|||||||
.input_output_types(vec![(Type::String, Type::String)])
|
.input_output_types(vec![(Type::String, Type::String)])
|
||||||
.named(
|
.named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"For a record or table input, convert strings at the given columns to their dirname",
|
"For a record or table input, convert strings at the given columns to their dirname",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
|
@ -34,7 +34,7 @@ impl Command for SubCommand {
|
|||||||
.input_output_types(vec![(Type::String, Type::Bool)])
|
.input_output_types(vec![(Type::String, Type::Bool)])
|
||||||
.named(
|
.named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"For a record or table input, check strings at the given columns, and replace with result",
|
"For a record or table input, check strings at the given columns, and replace with result",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
|
@ -43,7 +43,7 @@ impl Command for SubCommand {
|
|||||||
.switch("no-symlink", "Do not resolve symbolic links", Some('n'))
|
.switch("no-symlink", "Do not resolve symbolic links", Some('n'))
|
||||||
.named(
|
.named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"For a record or table input, expand strings at the given columns",
|
"For a record or table input, expand strings at the given columns",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
|
@ -39,7 +39,7 @@ impl Command for SubCommand {
|
|||||||
])
|
])
|
||||||
.named(
|
.named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"For a record or table input, join strings at the given columns",
|
"For a record or table input, join strings at the given columns",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
|
@ -35,7 +35,7 @@ impl Command for SubCommand {
|
|||||||
.input_output_types(vec![(Type::String, Type::Record(vec![]))])
|
.input_output_types(vec![(Type::String, Type::Record(vec![]))])
|
||||||
.named(
|
.named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"For a record or table input, convert strings at the given columns",
|
"For a record or table input, convert strings at the given columns",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
|
@ -40,7 +40,7 @@ impl Command for SubCommand {
|
|||||||
)
|
)
|
||||||
.named(
|
.named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"For a record or table input, convert strings at the given columns",
|
"For a record or table input, convert strings at the given columns",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
|
@ -32,7 +32,7 @@ impl Command for SubCommand {
|
|||||||
.input_output_types(vec![(Type::String, Type::List(Box::new(Type::String)))])
|
.input_output_types(vec![(Type::String, Type::List(Box::new(Type::String)))])
|
||||||
.named(
|
.named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"For a record or table input, split strings at the given columns",
|
"For a record or table input, split strings at the given columns",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
|
@ -32,7 +32,7 @@ impl Command for SubCommand {
|
|||||||
.input_output_types(vec![(Type::String, Type::String)])
|
.input_output_types(vec![(Type::String, Type::String)])
|
||||||
.named(
|
.named(
|
||||||
"columns",
|
"columns",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"For a record or table input, check strings at the given columns, and replace with result",
|
"For a record or table input, check strings at the given columns, and replace with result",
|
||||||
Some('c'),
|
Some('c'),
|
||||||
)
|
)
|
||||||
|
@ -29,7 +29,10 @@ impl Command for SubCommand {
|
|||||||
|
|
||||||
fn signature(&self) -> Signature {
|
fn signature(&self) -> Signature {
|
||||||
Signature::build("str contains")
|
Signature::build("str contains")
|
||||||
.input_output_types(vec![(Type::String, Type::Bool)])
|
.input_output_types(vec![
|
||||||
|
(Type::String, Type::Bool),
|
||||||
|
(Type::Table(vec![]), Type::Table(vec![])),
|
||||||
|
])
|
||||||
.vectorizes_over_list(true)
|
.vectorizes_over_list(true)
|
||||||
.required("string", SyntaxShape::String, "the substring to find")
|
.required("string", SyntaxShape::String, "the substring to find")
|
||||||
.rest(
|
.rest(
|
||||||
|
@ -28,7 +28,10 @@ impl Command for SubCommand {
|
|||||||
|
|
||||||
fn signature(&self) -> Signature {
|
fn signature(&self) -> Signature {
|
||||||
Signature::build("str distance")
|
Signature::build("str distance")
|
||||||
.input_output_types(vec![(Type::String, Type::Int)])
|
.input_output_types(vec![
|
||||||
|
(Type::String, Type::Int),
|
||||||
|
(Type::Table(vec![]), Type::Table(vec![])),
|
||||||
|
])
|
||||||
.required(
|
.required(
|
||||||
"compare-string",
|
"compare-string",
|
||||||
SyntaxShape::String,
|
SyntaxShape::String,
|
||||||
|
@ -2724,7 +2724,7 @@ pub fn parse_shape_name(
|
|||||||
_ if bytes.starts_with(b"record") => parse_collection_shape(working_set, bytes, span),
|
_ if bytes.starts_with(b"record") => parse_collection_shape(working_set, bytes, span),
|
||||||
b"signature" => SyntaxShape::Signature,
|
b"signature" => SyntaxShape::Signature,
|
||||||
b"string" => SyntaxShape::String,
|
b"string" => SyntaxShape::String,
|
||||||
b"table" => SyntaxShape::Table,
|
_ if bytes.starts_with(b"table") => parse_collection_shape(working_set, bytes, span),
|
||||||
b"variable" => SyntaxShape::Variable,
|
b"variable" => SyntaxShape::Variable,
|
||||||
b"var-with-opt-type" => SyntaxShape::VarWithOptType,
|
b"var-with-opt-type" => SyntaxShape::VarWithOptType,
|
||||||
_ => {
|
_ => {
|
||||||
@ -2765,14 +2765,24 @@ fn parse_collection_shape(
|
|||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
span: Span,
|
span: Span,
|
||||||
) -> SyntaxShape {
|
) -> SyntaxShape {
|
||||||
assert!(bytes.starts_with(b"record"));
|
assert!(bytes.starts_with(b"record") || bytes.starts_with(b"table"));
|
||||||
let name = "record";
|
let is_table = bytes.starts_with(b"table");
|
||||||
let mk_shape = SyntaxShape::Record;
|
|
||||||
|
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() {
|
if bytes == name.as_bytes() {
|
||||||
mk_shape(vec![])
|
mk_shape(vec![])
|
||||||
} else if bytes.starts_with(b"record<") {
|
} else if bytes.starts_with(prefix) {
|
||||||
let Some(inner_span) = prepare_inner_span(working_set, bytes, span, 7) else {
|
let Some(inner_span) = prepare_inner_span(working_set, bytes, span, prefix_len) else {
|
||||||
return SyntaxShape::Any;
|
return SyntaxShape::Any;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -3902,122 +3912,173 @@ pub fn parse_list_expression(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_table_expression(
|
fn parse_table_expression(working_set: &mut StateWorkingSet, span: Span) -> Expression {
|
||||||
working_set: &mut StateWorkingSet,
|
let bytes = working_set.get_span_contents(span);
|
||||||
original_span: Span,
|
let inner_span = {
|
||||||
) -> Expression {
|
let start = if bytes.starts_with(b"[") {
|
||||||
let bytes = working_set.get_span_contents(original_span);
|
span.start + 1
|
||||||
|
} else {
|
||||||
|
span.start
|
||||||
|
};
|
||||||
|
|
||||||
let mut start = original_span.start;
|
let end = if bytes.ends_with(b"]") {
|
||||||
let mut end = original_span.end;
|
span.end - 1
|
||||||
|
} else {
|
||||||
|
let end = span.end;
|
||||||
|
working_set.error(ParseError::Unclosed("]".into(), Span::new(end, end)));
|
||||||
|
span.end
|
||||||
|
};
|
||||||
|
|
||||||
if bytes.starts_with(b"[") {
|
Span::new(start, end)
|
||||||
start += 1;
|
};
|
||||||
}
|
|
||||||
if bytes.ends_with(b"]") {
|
|
||||||
end -= 1;
|
|
||||||
} else {
|
|
||||||
working_set.error(ParseError::Unclosed("]".into(), Span::new(end, end)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let inner_span = Span::new(start, end);
|
|
||||||
|
|
||||||
let source = working_set.get_span_contents(inner_span);
|
let source = working_set.get_span_contents(inner_span);
|
||||||
|
let (tokens, err) = lex(source, inner_span.start, &[b'\n', b'\r', b','], &[], true);
|
||||||
let (output, err) = lex(source, start, &[b'\n', b'\r', b','], &[], true);
|
|
||||||
if let Some(err) = err {
|
if let Some(err) = err {
|
||||||
working_set.error(err);
|
working_set.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (output, err) = lite_parse(&output);
|
let head = if let Some(first) = tokens.first() {
|
||||||
if let Some(err) = err {
|
if working_set.get_span_contents(first.span).starts_with(b"[") {
|
||||||
working_set.error(err);
|
parse_list_expression(working_set, first.span, &SyntaxShape::Any)
|
||||||
}
|
} else {
|
||||||
|
return parse_list_expression(working_set, span, &SyntaxShape::Any);
|
||||||
match output.block.len() {
|
|
||||||
0 => Expression {
|
|
||||||
expr: Expr::List(vec![]),
|
|
||||||
span: original_span,
|
|
||||||
ty: Type::List(Box::new(Type::Any)),
|
|
||||||
custom_completion: None,
|
|
||||||
},
|
|
||||||
1 => {
|
|
||||||
// List
|
|
||||||
parse_list_expression(working_set, original_span, &SyntaxShape::Any)
|
|
||||||
}
|
}
|
||||||
_ => {
|
} else {
|
||||||
match &output.block[0].commands[0] {
|
return parse_list_expression(working_set, span, &SyntaxShape::Any);
|
||||||
LiteElement::Command(_, command)
|
};
|
||||||
| LiteElement::Redirection(_, _, command)
|
|
||||||
| LiteElement::SeparateRedirection {
|
if tokens
|
||||||
out: (_, command), ..
|
.get(1)
|
||||||
|
.filter(|second| second.contents == TokenContents::Semicolon)
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
return parse_list_expression(working_set, span, &SyntaxShape::Any);
|
||||||
|
};
|
||||||
|
|
||||||
|
let rest = &tokens[2..];
|
||||||
|
if rest.is_empty() {
|
||||||
|
return parse_list_expression(working_set, span, &SyntaxShape::Any);
|
||||||
|
}
|
||||||
|
|
||||||
|
let head = {
|
||||||
|
let Expression { expr: Expr::List(vals), .. } = head else {
|
||||||
|
unreachable!("head must be a list by now")
|
||||||
|
};
|
||||||
|
|
||||||
|
vals
|
||||||
|
};
|
||||||
|
|
||||||
|
let errors = working_set.parse_errors.len();
|
||||||
|
|
||||||
|
let rows = rest
|
||||||
|
.iter()
|
||||||
|
.fold(Vec::with_capacity(rest.len()), |mut acc, it| {
|
||||||
|
use std::cmp::Ordering;
|
||||||
|
let text = working_set.get_span_contents(it.span).to_vec();
|
||||||
|
match text.as_slice() {
|
||||||
|
b"," => acc,
|
||||||
|
_ if !&text.starts_with(b"[") => {
|
||||||
|
let err = ParseError::LabeledErrorWithHelp {
|
||||||
|
error: String::from("Table item not list"),
|
||||||
|
label: String::from("not a list"),
|
||||||
|
span: it.span,
|
||||||
|
help: String::from("All table items must be lists"),
|
||||||
|
};
|
||||||
|
working_set.error(err);
|
||||||
|
acc
|
||||||
}
|
}
|
||||||
| LiteElement::SameTargetRedirection {
|
_ => {
|
||||||
cmd: (_, command), ..
|
let ls = parse_list_expression(working_set, it.span, &SyntaxShape::Any);
|
||||||
} => {
|
let Expression {
|
||||||
let mut table_headers = vec![];
|
expr: Expr::List(item),
|
||||||
|
span,
|
||||||
let headers =
|
|
||||||
parse_list_expression(working_set, command.parts[0], &SyntaxShape::Any);
|
|
||||||
|
|
||||||
if let Expression {
|
|
||||||
expr: Expr::List(headers),
|
|
||||||
..
|
..
|
||||||
} = headers
|
} = ls else {
|
||||||
{
|
unreachable!("the item must be a list")
|
||||||
table_headers = headers;
|
};
|
||||||
|
|
||||||
|
match item.len().cmp(&head.len()) {
|
||||||
|
Ordering::Less => {
|
||||||
|
let err = ParseError::MissingColumns(head.len(), span);
|
||||||
|
working_set.error(err);
|
||||||
|
}
|
||||||
|
Ordering::Greater => {
|
||||||
|
let span = {
|
||||||
|
let start = item[head.len()].span.start;
|
||||||
|
let end = span.end;
|
||||||
|
Span::new(start, end)
|
||||||
|
};
|
||||||
|
let err = ParseError::ExtraColumns(head.len(), span);
|
||||||
|
working_set.error(err);
|
||||||
|
}
|
||||||
|
Ordering::Equal => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
match &output.block[1].commands[0] {
|
acc.push(item);
|
||||||
LiteElement::Command(_, command)
|
acc
|
||||||
| LiteElement::Redirection(_, _, command)
|
|
||||||
| LiteElement::SeparateRedirection {
|
|
||||||
out: (_, command), ..
|
|
||||||
}
|
|
||||||
| LiteElement::SameTargetRedirection {
|
|
||||||
cmd: (_, command), ..
|
|
||||||
} => {
|
|
||||||
let mut rows = vec![];
|
|
||||||
for part in &command.parts {
|
|
||||||
let values =
|
|
||||||
parse_list_expression(working_set, *part, &SyntaxShape::Any);
|
|
||||||
if let Expression {
|
|
||||||
expr: Expr::List(values),
|
|
||||||
span,
|
|
||||||
..
|
|
||||||
} = values
|
|
||||||
{
|
|
||||||
match values.len().cmp(&table_headers.len()) {
|
|
||||||
std::cmp::Ordering::Less => working_set.error(
|
|
||||||
ParseError::MissingColumns(table_headers.len(), span),
|
|
||||||
),
|
|
||||||
std::cmp::Ordering::Equal => {}
|
|
||||||
std::cmp::Ordering::Greater => {
|
|
||||||
working_set.error(ParseError::ExtraColumns(
|
|
||||||
table_headers.len(),
|
|
||||||
values[table_headers.len()].span,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rows.push(values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Expression {
|
|
||||||
expr: Expr::Table(table_headers, rows),
|
|
||||||
span: original_span,
|
|
||||||
ty: Type::Table(vec![]), //FIXME
|
|
||||||
custom_completion: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
let ty = if working_set.parse_errors.len() == errors {
|
||||||
|
let (ty, errs) = table_type(&head, &rows);
|
||||||
|
working_set.parse_errors.extend(errs.into_iter());
|
||||||
|
ty
|
||||||
|
} else {
|
||||||
|
Type::Table(vec![])
|
||||||
|
};
|
||||||
|
|
||||||
|
Expression {
|
||||||
|
expr: Expr::Table(head, rows),
|
||||||
|
span,
|
||||||
|
ty,
|
||||||
|
custom_completion: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn table_type(head: &[Expression], rows: &[Vec<Expression>]) -> (Type, Vec<ParseError>) {
|
||||||
|
let mut errors = vec![];
|
||||||
|
let mut rows = rows.to_vec();
|
||||||
|
let mut mk_ty = || -> Type {
|
||||||
|
rows.iter_mut()
|
||||||
|
.map(|row| row.pop().map(|x| x.ty).unwrap_or_default())
|
||||||
|
.reduce(|acc, ty| -> Type {
|
||||||
|
if type_compatible(&acc, &ty) {
|
||||||
|
ty
|
||||||
|
} else {
|
||||||
|
Type::Any
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mk_error = |span| ParseError::LabeledErrorWithHelp {
|
||||||
|
error: "Table column name not string".into(),
|
||||||
|
label: "must be a string".into(),
|
||||||
|
help: "Table column names should be able to be converted into strings".into(),
|
||||||
|
span,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut ty = head
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.map(|expr| {
|
||||||
|
if let Some(str) = expr.as_string() {
|
||||||
|
str
|
||||||
|
} else {
|
||||||
|
errors.push(mk_error(expr.span));
|
||||||
|
String::from("{ column }")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(|title| (title, mk_ty()))
|
||||||
|
.collect_vec();
|
||||||
|
|
||||||
|
ty.reverse();
|
||||||
|
|
||||||
|
(Type::Table(ty), errors)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse_block_expression(working_set: &mut StateWorkingSet, span: Span) -> Expression {
|
pub fn parse_block_expression(working_set: &mut StateWorkingSet, span: Span) -> Expression {
|
||||||
trace!("parsing: block expression");
|
trace!("parsing: block expression");
|
||||||
|
|
||||||
@ -4456,7 +4517,7 @@ pub fn parse_value(
|
|||||||
b'[' => match shape {
|
b'[' => match shape {
|
||||||
SyntaxShape::Any
|
SyntaxShape::Any
|
||||||
| SyntaxShape::List(_)
|
| SyntaxShape::List(_)
|
||||||
| SyntaxShape::Table
|
| SyntaxShape::Table(_)
|
||||||
| SyntaxShape::Signature => {}
|
| SyntaxShape::Signature => {}
|
||||||
_ => {
|
_ => {
|
||||||
working_set.error(ParseError::Expected("non-[] value", span));
|
working_set.error(ParseError::Expected("non-[] value", span));
|
||||||
@ -4503,7 +4564,7 @@ pub fn parse_value(
|
|||||||
Expression::garbage(span)
|
Expression::garbage(span)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SyntaxShape::Table => {
|
SyntaxShape::Table(_) => {
|
||||||
if bytes.starts_with(b"[") {
|
if bytes.starts_with(b"[") {
|
||||||
parse_table_expression(working_set, span)
|
parse_table_expression(working_set, span)
|
||||||
} else {
|
} else {
|
||||||
|
@ -32,8 +32,8 @@ pub fn type_compatible(lhs: &Type, rhs: &Type) -> bool {
|
|||||||
(Type::Closure, Type::Block) => true,
|
(Type::Closure, Type::Block) => true,
|
||||||
(Type::Any, _) => true,
|
(Type::Any, _) => true,
|
||||||
(_, Type::Any) => true,
|
(_, Type::Any) => true,
|
||||||
(Type::Record(fields_lhs), Type::Record(fields_rhs)) => {
|
(Type::Record(lhs), Type::Record(rhs)) | (Type::Table(lhs), Type::Table(rhs)) => {
|
||||||
is_compatible(fields_lhs, fields_rhs)
|
is_compatible(lhs, rhs)
|
||||||
}
|
}
|
||||||
(lhs, rhs) => lhs == rhs,
|
(lhs, rhs) => lhs == rhs,
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ pub enum SyntaxShape {
|
|||||||
String,
|
String,
|
||||||
|
|
||||||
/// A table is allowed, eg `[[first, second]; [1, 2]]`
|
/// A table is allowed, eg `[[first, second]; [1, 2]]`
|
||||||
Table,
|
Table(Vec<(String, SyntaxShape)>),
|
||||||
|
|
||||||
/// A variable name, eg `$foo`
|
/// A variable name, eg `$foo`
|
||||||
Variable,
|
Variable,
|
||||||
@ -119,6 +119,12 @@ pub enum SyntaxShape {
|
|||||||
|
|
||||||
impl SyntaxShape {
|
impl SyntaxShape {
|
||||||
pub fn to_type(&self) -> Type {
|
pub fn to_type(&self) -> Type {
|
||||||
|
let mk_ty = |tys: &[(String, SyntaxShape)]| {
|
||||||
|
tys.iter()
|
||||||
|
.map(|(key, val)| (key.clone(), val.to_type()))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
SyntaxShape::Any => Type::Any,
|
SyntaxShape::Any => Type::Any,
|
||||||
SyntaxShape::Block => Type::Block,
|
SyntaxShape::Block => Type::Block,
|
||||||
@ -151,18 +157,12 @@ impl SyntaxShape {
|
|||||||
SyntaxShape::OneOf(_) => Type::Any,
|
SyntaxShape::OneOf(_) => Type::Any,
|
||||||
SyntaxShape::Operator => Type::Any,
|
SyntaxShape::Operator => Type::Any,
|
||||||
SyntaxShape::Range => Type::Any,
|
SyntaxShape::Range => Type::Any,
|
||||||
SyntaxShape::Record(entries) => {
|
SyntaxShape::Record(entries) => Type::Record(mk_ty(entries)),
|
||||||
let ty = entries
|
|
||||||
.iter()
|
|
||||||
.map(|(key, val)| (key.clone(), val.to_type()))
|
|
||||||
.collect();
|
|
||||||
Type::Record(ty)
|
|
||||||
}
|
|
||||||
SyntaxShape::RowCondition => Type::Bool,
|
SyntaxShape::RowCondition => Type::Bool,
|
||||||
SyntaxShape::Boolean => Type::Bool,
|
SyntaxShape::Boolean => Type::Bool,
|
||||||
SyntaxShape::Signature => Type::Signature,
|
SyntaxShape::Signature => Type::Signature,
|
||||||
SyntaxShape::String => Type::String,
|
SyntaxShape::String => Type::String,
|
||||||
SyntaxShape::Table => Type::Table(vec![]), // FIXME: What role should columns play in the Table type?
|
SyntaxShape::Table(columns) => Type::Table(mk_ty(columns)),
|
||||||
SyntaxShape::VarWithOptType => Type::Any,
|
SyntaxShape::VarWithOptType => Type::Any,
|
||||||
SyntaxShape::Variable => Type::Any,
|
SyntaxShape::Variable => Type::Any,
|
||||||
}
|
}
|
||||||
@ -171,6 +171,13 @@ impl SyntaxShape {
|
|||||||
|
|
||||||
impl Display for SyntaxShape {
|
impl Display for SyntaxShape {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mk_fmt = |tys: &[(String, SyntaxShape)]| -> String {
|
||||||
|
tys.iter()
|
||||||
|
.map(|(x, y)| format!("{x}: {y}"))
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join(", ")
|
||||||
|
};
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
SyntaxShape::Keyword(kw, shape) => {
|
SyntaxShape::Keyword(kw, shape) => {
|
||||||
write!(f, "\"{}\" {}", String::from_utf8_lossy(kw), shape)
|
write!(f, "\"{}\" {}", String::from_utf8_lossy(kw), shape)
|
||||||
@ -198,21 +205,19 @@ impl Display for SyntaxShape {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SyntaxShape::Binary => write!(f, "binary"),
|
SyntaxShape::Binary => write!(f, "binary"),
|
||||||
SyntaxShape::Table => write!(f, "table"),
|
|
||||||
SyntaxShape::List(x) => write!(f, "list<{x}>"),
|
SyntaxShape::List(x) => write!(f, "list<{x}>"),
|
||||||
|
SyntaxShape::Table(columns) => {
|
||||||
|
if columns.is_empty() {
|
||||||
|
write!(f, "table")
|
||||||
|
} else {
|
||||||
|
write!(f, "table<{}>", mk_fmt(columns))
|
||||||
|
}
|
||||||
|
}
|
||||||
SyntaxShape::Record(entries) => {
|
SyntaxShape::Record(entries) => {
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
write!(f, "record")
|
write!(f, "record")
|
||||||
} else {
|
} else {
|
||||||
write!(
|
write!(f, "record<{}>", mk_fmt(entries))
|
||||||
f,
|
|
||||||
"record<{}>",
|
|
||||||
entries
|
|
||||||
.iter()
|
|
||||||
.map(|(x, y)| format!("{x}: {y}"))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join(", "),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SyntaxShape::Filesize => write!(f, "filesize"),
|
SyntaxShape::Filesize => write!(f, "filesize"),
|
||||||
|
@ -81,6 +81,12 @@ impl Type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_shape(&self) -> SyntaxShape {
|
pub fn to_shape(&self) -> SyntaxShape {
|
||||||
|
let mk_shape = |tys: &[(String, Type)]| {
|
||||||
|
tys.iter()
|
||||||
|
.map(|(key, val)| (key.clone(), val.to_shape()))
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Type::Int => SyntaxShape::Int,
|
Type::Int => SyntaxShape::Int,
|
||||||
Type::Float => SyntaxShape::Number,
|
Type::Float => SyntaxShape::Number,
|
||||||
@ -96,14 +102,8 @@ impl Type {
|
|||||||
Type::List(x) => SyntaxShape::List(Box::new(x.to_shape())),
|
Type::List(x) => SyntaxShape::List(Box::new(x.to_shape())),
|
||||||
Type::Number => SyntaxShape::Number,
|
Type::Number => SyntaxShape::Number,
|
||||||
Type::Nothing => SyntaxShape::Nothing,
|
Type::Nothing => SyntaxShape::Nothing,
|
||||||
Type::Record(entries) => {
|
Type::Record(entries) => SyntaxShape::Record(mk_shape(entries)),
|
||||||
let entries = entries
|
Type::Table(columns) => SyntaxShape::Table(mk_shape(columns)),
|
||||||
.iter()
|
|
||||||
.map(|(key, val)| (key.clone(), val.to_shape()))
|
|
||||||
.collect();
|
|
||||||
SyntaxShape::Record(entries)
|
|
||||||
}
|
|
||||||
Type::Table(_) => SyntaxShape::Table,
|
|
||||||
Type::ListStream => SyntaxShape::List(Box::new(SyntaxShape::Any)),
|
Type::ListStream => SyntaxShape::List(Box::new(SyntaxShape::Any)),
|
||||||
Type::Any => SyntaxShape::Any,
|
Type::Any => SyntaxShape::Any,
|
||||||
Type::Error => SyntaxShape::Any,
|
Type::Error => SyntaxShape::Any,
|
||||||
|
@ -32,7 +32,7 @@ impl Plugin for Query {
|
|||||||
)
|
)
|
||||||
.named(
|
.named(
|
||||||
"as-table",
|
"as-table",
|
||||||
SyntaxShape::Table,
|
SyntaxShape::Table(vec![]),
|
||||||
"find table based on column header list",
|
"find table based on column header list",
|
||||||
Some('t'),
|
Some('t'),
|
||||||
)
|
)
|
||||||
|
@ -262,3 +262,116 @@ fn record_annotations_with_extra_characters() -> TestResult {
|
|||||||
let expected = "Extra characters in the parameter name";
|
let expected = "Extra characters in the parameter name";
|
||||||
fail_test(input, expected)
|
fail_test(input, expected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_none() -> TestResult {
|
||||||
|
let input = "def run [t: table] { $t }; run [[]; []] | describe";
|
||||||
|
let expected = "table";
|
||||||
|
run_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations() -> TestResult {
|
||||||
|
let input = "def run [t: table<age: int>] { $t }; run [[age]; [3]] | describe";
|
||||||
|
let expected = "table<age: int>";
|
||||||
|
run_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_two_types() -> TestResult {
|
||||||
|
let input = "\
|
||||||
|
def run [t: table<name: string age: int>] { $t };
|
||||||
|
run [[name, age]; [nushell, 3]] | describe";
|
||||||
|
let expected = "table<name: string, age: int>";
|
||||||
|
run_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_two_types_comma_sep() -> TestResult {
|
||||||
|
let input = "\
|
||||||
|
def run [t: table<name: string, age: int>] { $t };
|
||||||
|
run [[name, age]; [nushell, 3]] | describe";
|
||||||
|
let expected = "table<name: string, age: int>";
|
||||||
|
run_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_key_with_no_type() -> TestResult {
|
||||||
|
let input = "def run [t: table<name>] { $t }; run [[name]; [nushell]] | describe";
|
||||||
|
let expected = "table<name: string>";
|
||||||
|
run_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_two_types_one_with_no_type() -> TestResult {
|
||||||
|
let input = "\
|
||||||
|
def run [t: table<name: string, age>] { $t };
|
||||||
|
run [[name, age]; [nushell, 3]] | describe";
|
||||||
|
let expected = "table<name: string, age: int>";
|
||||||
|
run_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_two_types_both_with_no_types() -> TestResult {
|
||||||
|
let input = "\
|
||||||
|
def run [t: table<name, age>] { $t };
|
||||||
|
run [[name, age]; [nushell, 3]] | describe";
|
||||||
|
let expected = "table<name: string, age: int>";
|
||||||
|
run_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_type_inference_1() -> TestResult {
|
||||||
|
let input = "def run [t: table<age: any>] { $t }; run [[age]; [2wk]] | describe";
|
||||||
|
let expected = "table<age: duration>";
|
||||||
|
run_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_type_inference_2() -> TestResult {
|
||||||
|
let input = "def run [t: table<size>] { $t }; run [[size]; [2mb]] | describe";
|
||||||
|
let expected = "table<size: filesize>";
|
||||||
|
run_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_not_terminated() -> TestResult {
|
||||||
|
let input = "def run [t: table<age: int] { $t }";
|
||||||
|
let expected = "expected closing >";
|
||||||
|
fail_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_not_terminated_inner() -> TestResult {
|
||||||
|
let input = "def run [t: table<name: string, repos: list<string>] { $t }";
|
||||||
|
let expected = "expected closing >";
|
||||||
|
fail_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_no_type_after_colon() -> TestResult {
|
||||||
|
let input = "def run [t: table<name: >] { $t }";
|
||||||
|
let expected = "type after colon";
|
||||||
|
fail_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_type_mismatch_column() -> TestResult {
|
||||||
|
let input = "def run [t: table<name: string>] { $t }; run [[nme]; [nushell]]";
|
||||||
|
let expected = "expected table<name: string>, found table<nme: string>";
|
||||||
|
fail_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_type_mismatch_shape() -> TestResult {
|
||||||
|
let input = "def run [t: table<age: int>] { $t }; run [[age]; [2wk]]";
|
||||||
|
let expected = "expected table<age: int>, found table<age: duration>";
|
||||||
|
fail_test(input, expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_annotations_with_extra_characters() -> TestResult {
|
||||||
|
let input = "def run [t: table<int>extra] {$t | length}; run [[int]; [8]]";
|
||||||
|
let expected = "Extra characters in the parameter name";
|
||||||
|
fail_test(input, expected)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user