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:
Reilly Wood 2023-03-15 20:50:58 -07:00 committed by GitHub
parent d3be5ec750
commit 21b84a6d65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 510 additions and 277 deletions

View File

@ -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,

View File

@ -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,
}, },

View File

@ -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(),

View File

@ -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);

View File

@ -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),

View File

@ -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,
}); });

View File

@ -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?);
} }

View File

@ -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);

View File

@ -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![];

View File

@ -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![];

View File

@ -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())
} }

View File

@ -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()),
}; };

View File

@ -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(

View File

@ -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
"# "#
)); ));

View File

@ -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");
} }

View File

@ -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());
} }

View File

@ -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");

View File

@ -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,
}, },
]; ];

View File

@ -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, .. } => {

View File

@ -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(

View File

@ -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(),

View File

@ -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(

View File

@ -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 {

View File

@ -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);
( (

View File

@ -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();

View File

@ -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)]

View File

@ -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())),
} }
} }

View File

@ -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,
}], }],
}) })
} }

View File

@ -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)?

View File

@ -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)?;

View File

@ -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 {

View File

@ -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",
) )
} }