mirror of
https://github.com/nushell/nushell.git
synced 2024-11-22 08:23:24 +01:00
Optional members in cell paths: Attempt 2 (#8379)
This is a follow up from https://github.com/nushell/nushell/pull/7540. Please provide feedback if you have the time! ## Summary This PR lets you use `?` to indicate that a member in a cell path is optional and Nushell should return `null` if that member cannot be accessed. Unlike the previous PR, `?` is now a _postfix_ modifier for cell path members. A cell path of `.foo?.bar` means that `foo` is optional and `bar` is not. `?` does _not_ suppress all errors; it is intended to help in situations where data has "holes", i.e. the data types are correct but something is missing. Type mismatches (like trying to do a string path access on a date) will still fail. ### Record Examples ```bash { foo: 123 }.foo # returns 123 { foo: 123 }.bar # errors { foo: 123 }.bar? # returns null { foo: 123 } | get bar # errors { foo: 123 } | get bar? # returns null { foo: 123 }.bar.baz # errors { foo: 123 }.bar?.baz # errors because `baz` is not present on the result from `bar?` { foo: 123 }.bar.baz? # errors { foo: 123 }.bar?.baz? # returns null ``` ### List Examples ``` 〉[{foo: 1} {foo: 2} {}].foo Error: nu:🐚:column_not_found × Cannot find column ╭─[entry #30:1:1] 1 │ [{foo: 1} {foo: 2} {}].foo · ─┬ ─┬─ · │ ╰── cannot find column 'foo' · ╰── value originates here ╰──── 〉[{foo: 1} {foo: 2} {}].foo? ╭───┬───╮ │ 0 │ 1 │ │ 1 │ 2 │ │ 2 │ │ ╰───┴───╯ 〉[{foo: 1} {foo: 2} {}].foo?.2 | describe nothing 〉[a b c].4? | describe nothing 〉[{foo: 1} {foo: 2} {}] | where foo? == 1 ╭───┬─────╮ │ # │ foo │ ├───┼─────┤ │ 0 │ 1 │ ╰───┴─────╯ ``` # Breaking changes 1. Column names with `?` in them now need to be quoted. 2. The `-i`/`--ignore-errors` flag has been removed from `get` and `select` 1. After this PR, most `get` error handling can be done with `?` and/or `try`/`catch`. 4. Cell path accesses like this no longer work without a `?`: ```bash 〉[{a:1 b:2} {a:3}].b.0 2 ``` We had some clever code that was able to recognize that since we only want row `0`, it's OK if other rows are missing column `b`. I removed that because it's tricky to maintain, and now that query needs to be written like: ```bash 〉[{a:1 b:2} {a:3}].b?.0 2 ``` I think the regression is acceptable for now. I plan to do more work in the future to enable streaming of cell path accesses, and when that happens I'll be able to make `.b.0` work again.
This commit is contained in:
parent
d3be5ec750
commit
21b84a6d65
@ -879,12 +879,14 @@ pub fn eval_hook(
|
|||||||
let condition_path = PathMember::String {
|
let condition_path = PathMember::String {
|
||||||
val: "condition".to_string(),
|
val: "condition".to_string(),
|
||||||
span: value_span,
|
span: value_span,
|
||||||
|
optional: false,
|
||||||
};
|
};
|
||||||
let mut output = PipelineData::empty();
|
let mut output = PipelineData::empty();
|
||||||
|
|
||||||
let code_path = PathMember::String {
|
let code_path = PathMember::String {
|
||||||
val: "code".to_string(),
|
val: "code".to_string(),
|
||||||
span: value_span,
|
span: value_span,
|
||||||
|
optional: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
match value {
|
match value {
|
||||||
@ -894,63 +896,60 @@ pub fn eval_hook(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Value::Record { .. } => {
|
Value::Record { .. } => {
|
||||||
let do_run_hook = if let Ok(condition) =
|
let do_run_hook =
|
||||||
value
|
if let Ok(condition) = value.clone().follow_cell_path(&[condition_path], false) {
|
||||||
.clone()
|
match condition {
|
||||||
.follow_cell_path(&[condition_path], false, false)
|
Value::Block {
|
||||||
{
|
val: block_id,
|
||||||
match condition {
|
span: block_span,
|
||||||
Value::Block {
|
..
|
||||||
val: block_id,
|
}
|
||||||
span: block_span,
|
| Value::Closure {
|
||||||
..
|
val: block_id,
|
||||||
}
|
span: block_span,
|
||||||
| Value::Closure {
|
..
|
||||||
val: block_id,
|
} => {
|
||||||
span: block_span,
|
match run_hook_block(
|
||||||
..
|
engine_state,
|
||||||
} => {
|
stack,
|
||||||
match run_hook_block(
|
block_id,
|
||||||
engine_state,
|
None,
|
||||||
stack,
|
arguments.clone(),
|
||||||
block_id,
|
block_span,
|
||||||
None,
|
) {
|
||||||
arguments.clone(),
|
Ok(pipeline_data) => {
|
||||||
block_span,
|
if let PipelineData::Value(Value::Bool { val, .. }, ..) =
|
||||||
) {
|
pipeline_data
|
||||||
Ok(pipeline_data) => {
|
{
|
||||||
if let PipelineData::Value(Value::Bool { val, .. }, ..) =
|
val
|
||||||
pipeline_data
|
} else {
|
||||||
{
|
return Err(ShellError::UnsupportedConfigValue(
|
||||||
val
|
"boolean output".to_string(),
|
||||||
} else {
|
"other PipelineData variant".to_string(),
|
||||||
return Err(ShellError::UnsupportedConfigValue(
|
block_span,
|
||||||
"boolean output".to_string(),
|
));
|
||||||
"other PipelineData variant".to_string(),
|
}
|
||||||
block_span,
|
}
|
||||||
));
|
Err(err) => {
|
||||||
|
return Err(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
}
|
||||||
return Err(err);
|
other => {
|
||||||
}
|
return Err(ShellError::UnsupportedConfigValue(
|
||||||
|
"block".to_string(),
|
||||||
|
format!("{}", other.get_type()),
|
||||||
|
other.span()?,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
other => {
|
} else {
|
||||||
return Err(ShellError::UnsupportedConfigValue(
|
// always run the hook
|
||||||
"block".to_string(),
|
true
|
||||||
format!("{}", other.get_type()),
|
};
|
||||||
other.span()?,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// always run the hook
|
|
||||||
true
|
|
||||||
};
|
|
||||||
|
|
||||||
if do_run_hook {
|
if do_run_hook {
|
||||||
match value.clone().follow_cell_path(&[code_path], false, false)? {
|
match value.clone().follow_cell_path(&[code_path], false)? {
|
||||||
Value::String {
|
Value::String {
|
||||||
val,
|
val,
|
||||||
span: source_span,
|
span: source_span,
|
||||||
|
@ -240,7 +240,11 @@ mod test {
|
|||||||
},
|
},
|
||||||
Value::CellPath {
|
Value::CellPath {
|
||||||
val: CellPath {
|
val: CellPath {
|
||||||
members: vec![PathMember::Int { val: 0, span }],
|
members: vec![PathMember::Int {
|
||||||
|
val: 0,
|
||||||
|
span,
|
||||||
|
optional: false,
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
span,
|
span,
|
||||||
},
|
},
|
||||||
|
@ -208,10 +208,11 @@ mod util {
|
|||||||
let path = PathMember::String {
|
let path = PathMember::String {
|
||||||
val: header.to_owned(),
|
val: header.to_owned(),
|
||||||
span: Span::unknown(),
|
span: Span::unknown(),
|
||||||
|
optional: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
item.clone()
|
item.clone()
|
||||||
.follow_cell_path(&[path], false, false)
|
.follow_cell_path(&[path], false)
|
||||||
.unwrap_or_else(|_| item.clone())
|
.unwrap_or_else(|_| item.clone())
|
||||||
}
|
}
|
||||||
item => item.clone(),
|
item => item.clone(),
|
||||||
|
@ -109,10 +109,7 @@ fn dropcol(
|
|||||||
let mut vals = vec![];
|
let mut vals = vec![];
|
||||||
|
|
||||||
for path in &keep_columns {
|
for path in &keep_columns {
|
||||||
let fetcher =
|
let fetcher = input_val.clone().follow_cell_path(&path.members, false)?;
|
||||||
input_val
|
|
||||||
.clone()
|
|
||||||
.follow_cell_path(&path.members, false, false)?;
|
|
||||||
cols.push(path.into_string());
|
cols.push(path.into_string());
|
||||||
vals.push(fetcher);
|
vals.push(fetcher);
|
||||||
}
|
}
|
||||||
@ -136,10 +133,7 @@ fn dropcol(
|
|||||||
let mut vals = vec![];
|
let mut vals = vec![];
|
||||||
|
|
||||||
for path in &keep_columns {
|
for path in &keep_columns {
|
||||||
let fetcher =
|
let fetcher = input_val.clone().follow_cell_path(&path.members, false)?;
|
||||||
input_val
|
|
||||||
.clone()
|
|
||||||
.follow_cell_path(&path.members, false, false)?;
|
|
||||||
cols.push(path.into_string());
|
cols.push(path.into_string());
|
||||||
vals.push(fetcher);
|
vals.push(fetcher);
|
||||||
}
|
}
|
||||||
@ -155,9 +149,7 @@ fn dropcol(
|
|||||||
let mut vals = vec![];
|
let mut vals = vec![];
|
||||||
|
|
||||||
for cell_path in &keep_columns {
|
for cell_path in &keep_columns {
|
||||||
let result = v
|
let result = v.clone().follow_cell_path(&cell_path.members, false)?;
|
||||||
.clone()
|
|
||||||
.follow_cell_path(&cell_path.members, false, false)?;
|
|
||||||
|
|
||||||
cols.push(cell_path.into_string());
|
cols.push(cell_path.into_string());
|
||||||
vals.push(result);
|
vals.push(result);
|
||||||
|
@ -74,7 +74,7 @@ fn empty(
|
|||||||
for val in input {
|
for val in input {
|
||||||
for column in &columns {
|
for column in &columns {
|
||||||
let val = val.clone();
|
let val = val.clone();
|
||||||
match val.follow_cell_path(&column.members, false, false) {
|
match val.follow_cell_path(&column.members, false) {
|
||||||
Ok(Value::Nothing { .. }) => {}
|
Ok(Value::Nothing { .. }) => {}
|
||||||
Ok(_) => return Ok(Value::boolean(false, head).into_pipeline_data()),
|
Ok(_) => return Ok(Value::boolean(false, head).into_pipeline_data()),
|
||||||
Err(err) => return Err(err),
|
Err(err) => return Err(err),
|
||||||
|
@ -279,7 +279,7 @@ fn flat_value(columns: &[CellPath], item: &Value, _name_tag: Span, all: bool) ->
|
|||||||
if !columns.is_empty() {
|
if !columns.is_empty() {
|
||||||
let cell_path =
|
let cell_path =
|
||||||
column_requested.and_then(|x| match x.members.first() {
|
column_requested.and_then(|x| match x.members.first() {
|
||||||
Some(PathMember::String { val, span: _ }) => Some(val),
|
Some(PathMember::String { val, span: _, .. }) => Some(val),
|
||||||
_ => None,
|
_ => None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -41,11 +41,6 @@ If multiple cell paths are given, this will produce a list of values."#
|
|||||||
"the cell path to the data",
|
"the cell path to the data",
|
||||||
)
|
)
|
||||||
.rest("rest", SyntaxShape::CellPath, "additional cell paths")
|
.rest("rest", SyntaxShape::CellPath, "additional cell paths")
|
||||||
.switch(
|
|
||||||
"ignore-errors",
|
|
||||||
"when there are empty cells, instead of erroring out, replace them with nothing",
|
|
||||||
Some('i'),
|
|
||||||
)
|
|
||||||
.switch(
|
.switch(
|
||||||
"sensitive",
|
"sensitive",
|
||||||
"get path in a case sensitive manner",
|
"get path in a case sensitive manner",
|
||||||
@ -65,13 +60,12 @@ If multiple cell paths are given, this will produce a list of values."#
|
|||||||
let cell_path: CellPath = call.req(engine_state, stack, 0)?;
|
let cell_path: CellPath = call.req(engine_state, stack, 0)?;
|
||||||
let rest: Vec<CellPath> = call.rest(engine_state, stack, 1)?;
|
let rest: Vec<CellPath> = call.rest(engine_state, stack, 1)?;
|
||||||
let sensitive = call.has_flag("sensitive");
|
let sensitive = call.has_flag("sensitive");
|
||||||
let ignore_errors = call.has_flag("ignore-errors");
|
|
||||||
let ctrlc = engine_state.ctrlc.clone();
|
let ctrlc = engine_state.ctrlc.clone();
|
||||||
let metadata = input.metadata();
|
let metadata = input.metadata();
|
||||||
|
|
||||||
if rest.is_empty() {
|
if rest.is_empty() {
|
||||||
input
|
input
|
||||||
.follow_cell_path(&cell_path.members, call.head, !sensitive, ignore_errors)
|
.follow_cell_path(&cell_path.members, call.head, !sensitive)
|
||||||
.map(|x| x.into_pipeline_data())
|
.map(|x| x.into_pipeline_data())
|
||||||
} else {
|
} else {
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
@ -81,9 +75,7 @@ If multiple cell paths are given, this will produce a list of values."#
|
|||||||
let input = input.into_value(span);
|
let input = input.into_value(span);
|
||||||
|
|
||||||
for path in paths {
|
for path in paths {
|
||||||
let val = input
|
let val = input.clone().follow_cell_path(&path.members, !sensitive);
|
||||||
.clone()
|
|
||||||
.follow_cell_path(&path.members, !sensitive, false);
|
|
||||||
|
|
||||||
output.push(val?);
|
output.push(val?);
|
||||||
}
|
}
|
||||||
|
@ -22,11 +22,6 @@ impl Command for Select {
|
|||||||
(Type::Record(vec![]), Type::Record(vec![])),
|
(Type::Record(vec![]), Type::Record(vec![])),
|
||||||
(Type::Table(vec![]), Type::Table(vec![])),
|
(Type::Table(vec![]), Type::Table(vec![])),
|
||||||
])
|
])
|
||||||
.switch(
|
|
||||||
"ignore-errors",
|
|
||||||
"when an error occurs, instead of erroring out, suppress the error message",
|
|
||||||
Some('i'),
|
|
||||||
)
|
|
||||||
.rest(
|
.rest(
|
||||||
"rest",
|
"rest",
|
||||||
SyntaxShape::CellPath,
|
SyntaxShape::CellPath,
|
||||||
@ -58,9 +53,8 @@ produce a table, a list will produce a list, and a record will produce a record.
|
|||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let columns: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
|
let columns: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
|
||||||
let span = call.head;
|
let span = call.head;
|
||||||
let ignore_errors = call.has_flag("ignore-errors");
|
|
||||||
|
|
||||||
select(engine_state, span, columns, input, ignore_errors)
|
select(engine_state, span, columns, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
@ -97,7 +91,6 @@ fn select(
|
|||||||
call_span: Span,
|
call_span: Span,
|
||||||
columns: Vec<CellPath>,
|
columns: Vec<CellPath>,
|
||||||
input: PipelineData,
|
input: PipelineData,
|
||||||
ignore_errors: bool,
|
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
let mut unique_rows: HashSet<usize> = HashSet::new();
|
let mut unique_rows: HashSet<usize> = HashSet::new();
|
||||||
|
|
||||||
@ -106,11 +99,8 @@ fn select(
|
|||||||
for column in columns {
|
for column in columns {
|
||||||
let CellPath { ref members } = column;
|
let CellPath { ref members } = column;
|
||||||
match members.get(0) {
|
match members.get(0) {
|
||||||
Some(PathMember::Int { val, span }) => {
|
Some(PathMember::Int { val, span, .. }) => {
|
||||||
if members.len() > 1 {
|
if members.len() > 1 {
|
||||||
if ignore_errors {
|
|
||||||
return Ok(Value::nothing(call_span).into_pipeline_data());
|
|
||||||
}
|
|
||||||
return Err(ShellError::GenericError(
|
return Err(ShellError::GenericError(
|
||||||
"Select only allows row numbers for rows".into(),
|
"Select only allows row numbers for rows".into(),
|
||||||
"extra after row number".into(),
|
"extra after row number".into(),
|
||||||
@ -172,11 +162,7 @@ fn select(
|
|||||||
let mut vals = vec![];
|
let mut vals = vec![];
|
||||||
for path in &columns {
|
for path in &columns {
|
||||||
//FIXME: improve implementation to not clone
|
//FIXME: improve implementation to not clone
|
||||||
match input_val.clone().follow_cell_path(
|
match input_val.clone().follow_cell_path(&path.members, false) {
|
||||||
&path.members,
|
|
||||||
false,
|
|
||||||
ignore_errors,
|
|
||||||
) {
|
|
||||||
Ok(fetcher) => {
|
Ok(fetcher) => {
|
||||||
allempty = false;
|
allempty = false;
|
||||||
cols.push(path.into_string().replace('.', "_"));
|
cols.push(path.into_string().replace('.', "_"));
|
||||||
@ -214,10 +200,7 @@ fn select(
|
|||||||
let mut vals = vec![];
|
let mut vals = vec![];
|
||||||
for path in &columns {
|
for path in &columns {
|
||||||
//FIXME: improve implementation to not clone
|
//FIXME: improve implementation to not clone
|
||||||
match x
|
match x.clone().follow_cell_path(&path.members, false) {
|
||||||
.clone()
|
|
||||||
.follow_cell_path(&path.members, false, ignore_errors)
|
|
||||||
{
|
|
||||||
Ok(value) => {
|
Ok(value) => {
|
||||||
cols.push(path.into_string().replace('.', "_"));
|
cols.push(path.into_string().replace('.', "_"));
|
||||||
vals.push(value);
|
vals.push(value);
|
||||||
@ -246,10 +229,7 @@ fn select(
|
|||||||
|
|
||||||
for cell_path in columns {
|
for cell_path in columns {
|
||||||
// FIXME: remove clone
|
// FIXME: remove clone
|
||||||
match v
|
match v.clone().follow_cell_path(&cell_path.members, false) {
|
||||||
.clone()
|
|
||||||
.follow_cell_path(&cell_path.members, false, ignore_errors)
|
|
||||||
{
|
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
cols.push(cell_path.into_string().replace('.', "_"));
|
cols.push(cell_path.into_string().replace('.', "_"));
|
||||||
vals.push(result);
|
vals.push(result);
|
||||||
|
@ -143,7 +143,7 @@ fn update(
|
|||||||
ctrlc,
|
ctrlc,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
if let Some(PathMember::Int { val, span }) = cell_path.members.get(0) {
|
if let Some(PathMember::Int { val, span, .. }) = cell_path.members.get(0) {
|
||||||
let mut input = input.into_iter();
|
let mut input = input.into_iter();
|
||||||
let mut pre_elems = vec![];
|
let mut pre_elems = vec![];
|
||||||
|
|
||||||
|
@ -165,7 +165,7 @@ fn upsert(
|
|||||||
ctrlc,
|
ctrlc,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
if let Some(PathMember::Int { val, span }) = cell_path.members.get(0) {
|
if let Some(PathMember::Int { val, span, .. }) = cell_path.members.get(0) {
|
||||||
let mut input = input.into_iter();
|
let mut input = input.into_iter();
|
||||||
let mut pre_elems = vec![];
|
let mut pre_elems = vec![];
|
||||||
|
|
||||||
|
@ -279,12 +279,10 @@ fn format_record(
|
|||||||
.map(|path| PathMember::String {
|
.map(|path| PathMember::String {
|
||||||
val: path.to_string(),
|
val: path.to_string(),
|
||||||
span: *span,
|
span: *span,
|
||||||
|
optional: false,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
match data_as_value
|
match data_as_value.clone().follow_cell_path(&path_members, false) {
|
||||||
.clone()
|
|
||||||
.follow_cell_path(&path_members, false, false)
|
|
||||||
{
|
|
||||||
Ok(value_at_column) => {
|
Ok(value_at_column) => {
|
||||||
output.push_str(value_at_column.into_string(", ", config).as_str())
|
output.push_str(value_at_column.into_string(", ", config).as_str())
|
||||||
}
|
}
|
||||||
|
@ -277,9 +277,9 @@ fn convert_to_list(
|
|||||||
&[PathMember::String {
|
&[PathMember::String {
|
||||||
val: header.into(),
|
val: header.into(),
|
||||||
span: head,
|
span: head,
|
||||||
|
optional: false,
|
||||||
}],
|
}],
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
),
|
),
|
||||||
_ => Ok(item.clone()),
|
_ => Ok(item.clone()),
|
||||||
};
|
};
|
||||||
|
@ -934,8 +934,9 @@ fn convert_to_table(
|
|||||||
let path = PathMember::String {
|
let path = PathMember::String {
|
||||||
val: text.clone(),
|
val: text.clone(),
|
||||||
span: head,
|
span: head,
|
||||||
|
optional: false,
|
||||||
};
|
};
|
||||||
let val = item.clone().follow_cell_path(&[path], false, false);
|
let val = item.clone().follow_cell_path(&[path], false);
|
||||||
|
|
||||||
match val {
|
match val {
|
||||||
Ok(val) => DeferredStyleComputation::Value { value: val },
|
Ok(val) => DeferredStyleComputation::Value { value: val },
|
||||||
@ -1321,8 +1322,12 @@ fn create_table2_entry(
|
|||||||
match item {
|
match item {
|
||||||
Value::Record { .. } => {
|
Value::Record { .. } => {
|
||||||
let val = header.to_owned();
|
let val = header.to_owned();
|
||||||
let path = PathMember::String { val, span: head };
|
let path = PathMember::String {
|
||||||
let val = item.clone().follow_cell_path(&[path], false, false);
|
val,
|
||||||
|
span: head,
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
let val = item.clone().follow_cell_path(&[path], false);
|
||||||
|
|
||||||
match val {
|
match val {
|
||||||
Ok(val) => convert_to_table2_entry(
|
Ok(val) => convert_to_table2_entry(
|
||||||
|
@ -71,7 +71,7 @@ fn cal_sees_pipeline_year() {
|
|||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
cwd: ".", pipeline(
|
cwd: ".", pipeline(
|
||||||
r#"
|
r#"
|
||||||
cal --full-year 1020 | get -i monday | first 4 | to json -r
|
cal --full-year 1020 | get monday | first 4 | to json -r
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ fn regular_columns() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn skip_cell_rejection() {
|
fn skip_cell_rejection() {
|
||||||
let actual = nu!(cwd: ".", pipeline(
|
let actual = nu!(cwd: ".", pipeline(
|
||||||
r#"[ {a: 1, b: 2,c:txt}, { a:val } ] | reject a | get c.0"#));
|
r#"[ {a: 1, b: 2,c:txt}, { a:val } ] | reject a | get c?.0"#));
|
||||||
|
|
||||||
assert_eq!(actual.out, "txt");
|
assert_eq!(actual.out, "txt");
|
||||||
}
|
}
|
||||||
|
@ -165,7 +165,7 @@ fn select_ignores_errors_successfully1() {
|
|||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
cwd: ".", pipeline(
|
cwd: ".", pipeline(
|
||||||
r#"
|
r#"
|
||||||
[{a: 1, b: 2} {a: 3, b: 5} {a: 3}] | select -i b | length
|
[{a: 1, b: 2} {a: 3, b: 5} {a: 3}] | select b? | length
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -178,7 +178,7 @@ fn select_ignores_errors_successfully2() {
|
|||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
cwd: ".", pipeline(
|
cwd: ".", pipeline(
|
||||||
r#"
|
r#"
|
||||||
[{a: 1} {a: 2} {a: 3}] | select -i b | to nuon
|
[{a: 1} {a: 2} {a: 3}] | select b? | to nuon
|
||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
@ -190,7 +190,7 @@ fn select_ignores_errors_successfully2() {
|
|||||||
fn select_ignores_errors_successfully3() {
|
fn select_ignores_errors_successfully3() {
|
||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
cwd: ".", pipeline(
|
cwd: ".", pipeline(
|
||||||
r#"sys | select -i invalid_key | to nuon"#
|
r#"sys | select invalid_key? | to nuon"#
|
||||||
));
|
));
|
||||||
|
|
||||||
assert_eq!(actual.out, "{invalid_key: null}".to_string());
|
assert_eq!(actual.out, "{invalid_key: null}".to_string());
|
||||||
@ -201,38 +201,10 @@ fn select_ignores_errors_successfully3() {
|
|||||||
fn select_ignores_errors_successfully4() {
|
fn select_ignores_errors_successfully4() {
|
||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
cwd: ".", pipeline(
|
cwd: ".", pipeline(
|
||||||
r#"[a b c] | select -i invalid_key | to nuon"#
|
r#""key val\na 1\nb 2\n" | lines | split column -c " " | select foo? | to nuon"#
|
||||||
));
|
));
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(actual.out, r#"[[foo]; [null], [null], [null]]"#.to_string());
|
||||||
actual.out,
|
|
||||||
"[[invalid_key]; [null], [null], [null]]".to_string()
|
|
||||||
);
|
|
||||||
assert!(actual.err.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn select_ignores_errors_successfully5() {
|
|
||||||
let actual = nu!(
|
|
||||||
cwd: ".", pipeline(
|
|
||||||
r#"[a b c] | select -i 0.0"#
|
|
||||||
));
|
|
||||||
|
|
||||||
assert!(actual.out.is_empty());
|
|
||||||
assert!(actual.err.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn select_ignores_errors_successfully6() {
|
|
||||||
let actual = nu!(
|
|
||||||
cwd: ".", pipeline(
|
|
||||||
r#""key val\na 1\nb 2\n" | lines | split column -c " " | select -i "100" | to nuon"#
|
|
||||||
));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
actual.out,
|
|
||||||
r#"[["100"]; [null], [null], [null]]"#.to_string()
|
|
||||||
);
|
|
||||||
assert!(actual.err.is_empty());
|
assert!(actual.err.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ fn filters_by_unit_size_comparison() {
|
|||||||
fn filters_with_nothing_comparison() {
|
fn filters_with_nothing_comparison() {
|
||||||
let actual = nu!(
|
let actual = nu!(
|
||||||
cwd: "tests/fixtures/formats",
|
cwd: "tests/fixtures/formats",
|
||||||
r#"'[{"foo": 3}, {"foo": null}, {"foo": 4}]' | from json | get -i foo | compact | where $it > 1 | math sum"#
|
r#"'[{"foo": 3}, {"foo": null}, {"foo": 4}]' | from json | get foo | compact | where $it > 1 | math sum"#
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(actual.out, "7");
|
assert_eq!(actual.out, "7");
|
||||||
|
@ -319,10 +319,12 @@ fn get_converted_value(
|
|||||||
PathMember::String {
|
PathMember::String {
|
||||||
val: name.to_string(),
|
val: name.to_string(),
|
||||||
span: env_span,
|
span: env_span,
|
||||||
|
optional: false,
|
||||||
},
|
},
|
||||||
PathMember::String {
|
PathMember::String {
|
||||||
val: direction.to_string(),
|
val: direction.to_string(),
|
||||||
span: env_span,
|
span: env_span,
|
||||||
|
optional: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -300,7 +300,7 @@ pub fn eval_expression(
|
|||||||
Expr::FullCellPath(cell_path) => {
|
Expr::FullCellPath(cell_path) => {
|
||||||
let value = eval_expression(engine_state, stack, &cell_path.head)?;
|
let value = eval_expression(engine_state, stack, &cell_path.head)?;
|
||||||
|
|
||||||
value.follow_cell_path(&cell_path.tail, false, false)
|
value.follow_cell_path(&cell_path.tail, false)
|
||||||
}
|
}
|
||||||
Expr::ImportPattern(_) => Ok(Value::Nothing { span: expr.span }),
|
Expr::ImportPattern(_) => Ok(Value::Nothing { span: expr.span }),
|
||||||
Expr::Overlay(_) => {
|
Expr::Overlay(_) => {
|
||||||
@ -484,7 +484,6 @@ pub fn eval_expression(
|
|||||||
let vardata = lhs.follow_cell_path(
|
let vardata = lhs.follow_cell_path(
|
||||||
&[cell_path.tail[0].clone()],
|
&[cell_path.tail[0].clone()],
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
)?;
|
)?;
|
||||||
match &cell_path.tail[0] {
|
match &cell_path.tail[0] {
|
||||||
PathMember::String { val, .. } => {
|
PathMember::String { val, .. } => {
|
||||||
|
@ -599,8 +599,12 @@ fn create_table2_entry_basic(
|
|||||||
match item {
|
match item {
|
||||||
Value::Record { .. } => {
|
Value::Record { .. } => {
|
||||||
let val = header.to_owned();
|
let val = header.to_owned();
|
||||||
let path = PathMember::String { val, span: head };
|
let path = PathMember::String {
|
||||||
let val = item.clone().follow_cell_path(&[path], false, false);
|
val,
|
||||||
|
span: head,
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
let val = item.clone().follow_cell_path(&[path], false);
|
||||||
|
|
||||||
match val {
|
match val {
|
||||||
Ok(val) => value_to_styled_string(&val, config, style_computer),
|
Ok(val) => value_to_styled_string(&val, config, style_computer),
|
||||||
@ -627,8 +631,12 @@ fn create_table2_entry(
|
|||||||
match item {
|
match item {
|
||||||
Value::Record { .. } => {
|
Value::Record { .. } => {
|
||||||
let val = header.to_owned();
|
let val = header.to_owned();
|
||||||
let path = PathMember::String { val, span: head };
|
let path = PathMember::String {
|
||||||
let val = item.clone().follow_cell_path(&[path], false, false);
|
val,
|
||||||
|
span: head,
|
||||||
|
optional: false,
|
||||||
|
};
|
||||||
|
let val = item.clone().follow_cell_path(&[path], false);
|
||||||
|
|
||||||
match val {
|
match val {
|
||||||
Ok(val) => convert_to_table2_entry(
|
Ok(val) => convert_to_table2_entry(
|
||||||
|
@ -172,10 +172,11 @@ fn record_lookup_value(item: &Value, header: &str) -> Value {
|
|||||||
let path = PathMember::String {
|
let path = PathMember::String {
|
||||||
val: header.to_owned(),
|
val: header.to_owned(),
|
||||||
span: NuSpan::unknown(),
|
span: NuSpan::unknown(),
|
||||||
|
optional: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
item.clone()
|
item.clone()
|
||||||
.follow_cell_path(&[path], false, false)
|
.follow_cell_path(&[path], false)
|
||||||
.unwrap_or_else(|_| item.clone())
|
.unwrap_or_else(|_| item.clone())
|
||||||
}
|
}
|
||||||
item => item.clone(),
|
item => item.clone(),
|
||||||
|
@ -31,7 +31,7 @@ pub fn eval_constant(
|
|||||||
Expr::FullCellPath(cell_path) => {
|
Expr::FullCellPath(cell_path) => {
|
||||||
let value = eval_constant(working_set, &cell_path.head)?;
|
let value = eval_constant(working_set, &cell_path.head)?;
|
||||||
|
|
||||||
match value.follow_cell_path(&cell_path.tail, false, false) {
|
match value.follow_cell_path(&cell_path.tail, false) {
|
||||||
Ok(val) => Ok(val),
|
Ok(val) => Ok(val),
|
||||||
// TODO: Better error conversion
|
// TODO: Better error conversion
|
||||||
Err(shell_error) => Err(ParseError::LabeledError(
|
Err(shell_error) => Err(ParseError::LabeledError(
|
||||||
|
@ -941,10 +941,12 @@ pub fn parse_old_alias(
|
|||||||
PathMember::String {
|
PathMember::String {
|
||||||
val: "scope".to_string(),
|
val: "scope".to_string(),
|
||||||
span: Span::new(0, 0),
|
span: Span::new(0, 0),
|
||||||
|
optional: false,
|
||||||
},
|
},
|
||||||
PathMember::String {
|
PathMember::String {
|
||||||
val: "aliases".to_string(),
|
val: "aliases".to_string(),
|
||||||
span: Span::new(0, 0),
|
span: Span::new(0, 0),
|
||||||
|
optional: false,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let expr = Expression {
|
let expr = Expression {
|
||||||
|
@ -2015,54 +2015,100 @@ pub fn parse_variable_expr(
|
|||||||
pub fn parse_cell_path(
|
pub fn parse_cell_path(
|
||||||
working_set: &mut StateWorkingSet,
|
working_set: &mut StateWorkingSet,
|
||||||
tokens: impl Iterator<Item = Token>,
|
tokens: impl Iterator<Item = Token>,
|
||||||
mut expect_dot: bool,
|
expect_dot: bool,
|
||||||
expand_aliases_denylist: &[usize],
|
expand_aliases_denylist: &[usize],
|
||||||
span: Span,
|
|
||||||
) -> (Vec<PathMember>, Option<ParseError>) {
|
) -> (Vec<PathMember>, Option<ParseError>) {
|
||||||
|
enum TokenType {
|
||||||
|
Dot, // .
|
||||||
|
QuestionOrDot, // ? or .
|
||||||
|
PathMember, // an int or string, like `1` or `foo`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parsing a cell path is essentially a state machine, and this is the state
|
||||||
|
let mut expected_token = if expect_dot {
|
||||||
|
TokenType::Dot
|
||||||
|
} else {
|
||||||
|
TokenType::PathMember
|
||||||
|
};
|
||||||
|
|
||||||
let mut error = None;
|
let mut error = None;
|
||||||
let mut tail = vec![];
|
let mut tail = vec![];
|
||||||
|
|
||||||
for path_element in tokens {
|
for path_element in tokens {
|
||||||
let bytes = working_set.get_span_contents(path_element.span);
|
let bytes = working_set.get_span_contents(path_element.span);
|
||||||
|
|
||||||
if expect_dot {
|
match expected_token {
|
||||||
expect_dot = false;
|
TokenType::Dot => {
|
||||||
if bytes.len() != 1 || bytes[0] != b'.' {
|
if bytes.len() != 1 || bytes[0] != b'.' {
|
||||||
error = error.or_else(|| Some(ParseError::Expected('.'.into(), path_element.span)));
|
return (
|
||||||
|
tail,
|
||||||
|
Some(ParseError::Expected('.'.into(), path_element.span)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expected_token = TokenType::PathMember;
|
||||||
}
|
}
|
||||||
} else {
|
TokenType::QuestionOrDot => {
|
||||||
expect_dot = true;
|
if bytes.len() == 1 && bytes[0] == b'.' {
|
||||||
|
expected_token = TokenType::PathMember;
|
||||||
match parse_int(bytes, path_element.span) {
|
} else if bytes.len() == 1 && bytes[0] == b'?' {
|
||||||
(
|
if let Some(last) = tail.last_mut() {
|
||||||
Expression {
|
match last {
|
||||||
expr: Expr::Int(val),
|
PathMember::String {
|
||||||
span,
|
ref mut optional, ..
|
||||||
..
|
} => *optional = true,
|
||||||
},
|
PathMember::Int {
|
||||||
None,
|
ref mut optional, ..
|
||||||
) => tail.push(PathMember::Int {
|
} => *optional = true,
|
||||||
val: val as usize,
|
}
|
||||||
span,
|
}
|
||||||
}),
|
expected_token = TokenType::Dot;
|
||||||
_ => {
|
} else {
|
||||||
let (result, err) =
|
return (
|
||||||
parse_string(working_set, path_element.span, expand_aliases_denylist);
|
tail,
|
||||||
error = error.or(err);
|
Some(ParseError::Expected(". or ?".into(), path_element.span)),
|
||||||
match result {
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TokenType::PathMember => {
|
||||||
|
match parse_int(bytes, path_element.span) {
|
||||||
|
(
|
||||||
Expression {
|
Expression {
|
||||||
expr: Expr::String(string),
|
expr: Expr::Int(val),
|
||||||
span,
|
span,
|
||||||
..
|
..
|
||||||
} => {
|
},
|
||||||
tail.push(PathMember::String { val: string, span });
|
None,
|
||||||
}
|
) => tail.push(PathMember::Int {
|
||||||
_ => {
|
val: val as usize,
|
||||||
error =
|
span,
|
||||||
error.or_else(|| Some(ParseError::Expected("string".into(), span)));
|
optional: false,
|
||||||
|
}),
|
||||||
|
_ => {
|
||||||
|
let (result, err) =
|
||||||
|
parse_string(working_set, path_element.span, expand_aliases_denylist);
|
||||||
|
error = error.or(err);
|
||||||
|
match result {
|
||||||
|
Expression {
|
||||||
|
expr: Expr::String(string),
|
||||||
|
span,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
tail.push(PathMember::String {
|
||||||
|
val: string,
|
||||||
|
span,
|
||||||
|
optional: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return (
|
||||||
|
tail,
|
||||||
|
Some(ParseError::Expected("string".into(), path_element.span)),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
expected_token = TokenType::QuestionOrDot;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2081,7 +2127,7 @@ pub fn parse_full_cell_path(
|
|||||||
let source = working_set.get_span_contents(span);
|
let source = working_set.get_span_contents(span);
|
||||||
let mut error = None;
|
let mut error = None;
|
||||||
|
|
||||||
let (tokens, err) = lex(source, span.start, &[b'\n', b'\r'], &[b'.'], true);
|
let (tokens, err) = lex(source, span.start, &[b'\n', b'\r'], &[b'.', b'?'], true);
|
||||||
error = error.or(err);
|
error = error.or(err);
|
||||||
|
|
||||||
let mut tokens = tokens.into_iter().peekable();
|
let mut tokens = tokens.into_iter().peekable();
|
||||||
@ -2202,13 +2248,7 @@ pub fn parse_full_cell_path(
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
let (tail, err) = parse_cell_path(
|
let (tail, err) = parse_cell_path(working_set, tokens, expect_dot, expand_aliases_denylist);
|
||||||
working_set,
|
|
||||||
tokens,
|
|
||||||
expect_dot,
|
|
||||||
expand_aliases_denylist,
|
|
||||||
span,
|
|
||||||
);
|
|
||||||
error = error.or(err);
|
error = error.or(err);
|
||||||
|
|
||||||
(
|
(
|
||||||
@ -4597,13 +4637,13 @@ pub fn parse_value(
|
|||||||
let source = working_set.get_span_contents(span);
|
let source = working_set.get_span_contents(span);
|
||||||
let mut error = None;
|
let mut error = None;
|
||||||
|
|
||||||
let (tokens, err) = lex(source, span.start, &[b'\n', b'\r'], &[b'.'], true);
|
let (tokens, err) = lex(source, span.start, &[b'\n', b'\r'], &[b'.', b'?'], true);
|
||||||
error = error.or(err);
|
error = error.or(err);
|
||||||
|
|
||||||
let tokens = tokens.into_iter().peekable();
|
let tokens = tokens.into_iter().peekable();
|
||||||
|
|
||||||
let (cell_path, err) =
|
let (cell_path, err) =
|
||||||
parse_cell_path(working_set, tokens, false, expand_aliases_denylist, span);
|
parse_cell_path(working_set, tokens, false, expand_aliases_denylist);
|
||||||
error = error.or(err);
|
error = error.or(err);
|
||||||
|
|
||||||
(
|
(
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use nu_parser::ParseError;
|
use nu_parser::ParseError;
|
||||||
use nu_parser::*;
|
use nu_parser::*;
|
||||||
use nu_protocol::ast::Call;
|
use nu_protocol::ast::{Call, PathMember};
|
||||||
|
use nu_protocol::Span;
|
||||||
use nu_protocol::{
|
use nu_protocol::{
|
||||||
ast::{Expr, Expression, PipelineElement},
|
ast::{Expr, Expression, PipelineElement},
|
||||||
engine::{Command, EngineState, Stack, StateWorkingSet},
|
engine::{Command, EngineState, Stack, StateWorkingSet},
|
||||||
@ -321,6 +322,118 @@ pub fn parse_int_with_underscores() {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn parse_cell_path() {
|
||||||
|
let engine_state = EngineState::new();
|
||||||
|
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||||
|
|
||||||
|
working_set.add_variable(
|
||||||
|
"foo".to_string().into_bytes(),
|
||||||
|
Span::test_data(),
|
||||||
|
nu_protocol::Type::Record(vec![]),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (block, err) = parse(&mut working_set, None, b"$foo.bar.baz", true, &[]);
|
||||||
|
|
||||||
|
assert!(err.is_none());
|
||||||
|
assert_eq!(block.len(), 1);
|
||||||
|
let expressions = &block[0];
|
||||||
|
assert_eq!(expressions.len(), 1);
|
||||||
|
|
||||||
|
// hoo boy this pattern matching is a pain
|
||||||
|
if let PipelineElement::Expression(_, expr) = &expressions[0] {
|
||||||
|
if let Expr::FullCellPath(b) = &expr.expr {
|
||||||
|
assert!(matches!(
|
||||||
|
b.head,
|
||||||
|
Expression {
|
||||||
|
expr: Expr::Var(_),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
));
|
||||||
|
if let [a, b] = &b.tail[..] {
|
||||||
|
if let PathMember::String { val, optional, .. } = a {
|
||||||
|
assert_eq!(val, "bar");
|
||||||
|
assert_eq!(optional, &false);
|
||||||
|
} else {
|
||||||
|
panic!("wrong type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let PathMember::String { val, optional, .. } = b {
|
||||||
|
assert_eq!(val, "baz");
|
||||||
|
assert_eq!(optional, &false);
|
||||||
|
} else {
|
||||||
|
panic!("wrong type")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("cell path tail is unexpected")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Not a cell path");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Not an expression")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn parse_cell_path_optional() {
|
||||||
|
let engine_state = EngineState::new();
|
||||||
|
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||||
|
|
||||||
|
working_set.add_variable(
|
||||||
|
"foo".to_string().into_bytes(),
|
||||||
|
Span::test_data(),
|
||||||
|
nu_protocol::Type::Record(vec![]),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
let (block, err) = parse(&mut working_set, None, b"$foo.bar?.baz", true, &[]);
|
||||||
|
|
||||||
|
if let Some(err) = err {
|
||||||
|
dbg!(err);
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(block.len(), 1);
|
||||||
|
let expressions = &block[0];
|
||||||
|
assert_eq!(expressions.len(), 1);
|
||||||
|
|
||||||
|
// hoo boy this pattern matching is a pain
|
||||||
|
if let PipelineElement::Expression(_, expr) = &expressions[0] {
|
||||||
|
if let Expr::FullCellPath(b) = &expr.expr {
|
||||||
|
assert!(matches!(
|
||||||
|
b.head,
|
||||||
|
Expression {
|
||||||
|
expr: Expr::Var(_),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
));
|
||||||
|
if let [a, b] = &b.tail[..] {
|
||||||
|
if let PathMember::String { val, optional, .. } = a {
|
||||||
|
assert_eq!(val, "bar");
|
||||||
|
assert_eq!(optional, &true);
|
||||||
|
} else {
|
||||||
|
panic!("wrong type")
|
||||||
|
}
|
||||||
|
|
||||||
|
if let PathMember::String { val, optional, .. } = b {
|
||||||
|
assert_eq!(val, "baz");
|
||||||
|
assert_eq!(optional, &false);
|
||||||
|
} else {
|
||||||
|
panic!("wrong type")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("cell path tail is unexpected")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Not a cell path");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("Not an expression")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
pub fn parse_binary_with_hex_format() {
|
pub fn parse_binary_with_hex_format() {
|
||||||
let engine_state = EngineState::new();
|
let engine_state = EngineState::new();
|
||||||
|
@ -5,15 +5,45 @@ use std::fmt::Write;
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialOrd, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialOrd, Serialize, Deserialize)]
|
||||||
pub enum PathMember {
|
pub enum PathMember {
|
||||||
String { val: String, span: Span },
|
String {
|
||||||
Int { val: usize, span: Span },
|
val: String,
|
||||||
|
span: Span,
|
||||||
|
optional: bool,
|
||||||
|
},
|
||||||
|
Int {
|
||||||
|
val: usize,
|
||||||
|
span: Span,
|
||||||
|
optional: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for PathMember {
|
impl PartialEq for PathMember {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
match (self, other) {
|
match (self, other) {
|
||||||
(Self::String { val: l_val, .. }, Self::String { val: r_val, .. }) => l_val == r_val,
|
(
|
||||||
(Self::Int { val: l_val, .. }, Self::Int { val: r_val, .. }) => l_val == r_val,
|
Self::String {
|
||||||
|
val: l_val,
|
||||||
|
optional: l_opt,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
Self::String {
|
||||||
|
val: r_val,
|
||||||
|
optional: r_opt,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => l_val == r_val && l_opt == r_opt,
|
||||||
|
(
|
||||||
|
Self::Int {
|
||||||
|
val: l_val,
|
||||||
|
optional: l_opt,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
Self::Int {
|
||||||
|
val: r_val,
|
||||||
|
optional: r_opt,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => l_val == r_val && l_opt == r_opt,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -42,6 +72,19 @@ impl CellPath {
|
|||||||
|
|
||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn make_optional(&mut self) {
|
||||||
|
for member in &mut self.members {
|
||||||
|
match member {
|
||||||
|
PathMember::String {
|
||||||
|
ref mut optional, ..
|
||||||
|
} => *optional = true,
|
||||||
|
PathMember::Int {
|
||||||
|
ref mut optional, ..
|
||||||
|
} => *optional = true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
@ -322,7 +322,6 @@ impl PipelineData {
|
|||||||
cell_path: &[PathMember],
|
cell_path: &[PathMember],
|
||||||
head: Span,
|
head: Span,
|
||||||
insensitive: bool,
|
insensitive: bool,
|
||||||
ignore_errors: bool,
|
|
||||||
) -> Result<Value, ShellError> {
|
) -> Result<Value, ShellError> {
|
||||||
match self {
|
match self {
|
||||||
// FIXME: there are probably better ways of doing this
|
// FIXME: there are probably better ways of doing this
|
||||||
@ -330,8 +329,8 @@ impl PipelineData {
|
|||||||
vals: stream.collect(),
|
vals: stream.collect(),
|
||||||
span: head,
|
span: head,
|
||||||
}
|
}
|
||||||
.follow_cell_path(cell_path, insensitive, ignore_errors),
|
.follow_cell_path(cell_path, insensitive),
|
||||||
PipelineData::Value(v, ..) => v.follow_cell_path(cell_path, insensitive, ignore_errors),
|
PipelineData::Value(v, ..) => v.follow_cell_path(cell_path, insensitive),
|
||||||
_ => Err(ShellError::IOError("can't follow stream paths".into())),
|
_ => Err(ShellError::IOError("can't follow stream paths".into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -302,6 +302,7 @@ impl FromValue for CellPath {
|
|||||||
members: vec![PathMember::String {
|
members: vec![PathMember::String {
|
||||||
val: val.clone(),
|
val: val.clone(),
|
||||||
span,
|
span,
|
||||||
|
optional: false,
|
||||||
}],
|
}],
|
||||||
}),
|
}),
|
||||||
Value::Int { val, span } => {
|
Value::Int { val, span } => {
|
||||||
@ -312,6 +313,7 @@ impl FromValue for CellPath {
|
|||||||
members: vec![PathMember::Int {
|
members: vec![PathMember::Int {
|
||||||
val: *val as usize,
|
val: *val as usize,
|
||||||
span: *span,
|
span: *span,
|
||||||
|
optional: false,
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -681,9 +681,8 @@ impl Value {
|
|||||||
self,
|
self,
|
||||||
cell_path: &[PathMember],
|
cell_path: &[PathMember],
|
||||||
insensitive: bool,
|
insensitive: bool,
|
||||||
nullify_errors: bool,
|
|
||||||
) -> Result<Value, ShellError> {
|
) -> Result<Value, ShellError> {
|
||||||
self.follow_cell_path_helper(cell_path, insensitive, nullify_errors, true)
|
self.follow_cell_path_helper(cell_path, insensitive, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn follow_cell_path_not_from_user_input(
|
pub fn follow_cell_path_not_from_user_input(
|
||||||
@ -691,24 +690,20 @@ impl Value {
|
|||||||
cell_path: &[PathMember],
|
cell_path: &[PathMember],
|
||||||
insensitive: bool,
|
insensitive: bool,
|
||||||
) -> Result<Value, ShellError> {
|
) -> Result<Value, ShellError> {
|
||||||
self.follow_cell_path_helper(cell_path, insensitive, false, false)
|
self.follow_cell_path_helper(cell_path, insensitive, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn follow_cell_path_helper(
|
fn follow_cell_path_helper(
|
||||||
self,
|
self,
|
||||||
cell_path: &[PathMember],
|
cell_path: &[PathMember],
|
||||||
insensitive: bool,
|
insensitive: bool,
|
||||||
nullify_errors: bool, // Turn all errors into Value::Nothing
|
|
||||||
from_user_input: bool,
|
from_user_input: bool,
|
||||||
) -> Result<Value, ShellError> {
|
) -> Result<Value, ShellError> {
|
||||||
let mut current = self;
|
let mut current = self;
|
||||||
|
// TODO remove this
|
||||||
macro_rules! err_or_null {
|
macro_rules! err_or_null {
|
||||||
($e:expr, $span:expr) => {
|
($e:expr, $span:expr) => {
|
||||||
return if nullify_errors {
|
return Err($e)
|
||||||
Ok(Value::nothing($span))
|
|
||||||
} else {
|
|
||||||
Err($e)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
for member in cell_path {
|
for member in cell_path {
|
||||||
@ -718,12 +713,15 @@ impl Value {
|
|||||||
PathMember::Int {
|
PathMember::Int {
|
||||||
val: count,
|
val: count,
|
||||||
span: origin_span,
|
span: origin_span,
|
||||||
|
optional,
|
||||||
} => {
|
} => {
|
||||||
// Treat a numeric path member as `select <val>`
|
// Treat a numeric path member as `select <val>`
|
||||||
match &mut current {
|
match &mut current {
|
||||||
Value::List { vals: val, .. } => {
|
Value::List { vals: val, .. } => {
|
||||||
if let Some(item) = val.get(*count) {
|
if let Some(item) = val.get(*count) {
|
||||||
current = item.clone();
|
current = item.clone();
|
||||||
|
} else if *optional {
|
||||||
|
current = Value::nothing(*origin_span);
|
||||||
} else if val.is_empty() {
|
} else if val.is_empty() {
|
||||||
err_or_null!(
|
err_or_null!(
|
||||||
ShellError::AccessEmptyContent { span: *origin_span },
|
ShellError::AccessEmptyContent { span: *origin_span },
|
||||||
@ -742,6 +740,8 @@ impl Value {
|
|||||||
Value::Binary { val, .. } => {
|
Value::Binary { val, .. } => {
|
||||||
if let Some(item) = val.get(*count) {
|
if let Some(item) = val.get(*count) {
|
||||||
current = Value::int(*item as i64, *origin_span);
|
current = Value::int(*item as i64, *origin_span);
|
||||||
|
} else if *optional {
|
||||||
|
current = Value::nothing(*origin_span);
|
||||||
} else if val.is_empty() {
|
} else if val.is_empty() {
|
||||||
err_or_null!(
|
err_or_null!(
|
||||||
ShellError::AccessEmptyContent { span: *origin_span },
|
ShellError::AccessEmptyContent { span: *origin_span },
|
||||||
@ -760,6 +760,8 @@ impl Value {
|
|||||||
Value::Range { val, .. } => {
|
Value::Range { val, .. } => {
|
||||||
if let Some(item) = val.clone().into_range_iter(None)?.nth(*count) {
|
if let Some(item) = val.clone().into_range_iter(None)?.nth(*count) {
|
||||||
current = item.clone();
|
current = item.clone();
|
||||||
|
} else if *optional {
|
||||||
|
current = Value::nothing(*origin_span);
|
||||||
} else {
|
} else {
|
||||||
err_or_null!(
|
err_or_null!(
|
||||||
ShellError::AccessBeyondEndOfStream { span: *origin_span },
|
ShellError::AccessBeyondEndOfStream { span: *origin_span },
|
||||||
@ -768,7 +770,19 @@ impl Value {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Value::CustomValue { val, .. } => {
|
Value::CustomValue { val, .. } => {
|
||||||
current = val.follow_path_int(*count, *origin_span)?;
|
current = match val.follow_path_int(*count, *origin_span) {
|
||||||
|
Ok(val) => val,
|
||||||
|
Err(err) => {
|
||||||
|
if *optional {
|
||||||
|
Value::nothing(*origin_span)
|
||||||
|
} else {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Value::Nothing { .. } if *optional => {
|
||||||
|
current = Value::nothing(*origin_span);
|
||||||
}
|
}
|
||||||
// Records (and tables) are the only built-in which support column names,
|
// Records (and tables) are the only built-in which support column names,
|
||||||
// so only use this message for them.
|
// so only use this message for them.
|
||||||
@ -792,6 +806,7 @@ impl Value {
|
|||||||
PathMember::String {
|
PathMember::String {
|
||||||
val: column_name,
|
val: column_name,
|
||||||
span: origin_span,
|
span: origin_span,
|
||||||
|
optional,
|
||||||
} => match &mut current {
|
} => match &mut current {
|
||||||
Value::Record { cols, vals, span } => {
|
Value::Record { cols, vals, span } => {
|
||||||
let cols = cols.clone();
|
let cols = cols.clone();
|
||||||
@ -806,6 +821,8 @@ impl Value {
|
|||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
current = found.1.clone();
|
current = found.1.clone();
|
||||||
|
} else if *optional {
|
||||||
|
current = Value::nothing(*origin_span);
|
||||||
} else {
|
} else {
|
||||||
if from_user_input {
|
if from_user_input {
|
||||||
if let Some(suggestion) = did_you_mean(&cols, column_name) {
|
if let Some(suggestion) = did_you_mean(&cols, column_name) {
|
||||||
@ -830,6 +847,8 @@ impl Value {
|
|||||||
|
|
||||||
if columns.contains(&column_name.as_str()) {
|
if columns.contains(&column_name.as_str()) {
|
||||||
current = val.get_column_value(column_name)?;
|
current = val.get_column_value(column_name)?;
|
||||||
|
} else if *optional {
|
||||||
|
current = Value::nothing(*origin_span);
|
||||||
} else {
|
} else {
|
||||||
if from_user_input {
|
if from_user_input {
|
||||||
if let Some(suggestion) = did_you_mean(&columns, column_name) {
|
if let Some(suggestion) = did_you_mean(&columns, column_name) {
|
||||||
@ -852,10 +871,9 @@ impl Value {
|
|||||||
// String access of Lists always means Table access.
|
// String access of Lists always means Table access.
|
||||||
// Create a List which contains each matching value for contained
|
// Create a List which contains each matching value for contained
|
||||||
// records in the source list.
|
// records in the source list.
|
||||||
// If nullify_errors is true, table holes are converted to null.
|
|
||||||
Value::List { vals, span } => {
|
Value::List { vals, span } => {
|
||||||
|
// TODO: this should stream instead of collecting
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
let mut found_at_least_1_value = false;
|
|
||||||
for val in vals {
|
for val in vals {
|
||||||
// only look in records; this avoids unintentionally recursing into deeply nested tables
|
// only look in records; this avoids unintentionally recursing into deeply nested tables
|
||||||
if matches!(val, Value::Record { .. }) {
|
if matches!(val, Value::Record { .. }) {
|
||||||
@ -863,69 +881,40 @@ impl Value {
|
|||||||
&[PathMember::String {
|
&[PathMember::String {
|
||||||
val: column_name.clone(),
|
val: column_name.clone(),
|
||||||
span: *origin_span,
|
span: *origin_span,
|
||||||
|
optional: *optional,
|
||||||
}],
|
}],
|
||||||
insensitive,
|
insensitive,
|
||||||
nullify_errors,
|
|
||||||
) {
|
) {
|
||||||
found_at_least_1_value = true;
|
|
||||||
output.push(result);
|
output.push(result);
|
||||||
} else {
|
} else {
|
||||||
// Consider [{a:1 b:2} {a:3}]:
|
return Err(ShellError::CantFindColumn {
|
||||||
// [{a:1 b:2} {a:3}].b should error.
|
col_name: column_name.to_string(),
|
||||||
// [{a:1 b:2} {a:3}].b.1 should error.
|
span: *origin_span,
|
||||||
// [{a:1 b:2} {a:3}].b.0 should NOT error because the path can find a proper value (2)
|
src_span: val.span().unwrap_or(*span),
|
||||||
// but if this returns an error immediately, it will.
|
|
||||||
//
|
|
||||||
// Solution: push a Value::Error into this result list instead of returning it.
|
|
||||||
// This also means that `[{a:1 b:2} {a:2}].b | reject 1` also doesn't error.
|
|
||||||
// Anything that needs to use every value inside the list should propagate
|
|
||||||
// the error outward, though.
|
|
||||||
output.push(if nullify_errors {
|
|
||||||
Value::nothing(*origin_span)
|
|
||||||
} else {
|
|
||||||
Value::Error {
|
|
||||||
error: Box::new(ShellError::CantFindColumn {
|
|
||||||
col_name: column_name.to_string(),
|
|
||||||
span: *origin_span,
|
|
||||||
src_span: val.span().unwrap_or(*span),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if *optional && matches!(val, Value::Nothing { .. }) {
|
||||||
|
output.push(Value::nothing(*origin_span));
|
||||||
} else {
|
} else {
|
||||||
// See comment above.
|
return Err(ShellError::CantFindColumn {
|
||||||
output.push(if nullify_errors {
|
col_name: column_name.to_string(),
|
||||||
Value::nothing(*origin_span)
|
span: *origin_span,
|
||||||
} else {
|
src_span: val.span().unwrap_or(*span),
|
||||||
Value::Error {
|
|
||||||
error: Box::new(ShellError::CantFindColumn {
|
|
||||||
col_name: column_name.to_string(),
|
|
||||||
span: *origin_span,
|
|
||||||
src_span: val.span().unwrap_or(*span),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if found_at_least_1_value {
|
|
||||||
current = Value::List {
|
current = Value::List {
|
||||||
vals: output,
|
vals: output,
|
||||||
span: *span,
|
span: *span,
|
||||||
};
|
};
|
||||||
} else {
|
|
||||||
err_or_null!(
|
|
||||||
ShellError::CantFindColumn {
|
|
||||||
col_name: column_name.to_string(),
|
|
||||||
span: *origin_span,
|
|
||||||
src_span: *span
|
|
||||||
},
|
|
||||||
*origin_span
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Value::CustomValue { val, .. } => {
|
Value::CustomValue { val, .. } => {
|
||||||
current = val.follow_path_string(column_name.clone(), *origin_span)?;
|
current = val.follow_path_string(column_name.clone(), *origin_span)?;
|
||||||
}
|
}
|
||||||
|
Value::Nothing { .. } if *optional => {
|
||||||
|
current = Value::nothing(*origin_span);
|
||||||
|
}
|
||||||
Value::Error { error } => err_or_null!(*error.to_owned(), *origin_span),
|
Value::Error { error } => err_or_null!(*error.to_owned(), *origin_span),
|
||||||
x => {
|
x => {
|
||||||
err_or_null!(
|
err_or_null!(
|
||||||
@ -956,7 +945,7 @@ impl Value {
|
|||||||
) -> Result<(), ShellError> {
|
) -> Result<(), ShellError> {
|
||||||
let orig = self.clone();
|
let orig = self.clone();
|
||||||
|
|
||||||
let new_val = callback(&orig.follow_cell_path(cell_path, false, false)?);
|
let new_val = callback(&orig.follow_cell_path(cell_path, false)?);
|
||||||
|
|
||||||
match new_val {
|
match new_val {
|
||||||
Value::Error { error } => Err(*error),
|
Value::Error { error } => Err(*error),
|
||||||
@ -974,6 +963,7 @@ impl Value {
|
|||||||
PathMember::String {
|
PathMember::String {
|
||||||
val: col_name,
|
val: col_name,
|
||||||
span,
|
span,
|
||||||
|
..
|
||||||
} => match self {
|
} => match self {
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
for val in vals.iter_mut() {
|
for val in vals.iter_mut() {
|
||||||
@ -1055,7 +1045,9 @@ impl Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PathMember::Int { val: row_num, span } => match self {
|
PathMember::Int {
|
||||||
|
val: row_num, span, ..
|
||||||
|
} => match self {
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
if let Some(v) = vals.get_mut(*row_num) {
|
if let Some(v) = vals.get_mut(*row_num) {
|
||||||
v.upsert_data_at_cell_path(&cell_path[1..], new_val)?
|
v.upsert_data_at_cell_path(&cell_path[1..], new_val)?
|
||||||
@ -1094,7 +1086,7 @@ impl Value {
|
|||||||
) -> Result<(), ShellError> {
|
) -> Result<(), ShellError> {
|
||||||
let orig = self.clone();
|
let orig = self.clone();
|
||||||
|
|
||||||
let new_val = callback(&orig.follow_cell_path(cell_path, false, false)?);
|
let new_val = callback(&orig.follow_cell_path(cell_path, false)?);
|
||||||
|
|
||||||
match new_val {
|
match new_val {
|
||||||
Value::Error { error } => Err(*error),
|
Value::Error { error } => Err(*error),
|
||||||
@ -1113,6 +1105,7 @@ impl Value {
|
|||||||
PathMember::String {
|
PathMember::String {
|
||||||
val: col_name,
|
val: col_name,
|
||||||
span,
|
span,
|
||||||
|
..
|
||||||
} => match self {
|
} => match self {
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
for val in vals.iter_mut() {
|
for val in vals.iter_mut() {
|
||||||
@ -1183,7 +1176,9 @@ impl Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PathMember::Int { val: row_num, span } => match self {
|
PathMember::Int {
|
||||||
|
val: row_num, span, ..
|
||||||
|
} => match self {
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
if let Some(v) = vals.get_mut(*row_num) {
|
if let Some(v) = vals.get_mut(*row_num) {
|
||||||
v.update_data_at_cell_path(&cell_path[1..], new_val)?
|
v.update_data_at_cell_path(&cell_path[1..], new_val)?
|
||||||
@ -1221,6 +1216,7 @@ impl Value {
|
|||||||
PathMember::String {
|
PathMember::String {
|
||||||
val: col_name,
|
val: col_name,
|
||||||
span,
|
span,
|
||||||
|
..
|
||||||
} => match self {
|
} => match self {
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
for val in vals.iter_mut() {
|
for val in vals.iter_mut() {
|
||||||
@ -1289,7 +1285,9 @@ impl Value {
|
|||||||
src_span: v.span()?,
|
src_span: v.span()?,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
PathMember::Int { val: row_num, span } => match self {
|
PathMember::Int {
|
||||||
|
val: row_num, span, ..
|
||||||
|
} => match self {
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
if vals.get_mut(*row_num).is_some() {
|
if vals.get_mut(*row_num).is_some() {
|
||||||
vals.remove(*row_num);
|
vals.remove(*row_num);
|
||||||
@ -1316,6 +1314,7 @@ impl Value {
|
|||||||
PathMember::String {
|
PathMember::String {
|
||||||
val: col_name,
|
val: col_name,
|
||||||
span,
|
span,
|
||||||
|
..
|
||||||
} => match self {
|
} => match self {
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
for val in vals.iter_mut() {
|
for val in vals.iter_mut() {
|
||||||
@ -1380,7 +1379,9 @@ impl Value {
|
|||||||
src_span: v.span()?,
|
src_span: v.span()?,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
PathMember::Int { val: row_num, span } => match self {
|
PathMember::Int {
|
||||||
|
val: row_num, span, ..
|
||||||
|
} => match self {
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
if let Some(v) = vals.get_mut(*row_num) {
|
if let Some(v) = vals.get_mut(*row_num) {
|
||||||
v.remove_data_at_cell_path(&cell_path[1..])
|
v.remove_data_at_cell_path(&cell_path[1..])
|
||||||
@ -1414,6 +1415,7 @@ impl Value {
|
|||||||
PathMember::String {
|
PathMember::String {
|
||||||
val: col_name,
|
val: col_name,
|
||||||
span,
|
span,
|
||||||
|
..
|
||||||
} => match self {
|
} => match self {
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
for val in vals.iter_mut() {
|
for val in vals.iter_mut() {
|
||||||
@ -1492,7 +1494,9 @@ impl Value {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PathMember::Int { val: row_num, span } => match self {
|
PathMember::Int {
|
||||||
|
val: row_num, span, ..
|
||||||
|
} => match self {
|
||||||
Value::List { vals, .. } => {
|
Value::List { vals, .. } => {
|
||||||
if let Some(v) = vals.get_mut(*row_num) {
|
if let Some(v) = vals.get_mut(*row_num) {
|
||||||
v.insert_data_at_cell_path(&cell_path[1..], new_val, head_span)?
|
v.insert_data_at_cell_path(&cell_path[1..], new_val, head_span)?
|
||||||
|
@ -97,7 +97,7 @@ impl Inc {
|
|||||||
pub fn inc(&self, head: Span, value: &Value) -> Result<Value, LabeledError> {
|
pub fn inc(&self, head: Span, value: &Value) -> Result<Value, LabeledError> {
|
||||||
if let Some(cell_path) = &self.cell_path {
|
if let Some(cell_path) = &self.cell_path {
|
||||||
let working_value = value.clone();
|
let working_value = value.clone();
|
||||||
let cell_value = working_value.follow_cell_path(&cell_path.members, false, false)?;
|
let cell_value = working_value.follow_cell_path(&cell_path.members, false)?;
|
||||||
|
|
||||||
let cell_value = self.inc_value(head, &cell_value)?;
|
let cell_value = self.inc_value(head, &cell_value)?;
|
||||||
|
|
||||||
|
@ -17,6 +17,21 @@ fn record_single_field_success() -> TestResult {
|
|||||||
run_test("{foo: 'bar'}.foo == 'bar'", "true")
|
run_test("{foo: 'bar'}.foo == 'bar'", "true")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_single_field_optional_success() -> TestResult {
|
||||||
|
run_test("{foo: 'bar'}.foo? == 'bar'", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_works_with_cell_path_success() -> TestResult {
|
||||||
|
run_test("{foo: 'bar'} | get foo?", "bar")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_works_with_cell_path_missing_data() -> TestResult {
|
||||||
|
run_test("{foo: 'bar'} | get foobar? | to nuon", "null")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn record_single_field_failure() -> TestResult {
|
fn record_single_field_failure() -> TestResult {
|
||||||
fail_test("{foo: 'bar'}.foobar", "")
|
fail_test("{foo: 'bar'}.foobar", "")
|
||||||
@ -27,6 +42,21 @@ fn record_int_failure() -> TestResult {
|
|||||||
fail_test("{foo: 'bar'}.3", "")
|
fail_test("{foo: 'bar'}.3", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_single_field_optional() -> TestResult {
|
||||||
|
run_test("{foo: 'bar'}.foobar? | to nuon", "null")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_single_field_optional_does_not_short_circuit() -> TestResult {
|
||||||
|
fail_test("{foo: 'bar'}.foobar?.baz", "nothing")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn record_multiple_optional_fields() -> TestResult {
|
||||||
|
run_test("{foo: 'bar'}.foobar?.baz? | to nuon", "null")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nested_record_field_success() -> TestResult {
|
fn nested_record_field_success() -> TestResult {
|
||||||
run_test("{foo: {bar: 'baz'} }.foo.bar == 'baz'", "true")
|
run_test("{foo: {bar: 'baz'} }.foo.bar == 'baz'", "true")
|
||||||
@ -37,6 +67,11 @@ fn nested_record_field_failure() -> TestResult {
|
|||||||
fail_test("{foo: {bar: 'baz'} }.foo.asdf", "")
|
fail_test("{foo: {bar: 'baz'} }.foo.asdf", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nested_record_field_optional() -> TestResult {
|
||||||
|
run_test("{foo: {bar: 'baz'} }.foo.asdf? | to nuon", "null")
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn record_with_nested_list_success() -> TestResult {
|
fn record_with_nested_list_success() -> TestResult {
|
||||||
run_test("{foo: [{bar: 'baz'}]}.foo.0.bar == 'baz'", "true")
|
run_test("{foo: [{bar: 'baz'}]}.foo.0.bar == 'baz'", "true")
|
||||||
@ -72,12 +107,27 @@ fn jagged_list_access_fails() -> TestResult {
|
|||||||
fail_test("[{}, {foo: 'bar'}].foo", "cannot find column")
|
fail_test("[{}, {foo: 'bar'}].foo", "cannot find column")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn jagged_list_optional_access_succeeds() -> TestResult {
|
||||||
|
run_test("[{foo: 'bar'}, {}].foo?.0", "bar")?;
|
||||||
|
run_test("[{foo: 'bar'}, {}].foo?.1 | to nuon", "null")?;
|
||||||
|
|
||||||
|
run_test("[{}, {foo: 'bar'}].foo?.0 | to nuon", "null")?;
|
||||||
|
run_test("[{}, {foo: 'bar'}].foo?.1", "bar")
|
||||||
|
}
|
||||||
|
|
||||||
// test that accessing a nonexistent row fails
|
// test that accessing a nonexistent row fails
|
||||||
#[test]
|
#[test]
|
||||||
fn list_row_access_failure() -> TestResult {
|
fn list_row_access_failure() -> TestResult {
|
||||||
fail_test("[{foo: 'bar'}, {foo: 'baz'}].2", "")
|
fail_test("[{foo: 'bar'}, {foo: 'baz'}].2", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_row_optional_access_succeeds() -> TestResult {
|
||||||
|
run_test("[{foo: 'bar'}, {foo: 'baz'}].2? | to nuon", "null")?;
|
||||||
|
run_test("[{foo: 'bar'}, {foo: 'baz'}].3? | to nuon", "null")
|
||||||
|
}
|
||||||
|
|
||||||
// regression test for an old bug
|
// regression test for an old bug
|
||||||
#[test]
|
#[test]
|
||||||
fn do_not_delve_too_deep_in_nested_lists() -> TestResult {
|
fn do_not_delve_too_deep_in_nested_lists() -> TestResult {
|
||||||
|
@ -179,6 +179,33 @@ fn missing_column_errors() -> TestResult {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_optional_column_fills_in_nothing() -> TestResult {
|
||||||
|
// The empty value will be replaced with $nothing because of the ?
|
||||||
|
run_test(
|
||||||
|
r#"[ { name: ABC, size: 20 }, { name: HIJ } ].size?.1 == $nothing"#,
|
||||||
|
"true",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_required_row_fails() -> TestResult {
|
||||||
|
// .3 will fail if there is no 3rd row
|
||||||
|
fail_test(
|
||||||
|
r#"[ { name: ABC, size: 20 }, { name: HIJ } ].3"#,
|
||||||
|
"", // we just care if it errors
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_optional_row_fills_in_nothing() -> TestResult {
|
||||||
|
// ?.3 will return $nothing if there is no 3rd row
|
||||||
|
run_test(
|
||||||
|
r#"[ { name: ABC, size: 20 }, { name: HIJ } ].3? == $nothing"#,
|
||||||
|
"true",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn string_cell_path() -> TestResult {
|
fn string_cell_path() -> TestResult {
|
||||||
run_test(
|
run_test(
|
||||||
@ -257,9 +284,9 @@ fn length_defaulted_columns() -> TestResult {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn nullify_errors() -> TestResult {
|
fn nullify_errors() -> TestResult {
|
||||||
run_test("([{a:1} {a:2} {a:3}] | get -i foo | length) == 3", "true")?;
|
run_test("([{a:1} {a:2} {a:3}] | get foo? | length) == 3", "true")?;
|
||||||
run_test(
|
run_test(
|
||||||
"([{a:1} {a:2} {a:3}] | get -i foo | to nuon) == '[null, null, null]'",
|
"([{a:1} {a:2} {a:3}] | get foo? | to nuon) == '[null, null, null]'",
|
||||||
"true",
|
"true",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -267,7 +294,7 @@ fn nullify_errors() -> TestResult {
|
|||||||
#[test]
|
#[test]
|
||||||
fn nullify_holes() -> TestResult {
|
fn nullify_holes() -> TestResult {
|
||||||
run_test(
|
run_test(
|
||||||
"([{a:1} {b:2} {a:3}] | get -i a | to nuon) == '[1, null, 3]'",
|
"([{a:1} {b:2} {a:3}] | get a? | to nuon) == '[1, null, 3]'",
|
||||||
"true",
|
"true",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user