forked from extern/nushell
Merge branch 'main' of https://github.com/nushell/engine-q into parse-error
This commit is contained in:
commit
2ea19aeac0
44
Cargo.lock
generated
44
Cargo.lock
generated
@ -2,6 +2,15 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.12.1"
|
||||
@ -148,6 +157,7 @@ dependencies = [
|
||||
"nu-engine",
|
||||
"nu-parser",
|
||||
"nu-protocol",
|
||||
"nu-table",
|
||||
"pretty_assertions",
|
||||
"reedline",
|
||||
"tempfile",
|
||||
@ -164,6 +174,12 @@ dependencies = [
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.10"
|
||||
@ -276,8 +292,10 @@ dependencies = [
|
||||
name = "nu-command"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"nu-engine",
|
||||
"nu-protocol",
|
||||
"nu-table",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -303,6 +321,15 @@ dependencies = [
|
||||
"codespan-reporting",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-table"
|
||||
version = "0.36.0"
|
||||
dependencies = [
|
||||
"nu-ansi-term",
|
||||
"regex",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
@ -487,12 +514,29 @@ dependencies = [
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
|
||||
|
||||
[[package]]
|
||||
name = "remove_dir_all"
|
||||
version = "0.5.3"
|
||||
|
@ -16,6 +16,7 @@ nu-command = { path="./crates/nu-command" }
|
||||
nu-engine = { path="./crates/nu-engine" }
|
||||
nu-parser = { path="./crates/nu-parser" }
|
||||
nu-protocol = { path = "./crates/nu-protocol" }
|
||||
nu-table = { path = "./crates/nu-table" }
|
||||
|
||||
# mimalloc = { version = "*", default-features = false }
|
||||
|
||||
|
3
TODO.md
3
TODO.md
@ -17,7 +17,8 @@
|
||||
- [x] Column path
|
||||
- [x] ...rest without calling it rest
|
||||
- [x] Iteration (`each`) over tables
|
||||
- [ ] Row conditions
|
||||
- [x] Row conditions
|
||||
- [x] Simple completions
|
||||
- [ ] Value serialization
|
||||
- [ ] Handling rows with missing columns during a cell path
|
||||
- [ ] Error shortcircuit (stopping on first error)
|
||||
|
54
crates/nu-cli/src/completions.rs
Normal file
54
crates/nu-cli/src/completions.rs
Normal file
@ -0,0 +1,54 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use nu_parser::{flatten_block, parse};
|
||||
use nu_protocol::engine::{EngineState, StateWorkingSet};
|
||||
use reedline::Completer;
|
||||
|
||||
pub struct NuCompleter {
|
||||
engine_state: Rc<RefCell<EngineState>>,
|
||||
}
|
||||
|
||||
impl NuCompleter {
|
||||
pub fn new(engine_state: Rc<RefCell<EngineState>>) -> Self {
|
||||
Self { engine_state }
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for NuCompleter {
|
||||
fn complete(&self, line: &str, pos: usize) -> Vec<(reedline::Span, String)> {
|
||||
let engine_state = self.engine_state.borrow();
|
||||
let mut working_set = StateWorkingSet::new(&*engine_state);
|
||||
let offset = working_set.next_span_start();
|
||||
let pos = offset + pos;
|
||||
let (output, _err) = parse(&mut working_set, Some("completer"), line.as_bytes(), false);
|
||||
|
||||
let flattened = flatten_block(&working_set, &output);
|
||||
|
||||
for flat in flattened {
|
||||
if pos >= flat.0.start && pos <= flat.0.end {
|
||||
match flat.1 {
|
||||
nu_parser::FlatShape::External | nu_parser::FlatShape::InternalCall => {
|
||||
let prefix = working_set.get_span_contents(flat.0);
|
||||
let results = working_set.find_commands_by_prefix(prefix);
|
||||
|
||||
return results
|
||||
.into_iter()
|
||||
.map(move |x| {
|
||||
(
|
||||
reedline::Span {
|
||||
start: flat.0.start - offset,
|
||||
end: flat.0.end - offset,
|
||||
},
|
||||
String::from_utf8_lossy(&x).to_string(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vec![]
|
||||
}
|
||||
}
|
@ -1,5 +1,7 @@
|
||||
mod completions;
|
||||
mod errors;
|
||||
mod syntax_highlight;
|
||||
|
||||
pub use completions::NuCompleter;
|
||||
pub use errors::{report_parsing_error, report_shell_error};
|
||||
pub use syntax_highlight::NuHighlighter;
|
||||
|
@ -8,3 +8,7 @@ edition = "2018"
|
||||
[dependencies]
|
||||
nu-protocol = { path = "../nu-protocol" }
|
||||
nu-engine = { path = "../nu-engine" }
|
||||
nu-table = { path = "../nu-table" }
|
||||
|
||||
# Potential dependencies for extras
|
||||
glob = "0.3.0"
|
@ -5,7 +5,10 @@ use nu_protocol::{
|
||||
Signature, SyntaxShape,
|
||||
};
|
||||
|
||||
use crate::{Alias, Benchmark, BuildString, Def, Do, Each, For, If, Length, Let, LetEnv};
|
||||
use crate::{
|
||||
where_::Where, Alias, Benchmark, BuildString, Def, Do, Each, For, If, Length, Let, LetEnv, Ls,
|
||||
Table,
|
||||
};
|
||||
|
||||
pub fn create_default_context() -> Rc<RefCell<EngineState>> {
|
||||
let engine_state = Rc::new(RefCell::new(EngineState::new()));
|
||||
@ -33,12 +36,18 @@ pub fn create_default_context() -> Rc<RefCell<EngineState>> {
|
||||
|
||||
working_set.add_decl(Box::new(Each));
|
||||
|
||||
working_set.add_decl(Box::new(Where));
|
||||
|
||||
working_set.add_decl(Box::new(Do));
|
||||
|
||||
working_set.add_decl(Box::new(Benchmark));
|
||||
|
||||
working_set.add_decl(Box::new(Length));
|
||||
|
||||
working_set.add_decl(Box::new(Ls));
|
||||
|
||||
working_set.add_decl(Box::new(Table));
|
||||
|
||||
let sig = Signature::build("exit");
|
||||
working_set.add_decl(sig.predeclare());
|
||||
let sig = Signature::build("vars");
|
||||
|
@ -11,7 +11,7 @@ impl Command for If {
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Create a variable and give it a value."
|
||||
"Conditionally run a block."
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
|
@ -10,6 +10,9 @@ mod if_;
|
||||
mod length;
|
||||
mod let_;
|
||||
mod let_env;
|
||||
mod ls;
|
||||
mod table;
|
||||
mod where_;
|
||||
|
||||
pub use alias::Alias;
|
||||
pub use benchmark::Benchmark;
|
||||
@ -23,3 +26,5 @@ pub use if_::If;
|
||||
pub use length::Length;
|
||||
pub use let_::Let;
|
||||
pub use let_env::LetEnv;
|
||||
pub use ls::Ls;
|
||||
pub use table::Table;
|
||||
|
93
crates/nu-command/src/ls.rs
Normal file
93
crates/nu-command/src/ls.rs
Normal file
@ -0,0 +1,93 @@
|
||||
use nu_engine::eval_expression;
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EvaluationContext};
|
||||
use nu_protocol::{IntoValueStream, Signature, SyntaxShape, Value};
|
||||
|
||||
pub struct Ls;
|
||||
|
||||
//NOTE: this is not a real implementation :D. It's just a simple one to test with until we port the real one.
|
||||
impl Command for Ls {
|
||||
fn name(&self) -> &str {
|
||||
"ls"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"List the files in a directory."
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("ls").optional(
|
||||
"pattern",
|
||||
SyntaxShape::GlobPattern,
|
||||
"the glob pattern to use",
|
||||
)
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
context: &EvaluationContext,
|
||||
call: &Call,
|
||||
_input: Value,
|
||||
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
|
||||
let pattern = if let Some(expr) = call.positional.get(0) {
|
||||
let result = eval_expression(context, expr)?;
|
||||
result.as_string()?
|
||||
} else {
|
||||
"*".into()
|
||||
};
|
||||
|
||||
let call_span = call.head;
|
||||
let glob = glob::glob(&pattern).unwrap();
|
||||
|
||||
Ok(Value::Stream {
|
||||
stream: glob
|
||||
.into_iter()
|
||||
.map(move |x| match x {
|
||||
Ok(path) => match std::fs::symlink_metadata(&path) {
|
||||
Ok(metadata) => {
|
||||
let is_file = metadata.is_file();
|
||||
let is_dir = metadata.is_dir();
|
||||
let filesize = metadata.len();
|
||||
|
||||
Value::Record {
|
||||
cols: vec!["name".into(), "type".into(), "size".into()],
|
||||
vals: vec![
|
||||
Value::String {
|
||||
val: path.to_string_lossy().to_string(),
|
||||
span: call_span,
|
||||
},
|
||||
if is_file {
|
||||
Value::string("file", call_span)
|
||||
} else if is_dir {
|
||||
Value::string("dir", call_span)
|
||||
} else {
|
||||
Value::Nothing { span: call_span }
|
||||
},
|
||||
Value::Int {
|
||||
val: filesize as i64,
|
||||
span: call_span,
|
||||
},
|
||||
],
|
||||
span: call_span,
|
||||
}
|
||||
}
|
||||
Err(_) => Value::Record {
|
||||
cols: vec!["name".into(), "type".into(), "size".into()],
|
||||
vals: vec![
|
||||
Value::String {
|
||||
val: path.to_string_lossy().to_string(),
|
||||
span: call_span,
|
||||
},
|
||||
Value::Nothing { span: call_span },
|
||||
Value::Nothing { span: call_span },
|
||||
],
|
||||
span: call_span,
|
||||
},
|
||||
},
|
||||
_ => Value::Nothing { span: call_span },
|
||||
})
|
||||
.into_value_stream(),
|
||||
span: call_span,
|
||||
})
|
||||
}
|
||||
}
|
125
crates/nu-command/src/table.rs
Normal file
125
crates/nu-command/src/table.rs
Normal file
@ -0,0 +1,125 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use nu_protocol::ast::{Call, PathMember};
|
||||
use nu_protocol::engine::{Command, EvaluationContext};
|
||||
use nu_protocol::{Signature, Span, Value};
|
||||
use nu_table::StyledString;
|
||||
|
||||
pub struct Table;
|
||||
|
||||
//NOTE: this is not a real implementation :D. It's just a simple one to test with until we port the real one.
|
||||
impl Command for Table {
|
||||
fn name(&self) -> &str {
|
||||
"table"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Render the table."
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("table")
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
_context: &EvaluationContext,
|
||||
call: &Call,
|
||||
input: Value,
|
||||
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
|
||||
match input {
|
||||
Value::List { vals, .. } => {
|
||||
let table = convert_to_table(vals);
|
||||
|
||||
if let Some(table) = table {
|
||||
let result = nu_table::draw_table(&table, 80, &HashMap::new());
|
||||
|
||||
Ok(Value::String {
|
||||
val: result,
|
||||
span: call.head,
|
||||
})
|
||||
} else {
|
||||
Ok(Value::Nothing { span: call.head })
|
||||
}
|
||||
}
|
||||
Value::Stream { stream, .. } => {
|
||||
let table = convert_to_table(stream);
|
||||
|
||||
if let Some(table) = table {
|
||||
let result = nu_table::draw_table(&table, 80, &HashMap::new());
|
||||
|
||||
Ok(Value::String {
|
||||
val: result,
|
||||
span: call.head,
|
||||
})
|
||||
} else {
|
||||
Ok(Value::Nothing { span: call.head })
|
||||
}
|
||||
}
|
||||
x => Ok(x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_to_table(iter: impl IntoIterator<Item = Value>) -> Option<nu_table::Table> {
|
||||
let mut iter = iter.into_iter().peekable();
|
||||
|
||||
if let Some(first) = iter.peek() {
|
||||
let mut headers = first.columns();
|
||||
headers.insert(0, "#".into());
|
||||
|
||||
let mut data = vec![];
|
||||
|
||||
for (row_num, item) in iter.enumerate() {
|
||||
let mut row = vec![row_num.to_string()];
|
||||
|
||||
for header in headers.iter().skip(1) {
|
||||
let result = item.clone().follow_cell_path(&[PathMember::String {
|
||||
val: header.into(),
|
||||
span: Span::unknown(),
|
||||
}]);
|
||||
|
||||
match result {
|
||||
Ok(value) => row.push(value.into_string()),
|
||||
Err(_) => row.push(String::new()),
|
||||
}
|
||||
}
|
||||
|
||||
data.push(row);
|
||||
}
|
||||
|
||||
Some(nu_table::Table {
|
||||
headers: headers
|
||||
.into_iter()
|
||||
.map(|x| StyledString {
|
||||
contents: x,
|
||||
style: nu_table::TextStyle::default_header(),
|
||||
})
|
||||
.collect(),
|
||||
data: data
|
||||
.into_iter()
|
||||
.map(|x| {
|
||||
x.into_iter()
|
||||
.enumerate()
|
||||
.map(|(col, y)| {
|
||||
if col == 0 {
|
||||
StyledString {
|
||||
contents: y,
|
||||
style: nu_table::TextStyle::default_header(),
|
||||
}
|
||||
} else {
|
||||
StyledString {
|
||||
contents: y,
|
||||
style: nu_table::TextStyle::basic_left(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<StyledString>>()
|
||||
})
|
||||
.collect(),
|
||||
theme: nu_table::Theme::rounded(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
92
crates/nu-command/src/where_.rs
Normal file
92
crates/nu-command/src/where_.rs
Normal file
@ -0,0 +1,92 @@
|
||||
use nu_engine::eval_expression;
|
||||
use nu_protocol::ast::{Call, Expr, Expression};
|
||||
use nu_protocol::engine::{Command, EvaluationContext};
|
||||
use nu_protocol::{IntoValueStream, ShellError, Signature, SyntaxShape, Value};
|
||||
|
||||
pub struct Where;
|
||||
|
||||
impl Command for Where {
|
||||
fn name(&self) -> &str {
|
||||
"where"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Filter values based on a condition."
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("where").required("cond", SyntaxShape::RowCondition, "condition")
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
context: &EvaluationContext,
|
||||
call: &Call,
|
||||
input: Value,
|
||||
) -> Result<nu_protocol::Value, nu_protocol::ShellError> {
|
||||
let cond = call.positional[0].clone();
|
||||
|
||||
let context = context.enter_scope();
|
||||
|
||||
let (var_id, cond) = match cond {
|
||||
Expression {
|
||||
expr: Expr::RowCondition(var_id, expr),
|
||||
..
|
||||
} => (var_id, expr),
|
||||
_ => return Err(ShellError::InternalError("Expected row condition".into())),
|
||||
};
|
||||
|
||||
match input {
|
||||
Value::Stream { stream, span } => {
|
||||
let output_stream = stream
|
||||
.filter(move |value| {
|
||||
context.add_var(var_id, value.clone());
|
||||
|
||||
let result = eval_expression(&context, &cond);
|
||||
|
||||
match result {
|
||||
Ok(result) => result.is_true(),
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
.into_value_stream();
|
||||
|
||||
Ok(Value::Stream {
|
||||
stream: output_stream,
|
||||
span,
|
||||
})
|
||||
}
|
||||
Value::List { vals, span } => {
|
||||
let output_stream = vals
|
||||
.into_iter()
|
||||
.filter(move |value| {
|
||||
context.add_var(var_id, value.clone());
|
||||
|
||||
let result = eval_expression(&context, &cond);
|
||||
|
||||
match result {
|
||||
Ok(result) => result.is_true(),
|
||||
_ => false,
|
||||
}
|
||||
})
|
||||
.into_value_stream();
|
||||
|
||||
Ok(Value::Stream {
|
||||
stream: output_stream,
|
||||
span,
|
||||
})
|
||||
}
|
||||
x => {
|
||||
context.add_var(var_id, x.clone());
|
||||
|
||||
let result = eval_expression(&context, &cond)?;
|
||||
|
||||
if result.is_true() {
|
||||
Ok(x)
|
||||
} else {
|
||||
Ok(Value::Nothing { span: call.head })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -135,6 +135,7 @@ pub fn eval_expression(
|
||||
|
||||
value.follow_cell_path(&column_path.tail)
|
||||
}
|
||||
Expr::RowCondition(_, expr) => eval_expression(context, expr),
|
||||
Expr::Call(call) => eval_call(context, call, Value::nothing()),
|
||||
Expr::ExternalCall(_, _) => Err(ShellError::ExternalNotSupported(expr.span)),
|
||||
Expr::Operator(_) => Ok(Value::Nothing { span: expr.span }),
|
||||
|
@ -114,6 +114,7 @@ pub fn flatten_expression(
|
||||
Expr::String(_) => {
|
||||
vec![(expr.span, FlatShape::String)]
|
||||
}
|
||||
Expr::RowCondition(_, expr) => flatten_expression(working_set, expr),
|
||||
Expr::Subexpression(block_id) => {
|
||||
flatten_block(working_set, working_set.get_block(*block_id))
|
||||
}
|
||||
|
@ -871,7 +871,7 @@ pub(crate) fn parse_dollar_expr(
|
||||
} else if let (expr, None) = parse_range(working_set, span) {
|
||||
(expr, None)
|
||||
} else {
|
||||
parse_full_column_path(working_set, span)
|
||||
parse_full_column_path(working_set, None, span)
|
||||
}
|
||||
}
|
||||
|
||||
@ -942,7 +942,7 @@ pub fn parse_string_interpolation(
|
||||
end: b + 1,
|
||||
};
|
||||
|
||||
let (expr, err) = parse_full_column_path(working_set, span);
|
||||
let (expr, err) = parse_full_column_path(working_set, None, span);
|
||||
error = error.or(err);
|
||||
output.push(expr);
|
||||
}
|
||||
@ -977,7 +977,7 @@ pub fn parse_string_interpolation(
|
||||
end,
|
||||
};
|
||||
|
||||
let (expr, err) = parse_full_column_path(working_set, span);
|
||||
let (expr, err) = parse_full_column_path(working_set, None, span);
|
||||
error = error.or(err);
|
||||
output.push(expr);
|
||||
}
|
||||
@ -1067,6 +1067,7 @@ pub fn parse_variable_expr(
|
||||
|
||||
pub fn parse_full_column_path(
|
||||
working_set: &mut StateWorkingSet,
|
||||
implicit_head: Option<VarId>,
|
||||
span: Span,
|
||||
) -> (Expression, Option<ParseError>) {
|
||||
// FIXME: assume for now a paren expr, but needs more
|
||||
@ -1077,10 +1078,10 @@ pub fn parse_full_column_path(
|
||||
let (tokens, err) = lex(source, span.start, &[b'\n'], &[b'.']);
|
||||
error = error.or(err);
|
||||
|
||||
let mut tokens = tokens.into_iter();
|
||||
if let Some(head) = tokens.next() {
|
||||
let mut tokens = tokens.into_iter().peekable();
|
||||
if let Some(head) = tokens.peek() {
|
||||
let bytes = working_set.get_span_contents(head.span);
|
||||
let head = if bytes.starts_with(b"(") {
|
||||
let (head, mut expect_dot) = if bytes.starts_with(b"(") {
|
||||
let mut start = head.span.start;
|
||||
let mut end = head.span.end;
|
||||
|
||||
@ -1105,27 +1106,42 @@ pub fn parse_full_column_path(
|
||||
|
||||
let source = working_set.get_span_contents(span);
|
||||
|
||||
let (tokens, err) = lex(source, span.start, &[b'\n'], &[]);
|
||||
let (output, err) = lex(source, span.start, &[b'\n'], &[]);
|
||||
error = error.or(err);
|
||||
|
||||
let (output, err) = lite_parse(&tokens);
|
||||
let (output, err) = lite_parse(&output);
|
||||
error = error.or(err);
|
||||
|
||||
let (output, err) = parse_block(working_set, &output, true);
|
||||
error = error.or(err);
|
||||
|
||||
let block_id = working_set.add_block(output);
|
||||
tokens.next();
|
||||
|
||||
(
|
||||
Expression {
|
||||
expr: Expr::Subexpression(block_id),
|
||||
span,
|
||||
ty: Type::Unknown, // FIXME
|
||||
}
|
||||
},
|
||||
true,
|
||||
)
|
||||
} else if bytes.starts_with(b"$") {
|
||||
let (out, err) = parse_variable_expr(working_set, head.span);
|
||||
error = error.or(err);
|
||||
|
||||
out
|
||||
tokens.next();
|
||||
|
||||
(out, true)
|
||||
} else if let Some(var_id) = implicit_head {
|
||||
(
|
||||
Expression {
|
||||
expr: Expr::Var(var_id),
|
||||
span: Span::unknown(),
|
||||
ty: Type::Unknown,
|
||||
},
|
||||
false,
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
garbage(span),
|
||||
@ -1139,7 +1155,6 @@ pub fn parse_full_column_path(
|
||||
|
||||
let mut tail = vec![];
|
||||
|
||||
let mut expect_dot = true;
|
||||
for path_element in tokens {
|
||||
let bytes = working_set.get_span_contents(path_element.span);
|
||||
|
||||
@ -1313,11 +1328,40 @@ pub fn parse_var_with_opt_type(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_to_cell_path(
|
||||
working_set: &mut StateWorkingSet,
|
||||
expression: &mut Expression,
|
||||
var_id: VarId,
|
||||
) {
|
||||
if let Expression {
|
||||
expr: Expr::String(_),
|
||||
span,
|
||||
..
|
||||
} = expression
|
||||
{
|
||||
// Re-parse the string as if it were a cell-path
|
||||
let (new_expression, _err) = parse_full_column_path(working_set, Some(var_id), *span);
|
||||
|
||||
*expression = new_expression;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_row_condition(
|
||||
working_set: &mut StateWorkingSet,
|
||||
spans: &[Span],
|
||||
) -> (Expression, Option<ParseError>) {
|
||||
parse_math_expression(working_set, spans)
|
||||
let var_id = working_set.add_variable(b"$it".to_vec(), Type::Unknown);
|
||||
let (expression, err) = parse_math_expression(working_set, spans, Some(var_id));
|
||||
let span = span(spans);
|
||||
(
|
||||
Expression {
|
||||
ty: Type::Bool,
|
||||
span,
|
||||
expr: Expr::RowCondition(var_id, Box::new(expression)),
|
||||
},
|
||||
err,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn parse_signature(
|
||||
@ -2015,7 +2059,7 @@ pub fn parse_value(
|
||||
if let (expr, None) = parse_range(working_set, span) {
|
||||
return (expr, None);
|
||||
} else {
|
||||
return parse_full_column_path(working_set, span);
|
||||
return parse_full_column_path(working_set, None, span);
|
||||
}
|
||||
} else if bytes.starts_with(b"{") {
|
||||
if matches!(shape, SyntaxShape::Block) || matches!(shape, SyntaxShape::Any) {
|
||||
@ -2162,6 +2206,7 @@ pub fn parse_operator(
|
||||
pub fn parse_math_expression(
|
||||
working_set: &mut StateWorkingSet,
|
||||
spans: &[Span],
|
||||
lhs_row_var_id: Option<VarId>,
|
||||
) -> (Expression, Option<ParseError>) {
|
||||
// As the expr_stack grows, we increase the required precedence to grow larger
|
||||
// If, at any time, the operator we're looking at is the same or lower precedence
|
||||
@ -2220,6 +2265,10 @@ pub fn parse_math_expression(
|
||||
.pop()
|
||||
.expect("internal error: expression stack empty");
|
||||
|
||||
if let Some(row_var_id) = lhs_row_var_id {
|
||||
expand_to_cell_path(working_set, &mut lhs, row_var_id);
|
||||
}
|
||||
|
||||
let (result_ty, err) = math_result_type(working_set, &mut lhs, &mut op, &mut rhs);
|
||||
error = error.or(err);
|
||||
|
||||
@ -2250,6 +2299,10 @@ pub fn parse_math_expression(
|
||||
.pop()
|
||||
.expect("internal error: expression stack empty");
|
||||
|
||||
if let Some(row_var_id) = lhs_row_var_id {
|
||||
expand_to_cell_path(working_set, &mut lhs, row_var_id);
|
||||
}
|
||||
|
||||
let (result_ty, err) = math_result_type(working_set, &mut lhs, &mut op, &mut rhs);
|
||||
error = error.or(err);
|
||||
|
||||
@ -2276,7 +2329,7 @@ pub fn parse_expression(
|
||||
|
||||
match bytes[0] {
|
||||
b'0' | b'1' | b'2' | b'3' | b'4' | b'5' | b'6' | b'7' | b'8' | b'9' | b'(' | b'{'
|
||||
| b'[' | b'$' | b'"' | b'\'' | b'-' => parse_math_expression(working_set, spans),
|
||||
| b'[' | b'$' | b'"' | b'\'' | b'-' => parse_math_expression(working_set, spans, None),
|
||||
_ => parse_call(working_set, spans, true),
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ pub enum Expr {
|
||||
Call(Box<Call>),
|
||||
ExternalCall(Vec<u8>, Vec<Vec<u8>>),
|
||||
Operator(Operator),
|
||||
RowCondition(VarId, Box<Expression>),
|
||||
BinaryOp(Box<Expression>, Box<Expression>, Box<Expression>), //lhs, op, rhs
|
||||
Subexpression(BlockId),
|
||||
Block(BlockId),
|
||||
|
@ -123,6 +123,24 @@ impl EngineState {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn find_commands_by_prefix(&self, name: &[u8]) -> Vec<Vec<u8>> {
|
||||
let mut output = vec![];
|
||||
|
||||
for scope in self.scope.iter().rev() {
|
||||
for decl in &scope.decls {
|
||||
if decl.0.starts_with(name) {
|
||||
output.push(decl.0.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn get_span_contents(&self, span: Span) -> &[u8] {
|
||||
&self.file_contents[span.start..span.end]
|
||||
}
|
||||
|
||||
pub fn get_var(&self, var_id: VarId) -> &Type {
|
||||
self.vars
|
||||
.get(var_id)
|
||||
@ -496,6 +514,24 @@ impl<'a> StateWorkingSet<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_commands_by_prefix(&self, name: &[u8]) -> Vec<Vec<u8>> {
|
||||
let mut output = vec![];
|
||||
|
||||
for scope in self.delta.scope.iter().rev() {
|
||||
for decl in &scope.decls {
|
||||
if decl.0.starts_with(name) {
|
||||
output.push(decl.0.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut permanent = self.permanent_state.find_commands_by_prefix(name);
|
||||
|
||||
output.append(&mut permanent);
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn get_block(&self, block_id: BlockId) -> &Block {
|
||||
let num_permanent_blocks = self.permanent_state.num_blocks();
|
||||
if block_id < num_permanent_blocks {
|
||||
|
@ -277,6 +277,24 @@ impl Value {
|
||||
|
||||
Ok(current)
|
||||
}
|
||||
|
||||
pub fn string(s: &str, span: Span) -> Value {
|
||||
Value::String {
|
||||
val: s.into(),
|
||||
span,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_true(&self) -> bool {
|
||||
matches!(self, Value::Bool { val: true, .. })
|
||||
}
|
||||
|
||||
pub fn columns(&self) -> Vec<String> {
|
||||
match self {
|
||||
Value::Record { cols, .. } => cols.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
|
22
crates/nu-table/.gitignore
vendored
Normal file
22
crates/nu-table/.gitignore
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
/target
|
||||
/scratch
|
||||
**/*.rs.bk
|
||||
history.txt
|
||||
tests/fixtures/nuplayground
|
||||
crates/*/target
|
||||
|
||||
# Debian/Ubuntu
|
||||
debian/.debhelper/
|
||||
debian/debhelper-build-stamp
|
||||
debian/files
|
||||
debian/nu.substvars
|
||||
debian/nu/
|
||||
|
||||
# macOS junk
|
||||
.DS_Store
|
||||
|
||||
# JetBrains' IDE items
|
||||
.idea/*
|
||||
|
||||
# VSCode's IDE items
|
||||
.vscode/*
|
18
crates/nu-table/Cargo.toml
Normal file
18
crates/nu-table/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[package]
|
||||
authors = ["The Nu Project Contributors"]
|
||||
description = "Nushell table printing"
|
||||
edition = "2018"
|
||||
license = "MIT"
|
||||
name = "nu-table"
|
||||
version = "0.36.0"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[[bin]]
|
||||
name = "table"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
nu-ansi-term = "0.36.0"
|
||||
|
||||
regex = "1.4"
|
||||
unicode-width = "0.1.8"
|
5
crates/nu-table/src/lib.rs
Normal file
5
crates/nu-table/src/lib.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod table;
|
||||
mod wrap;
|
||||
|
||||
pub use table::{draw_table, StyledString, Table, TextStyle, Theme};
|
||||
pub use wrap::Alignment;
|
86
crates/nu-table/src/main.rs
Normal file
86
crates/nu-table/src/main.rs
Normal file
@ -0,0 +1,86 @@
|
||||
use nu_table::{draw_table, StyledString, Table, TextStyle, Theme};
|
||||
use std::collections::HashMap;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<_> = std::env::args().collect();
|
||||
let mut width = 0;
|
||||
|
||||
if args.len() > 1 {
|
||||
// Width in terminal characters
|
||||
width = args[1].parse::<usize>().expect("Need a width in columns");
|
||||
}
|
||||
|
||||
if width < 4 {
|
||||
println!("Width must be greater than or equal to 4, setting width to 80");
|
||||
width = 80;
|
||||
}
|
||||
|
||||
// The mocked up table data
|
||||
let (table_headers, row_data) = make_table_data();
|
||||
// The table headers
|
||||
let headers = vec_of_str_to_vec_of_styledstr(&table_headers, true);
|
||||
// The table rows
|
||||
let rows = vec_of_str_to_vec_of_styledstr(&row_data, false);
|
||||
// The table itself
|
||||
let table = Table::new(headers, vec![rows; 3], Theme::rounded());
|
||||
// FIXME: Config isn't available from here so just put these here to compile
|
||||
let color_hm: HashMap<String, nu_ansi_term::Style> = HashMap::new();
|
||||
// Capture the table as a string
|
||||
let output_table = draw_table(&table, width, &color_hm);
|
||||
// Draw the table
|
||||
println!("{}", output_table)
|
||||
}
|
||||
|
||||
fn make_table_data() -> (Vec<&'static str>, Vec<&'static str>) {
|
||||
let table_headers = vec![
|
||||
"category",
|
||||
"description",
|
||||
"emoji",
|
||||
"ios_version",
|
||||
"unicode_version",
|
||||
"aliases",
|
||||
"tags",
|
||||
"category2",
|
||||
"description2",
|
||||
"emoji2",
|
||||
"ios_version2",
|
||||
"unicode_version2",
|
||||
"aliases2",
|
||||
"tags2",
|
||||
];
|
||||
|
||||
let row_data = vec![
|
||||
"Smileys & Emotion",
|
||||
"grinning face",
|
||||
"😀",
|
||||
"6",
|
||||
"6.1",
|
||||
"grinning",
|
||||
"smile",
|
||||
"Smileys & Emotion",
|
||||
"grinning face",
|
||||
"😀",
|
||||
"6",
|
||||
"6.1",
|
||||
"grinning",
|
||||
"smile",
|
||||
];
|
||||
|
||||
(table_headers, row_data)
|
||||
}
|
||||
|
||||
fn vec_of_str_to_vec_of_styledstr(data: &[&str], is_header: bool) -> Vec<StyledString> {
|
||||
let mut v = vec![];
|
||||
|
||||
for x in data {
|
||||
if is_header {
|
||||
v.push(StyledString::new(
|
||||
String::from(*x),
|
||||
TextStyle::default_header(),
|
||||
))
|
||||
} else {
|
||||
v.push(StyledString::new(String::from(*x), TextStyle::basic_left()))
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
1259
crates/nu-table/src/table.rs
Normal file
1259
crates/nu-table/src/table.rs
Normal file
File diff suppressed because it is too large
Load Diff
274
crates/nu-table/src/wrap.rs
Normal file
274
crates/nu-table/src/wrap.rs
Normal file
@ -0,0 +1,274 @@
|
||||
use crate::table::TextStyle;
|
||||
use nu_ansi_term::Style;
|
||||
use std::collections::HashMap;
|
||||
use std::{fmt::Display, iter::Iterator};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Subline<'a> {
|
||||
pub subline: &'a str,
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Line<'a> {
|
||||
pub sublines: Vec<Subline<'a>>,
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WrappedLine {
|
||||
pub line: String,
|
||||
pub width: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WrappedCell {
|
||||
pub lines: Vec<WrappedLine>,
|
||||
pub max_width: usize,
|
||||
|
||||
pub style: TextStyle,
|
||||
}
|
||||
|
||||
impl<'a> Display for Line<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut first = true;
|
||||
for subline in &self.sublines {
|
||||
if !first {
|
||||
write!(f, " ")?;
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
write!(f, "{}", subline.subline)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn split_sublines(input: &str) -> Vec<Vec<Subline>> {
|
||||
input
|
||||
.split_terminator('\n')
|
||||
.map(|line| {
|
||||
line.split_terminator(' ')
|
||||
.map(|x| Subline {
|
||||
subline: x,
|
||||
width: {
|
||||
// We've tried UnicodeWidthStr::width(x), UnicodeSegmentation::graphemes(x, true).count()
|
||||
// and x.chars().count() with all types of combinations. Currently, it appears that
|
||||
// getting the max of char count and Unicode width seems to produce the best layout.
|
||||
// However, it's not perfect.
|
||||
let c = x.chars().count();
|
||||
let u = UnicodeWidthStr::width(x);
|
||||
std::cmp::max(c, u)
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn column_width(input: &[Vec<Subline>]) -> usize {
|
||||
let mut max = 0;
|
||||
|
||||
for line in input {
|
||||
let mut total = 0;
|
||||
|
||||
let mut first = true;
|
||||
for inp in line {
|
||||
if !first {
|
||||
// Account for the space
|
||||
total += 1;
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
total += inp.width;
|
||||
}
|
||||
|
||||
if total > max {
|
||||
max = total;
|
||||
}
|
||||
}
|
||||
|
||||
max
|
||||
}
|
||||
|
||||
fn split_word(cell_width: usize, word: &str) -> Vec<Subline> {
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
let mut output = vec![];
|
||||
let mut current_width = 0;
|
||||
let mut start_index = 0;
|
||||
let mut end_index;
|
||||
|
||||
for c in word.char_indices() {
|
||||
if let Some(width) = c.1.width() {
|
||||
end_index = c.0;
|
||||
if current_width + width > cell_width {
|
||||
output.push(Subline {
|
||||
subline: &word[start_index..end_index],
|
||||
width: current_width,
|
||||
});
|
||||
|
||||
start_index = c.0;
|
||||
current_width = width;
|
||||
} else {
|
||||
current_width += width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if start_index != word.len() {
|
||||
output.push(Subline {
|
||||
subline: &word[start_index..],
|
||||
width: current_width,
|
||||
});
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
pub fn wrap<'a>(
|
||||
cell_width: usize,
|
||||
mut input: impl Iterator<Item = Subline<'a>>,
|
||||
color_hm: &HashMap<String, Style>,
|
||||
re_leading: ®ex::Regex,
|
||||
re_trailing: ®ex::Regex,
|
||||
) -> (Vec<WrappedLine>, usize) {
|
||||
let mut lines = vec![];
|
||||
let mut current_line: Vec<Subline> = vec![];
|
||||
let mut current_width = 0;
|
||||
let mut first = true;
|
||||
let mut max_width = 0;
|
||||
let lead_trail_space_bg_color = color_hm
|
||||
.get("leading_trailing_space_bg")
|
||||
.unwrap_or(&Style::default())
|
||||
.to_owned();
|
||||
|
||||
loop {
|
||||
match input.next() {
|
||||
Some(item) => {
|
||||
if !first {
|
||||
current_width += 1;
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
|
||||
if item.width + current_width > cell_width {
|
||||
// If this is a really long single word, we need to split the word
|
||||
if current_line.len() == 1 && current_width > cell_width {
|
||||
max_width = cell_width;
|
||||
let sublines = split_word(cell_width, current_line[0].subline);
|
||||
for subline in sublines {
|
||||
let width = subline.width;
|
||||
lines.push(Line {
|
||||
sublines: vec![subline],
|
||||
width,
|
||||
});
|
||||
}
|
||||
|
||||
first = true;
|
||||
|
||||
current_width = item.width;
|
||||
current_line = vec![item];
|
||||
} else {
|
||||
if !current_line.is_empty() {
|
||||
lines.push(Line {
|
||||
sublines: current_line,
|
||||
width: current_width,
|
||||
});
|
||||
}
|
||||
|
||||
first = true;
|
||||
|
||||
current_width = item.width;
|
||||
current_line = vec![item];
|
||||
max_width = std::cmp::max(max_width, current_width);
|
||||
}
|
||||
} else {
|
||||
current_width += item.width;
|
||||
current_line.push(item);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
if current_width > cell_width {
|
||||
// We need to break up the last word
|
||||
let sublines = split_word(cell_width, current_line[0].subline);
|
||||
for subline in sublines {
|
||||
let width = subline.width;
|
||||
lines.push(Line {
|
||||
sublines: vec![subline],
|
||||
width,
|
||||
});
|
||||
}
|
||||
} else if current_width > 0 {
|
||||
lines.push(Line {
|
||||
sublines: current_line,
|
||||
width: current_width,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut current_max = 0;
|
||||
let mut output = vec![];
|
||||
|
||||
for line in lines {
|
||||
let mut current_line_width = 0;
|
||||
let mut first = true;
|
||||
let mut current_line = String::new();
|
||||
|
||||
for subline in line.sublines {
|
||||
if !first {
|
||||
current_line_width += 1 + subline.width;
|
||||
current_line.push(' ');
|
||||
} else {
|
||||
first = false;
|
||||
current_line_width = subline.width;
|
||||
}
|
||||
current_line.push_str(subline.subline);
|
||||
}
|
||||
|
||||
if current_line_width > current_max {
|
||||
current_max = current_line_width;
|
||||
}
|
||||
|
||||
// highlight leading and trailing spaces so they stand out.
|
||||
let mut bg_color_string = Style::default().prefix().to_string();
|
||||
// right now config settings can only set foreground colors so, in this
|
||||
// instance we take the foreground color and make it a background color
|
||||
if let Some(bg) = lead_trail_space_bg_color.foreground {
|
||||
bg_color_string = Style::default().on(bg).prefix().to_string()
|
||||
};
|
||||
|
||||
if let Some(leading_match) = re_leading.find(¤t_line.clone()) {
|
||||
String::insert_str(
|
||||
&mut current_line,
|
||||
leading_match.end(),
|
||||
nu_ansi_term::ansi::RESET,
|
||||
);
|
||||
String::insert_str(&mut current_line, leading_match.start(), &bg_color_string);
|
||||
}
|
||||
|
||||
if let Some(trailing_match) = re_trailing.find(¤t_line.clone()) {
|
||||
String::insert_str(&mut current_line, trailing_match.start(), &bg_color_string);
|
||||
current_line += nu_ansi_term::ansi::RESET;
|
||||
}
|
||||
|
||||
output.push(WrappedLine {
|
||||
line: current_line,
|
||||
width: current_line_width,
|
||||
});
|
||||
}
|
||||
|
||||
(output, current_max)
|
||||
}
|
10
src/main.rs
10
src/main.rs
@ -1,4 +1,4 @@
|
||||
use nu_cli::{report_parsing_error, report_shell_error, NuHighlighter};
|
||||
use nu_cli::{report_parsing_error, report_shell_error, NuCompleter, NuHighlighter};
|
||||
use nu_command::create_default_context;
|
||||
use nu_engine::eval_block;
|
||||
use nu_parser::parse;
|
||||
@ -6,6 +6,7 @@ use nu_protocol::{
|
||||
engine::{EngineState, EvaluationContext, StateWorkingSet},
|
||||
Value,
|
||||
};
|
||||
use reedline::DefaultCompletionActionHandler;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@ -53,6 +54,8 @@ fn main() -> std::io::Result<()> {
|
||||
} else {
|
||||
use reedline::{DefaultPrompt, FileBackedHistory, Reedline, Signal};
|
||||
|
||||
let completer = NuCompleter::new(engine_state.clone());
|
||||
|
||||
let mut line_editor = Reedline::create()?
|
||||
.with_history(Box::new(FileBackedHistory::with_file(
|
||||
1000,
|
||||
@ -60,7 +63,10 @@ fn main() -> std::io::Result<()> {
|
||||
)?))?
|
||||
.with_highlighter(Box::new(NuHighlighter {
|
||||
engine_state: engine_state.clone(),
|
||||
}));
|
||||
}))
|
||||
.with_completion_action_handler(Box::new(
|
||||
DefaultCompletionActionHandler::default().with_completer(Box::new(completer)),
|
||||
));
|
||||
|
||||
let prompt = DefaultPrompt::new(1);
|
||||
let mut current_line = 1;
|
||||
|
16
src/tests.rs
16
src/tests.rs
@ -292,3 +292,19 @@ fn row_iteration() -> TestResult {
|
||||
fn record_iteration() -> TestResult {
|
||||
run_test("([[name, level]; [aa, 100], [bb, 200]] | each { $it | each { |x| if $x.column == \"level\" { $x.value + 100 } else { $x.value } } }).level", "[200, 300]")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_condition1() -> TestResult {
|
||||
run_test(
|
||||
"([[name, size]; [a, 1], [b, 2], [c, 3]] | where size < 3).name",
|
||||
"[a, b]",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn row_condition2() -> TestResult {
|
||||
run_test(
|
||||
"[[name, size]; [a, 1], [b, 2], [c, 3]] | where $it.size > 2 | length",
|
||||
"1",
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user