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 {
val: "condition".to_string(),
span: value_span,
optional: false,
};
let mut output = PipelineData::empty();
let code_path = PathMember::String {
val: "code".to_string(),
span: value_span,
optional: false,
};
match value {
@ -894,63 +896,60 @@ pub fn eval_hook(
}
}
Value::Record { .. } => {
let do_run_hook = if let Ok(condition) =
value
.clone()
.follow_cell_path(&[condition_path], false, false)
{
match condition {
Value::Block {
val: block_id,
span: block_span,
..
}
| Value::Closure {
val: block_id,
span: block_span,
..
} => {
match run_hook_block(
engine_state,
stack,
block_id,
None,
arguments.clone(),
block_span,
) {
Ok(pipeline_data) => {
if let PipelineData::Value(Value::Bool { val, .. }, ..) =
pipeline_data
{
val
} else {
return Err(ShellError::UnsupportedConfigValue(
"boolean output".to_string(),
"other PipelineData variant".to_string(),
block_span,
));
let do_run_hook =
if let Ok(condition) = value.clone().follow_cell_path(&[condition_path], false) {
match condition {
Value::Block {
val: block_id,
span: block_span,
..
}
| Value::Closure {
val: block_id,
span: block_span,
..
} => {
match run_hook_block(
engine_state,
stack,
block_id,
None,
arguments.clone(),
block_span,
) {
Ok(pipeline_data) => {
if let PipelineData::Value(Value::Bool { val, .. }, ..) =
pipeline_data
{
val
} else {
return Err(ShellError::UnsupportedConfigValue(
"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 => {
return Err(ShellError::UnsupportedConfigValue(
"block".to_string(),
format!("{}", other.get_type()),
other.span()?,
));
}
}
} else {
// always run the hook
true
};
} else {
// always run the hook
true
};
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 {
val,
span: source_span,

View File

@ -240,7 +240,11 @@ mod test {
},
Value::CellPath {
val: CellPath {
members: vec![PathMember::Int { val: 0, span }],
members: vec![PathMember::Int {
val: 0,
span,
optional: false,
}],
},
span,
},

View File

@ -208,10 +208,11 @@ mod util {
let path = PathMember::String {
val: header.to_owned(),
span: Span::unknown(),
optional: false,
};
item.clone()
.follow_cell_path(&[path], false, false)
.follow_cell_path(&[path], false)
.unwrap_or_else(|_| item.clone())
}
item => item.clone(),

View File

@ -109,10 +109,7 @@ fn dropcol(
let mut vals = vec![];
for path in &keep_columns {
let fetcher =
input_val
.clone()
.follow_cell_path(&path.members, false, false)?;
let fetcher = input_val.clone().follow_cell_path(&path.members, false)?;
cols.push(path.into_string());
vals.push(fetcher);
}
@ -136,10 +133,7 @@ fn dropcol(
let mut vals = vec![];
for path in &keep_columns {
let fetcher =
input_val
.clone()
.follow_cell_path(&path.members, false, false)?;
let fetcher = input_val.clone().follow_cell_path(&path.members, false)?;
cols.push(path.into_string());
vals.push(fetcher);
}
@ -155,9 +149,7 @@ fn dropcol(
let mut vals = vec![];
for cell_path in &keep_columns {
let result = v
.clone()
.follow_cell_path(&cell_path.members, false, false)?;
let result = v.clone().follow_cell_path(&cell_path.members, false)?;
cols.push(cell_path.into_string());
vals.push(result);

View File

@ -74,7 +74,7 @@ fn empty(
for val in input {
for column in &columns {
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(_) => return Ok(Value::boolean(false, head).into_pipeline_data()),
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() {
let cell_path =
column_requested.and_then(|x| match x.members.first() {
Some(PathMember::String { val, span: _ }) => Some(val),
Some(PathMember::String { val, span: _, .. }) => Some(val),
_ => 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",
)
.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(
"sensitive",
"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 rest: Vec<CellPath> = call.rest(engine_state, stack, 1)?;
let sensitive = call.has_flag("sensitive");
let ignore_errors = call.has_flag("ignore-errors");
let ctrlc = engine_state.ctrlc.clone();
let metadata = input.metadata();
if rest.is_empty() {
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())
} else {
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);
for path in paths {
let val = input
.clone()
.follow_cell_path(&path.members, !sensitive, false);
let val = input.clone().follow_cell_path(&path.members, !sensitive);
output.push(val?);
}

View File

@ -22,11 +22,6 @@ impl Command for Select {
(Type::Record(vec![]), Type::Record(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",
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> {
let columns: Vec<CellPath> = call.rest(engine_state, stack, 0)?;
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> {
@ -97,7 +91,6 @@ fn select(
call_span: Span,
columns: Vec<CellPath>,
input: PipelineData,
ignore_errors: bool,
) -> Result<PipelineData, ShellError> {
let mut unique_rows: HashSet<usize> = HashSet::new();
@ -106,11 +99,8 @@ fn select(
for column in columns {
let CellPath { ref members } = column;
match members.get(0) {
Some(PathMember::Int { val, span }) => {
Some(PathMember::Int { val, span, .. }) => {
if members.len() > 1 {
if ignore_errors {
return Ok(Value::nothing(call_span).into_pipeline_data());
}
return Err(ShellError::GenericError(
"Select only allows row numbers for rows".into(),
"extra after row number".into(),
@ -172,11 +162,7 @@ fn select(
let mut vals = vec![];
for path in &columns {
//FIXME: improve implementation to not clone
match input_val.clone().follow_cell_path(
&path.members,
false,
ignore_errors,
) {
match input_val.clone().follow_cell_path(&path.members, false) {
Ok(fetcher) => {
allempty = false;
cols.push(path.into_string().replace('.', "_"));
@ -214,10 +200,7 @@ fn select(
let mut vals = vec![];
for path in &columns {
//FIXME: improve implementation to not clone
match x
.clone()
.follow_cell_path(&path.members, false, ignore_errors)
{
match x.clone().follow_cell_path(&path.members, false) {
Ok(value) => {
cols.push(path.into_string().replace('.', "_"));
vals.push(value);
@ -246,10 +229,7 @@ fn select(
for cell_path in columns {
// FIXME: remove clone
match v
.clone()
.follow_cell_path(&cell_path.members, false, ignore_errors)
{
match v.clone().follow_cell_path(&cell_path.members, false) {
Ok(result) => {
cols.push(cell_path.into_string().replace('.', "_"));
vals.push(result);

View File

@ -143,7 +143,7 @@ fn update(
ctrlc,
)
} 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 pre_elems = vec![];

View File

@ -165,7 +165,7 @@ fn upsert(
ctrlc,
)
} 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 pre_elems = vec![];

View File

@ -279,12 +279,10 @@ fn format_record(
.map(|path| PathMember::String {
val: path.to_string(),
span: *span,
optional: false,
})
.collect();
match data_as_value
.clone()
.follow_cell_path(&path_members, false, false)
{
match data_as_value.clone().follow_cell_path(&path_members, false) {
Ok(value_at_column) => {
output.push_str(value_at_column.into_string(", ", config).as_str())
}

View File

@ -277,9 +277,9 @@ fn convert_to_list(
&[PathMember::String {
val: header.into(),
span: head,
optional: false,
}],
false,
false,
),
_ => Ok(item.clone()),
};

View File

@ -934,8 +934,9 @@ fn convert_to_table(
let path = PathMember::String {
val: text.clone(),
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 {
Ok(val) => DeferredStyleComputation::Value { value: val },
@ -1321,8 +1322,12 @@ fn create_table2_entry(
match item {
Value::Record { .. } => {
let val = header.to_owned();
let path = PathMember::String { val, span: head };
let val = item.clone().follow_cell_path(&[path], false, false);
let path = PathMember::String {
val,
span: head,
optional: false,
};
let val = item.clone().follow_cell_path(&[path], false);
match val {
Ok(val) => convert_to_table2_entry(

View File

@ -71,7 +71,7 @@ fn cal_sees_pipeline_year() {
let actual = nu!(
cwd: ".", pipeline(
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]
fn skip_cell_rejection() {
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");
}

View File

@ -165,7 +165,7 @@ fn select_ignores_errors_successfully1() {
let actual = nu!(
cwd: ".", pipeline(
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!(
cwd: ".", pipeline(
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() {
let actual = nu!(
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());
@ -201,38 +201,10 @@ fn select_ignores_errors_successfully3() {
fn select_ignores_errors_successfully4() {
let actual = nu!(
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!(
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_eq!(actual.out, r#"[[foo]; [null], [null], [null]]"#.to_string());
assert!(actual.err.is_empty());
}

View File

@ -15,7 +15,7 @@ fn filters_by_unit_size_comparison() {
fn filters_with_nothing_comparison() {
let actual = nu!(
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");

View File

@ -319,10 +319,12 @@ fn get_converted_value(
PathMember::String {
val: name.to_string(),
span: env_span,
optional: false,
},
PathMember::String {
val: direction.to_string(),
span: env_span,
optional: false,
},
];

View File

@ -300,7 +300,7 @@ pub fn eval_expression(
Expr::FullCellPath(cell_path) => {
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::Overlay(_) => {
@ -484,7 +484,6 @@ pub fn eval_expression(
let vardata = lhs.follow_cell_path(
&[cell_path.tail[0].clone()],
false,
false,
)?;
match &cell_path.tail[0] {
PathMember::String { val, .. } => {

View File

@ -599,8 +599,12 @@ fn create_table2_entry_basic(
match item {
Value::Record { .. } => {
let val = header.to_owned();
let path = PathMember::String { val, span: head };
let val = item.clone().follow_cell_path(&[path], false, false);
let path = PathMember::String {
val,
span: head,
optional: false,
};
let val = item.clone().follow_cell_path(&[path], false);
match val {
Ok(val) => value_to_styled_string(&val, config, style_computer),
@ -627,8 +631,12 @@ fn create_table2_entry(
match item {
Value::Record { .. } => {
let val = header.to_owned();
let path = PathMember::String { val, span: head };
let val = item.clone().follow_cell_path(&[path], false, false);
let path = PathMember::String {
val,
span: head,
optional: false,
};
let val = item.clone().follow_cell_path(&[path], false);
match val {
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 {
val: header.to_owned(),
span: NuSpan::unknown(),
optional: false,
};
item.clone()
.follow_cell_path(&[path], false, false)
.follow_cell_path(&[path], false)
.unwrap_or_else(|_| item.clone())
}
item => item.clone(),

View File

@ -31,7 +31,7 @@ pub fn eval_constant(
Expr::FullCellPath(cell_path) => {
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),
// TODO: Better error conversion
Err(shell_error) => Err(ParseError::LabeledError(

View File

@ -941,10 +941,12 @@ pub fn parse_old_alias(
PathMember::String {
val: "scope".to_string(),
span: Span::new(0, 0),
optional: false,
},
PathMember::String {
val: "aliases".to_string(),
span: Span::new(0, 0),
optional: false,
},
];
let expr = Expression {

View File

@ -2015,54 +2015,100 @@ pub fn parse_variable_expr(
pub fn parse_cell_path(
working_set: &mut StateWorkingSet,
tokens: impl Iterator<Item = Token>,
mut expect_dot: bool,
expect_dot: bool,
expand_aliases_denylist: &[usize],
span: Span,
) -> (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 tail = vec![];
for path_element in tokens {
let bytes = working_set.get_span_contents(path_element.span);
if expect_dot {
expect_dot = false;
if bytes.len() != 1 || bytes[0] != b'.' {
error = error.or_else(|| Some(ParseError::Expected('.'.into(), path_element.span)));
match expected_token {
TokenType::Dot => {
if bytes.len() != 1 || bytes[0] != b'.' {
return (
tail,
Some(ParseError::Expected('.'.into(), path_element.span)),
);
}
expected_token = TokenType::PathMember;
}
} else {
expect_dot = true;
match parse_int(bytes, path_element.span) {
(
Expression {
expr: Expr::Int(val),
span,
..
},
None,
) => tail.push(PathMember::Int {
val: val as usize,
span,
}),
_ => {
let (result, err) =
parse_string(working_set, path_element.span, expand_aliases_denylist);
error = error.or(err);
match result {
TokenType::QuestionOrDot => {
if bytes.len() == 1 && bytes[0] == b'.' {
expected_token = TokenType::PathMember;
} else if bytes.len() == 1 && bytes[0] == b'?' {
if let Some(last) = tail.last_mut() {
match last {
PathMember::String {
ref mut optional, ..
} => *optional = true,
PathMember::Int {
ref mut optional, ..
} => *optional = true,
}
}
expected_token = TokenType::Dot;
} else {
return (
tail,
Some(ParseError::Expected(". or ?".into(), path_element.span)),
);
}
}
TokenType::PathMember => {
match parse_int(bytes, path_element.span) {
(
Expression {
expr: Expr::String(string),
expr: Expr::Int(val),
span,
..
} => {
tail.push(PathMember::String { val: string, span });
}
_ => {
error =
error.or_else(|| Some(ParseError::Expected("string".into(), span)));
},
None,
) => tail.push(PathMember::Int {
val: val as usize,
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 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);
let mut tokens = tokens.into_iter().peekable();
@ -2202,13 +2248,7 @@ pub fn parse_full_cell_path(
);
};
let (tail, err) = parse_cell_path(
working_set,
tokens,
expect_dot,
expand_aliases_denylist,
span,
);
let (tail, err) = parse_cell_path(working_set, tokens, expect_dot, expand_aliases_denylist);
error = error.or(err);
(
@ -4597,13 +4637,13 @@ pub fn parse_value(
let source = working_set.get_span_contents(span);
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);
let tokens = tokens.into_iter().peekable();
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);
(

View File

@ -1,6 +1,7 @@
use nu_parser::ParseError;
use nu_parser::*;
use nu_protocol::ast::Call;
use nu_protocol::ast::{Call, PathMember};
use nu_protocol::Span;
use nu_protocol::{
ast::{Expr, Expression, PipelineElement},
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]
pub fn parse_binary_with_hex_format() {
let engine_state = EngineState::new();

View File

@ -5,15 +5,45 @@ use std::fmt::Write;
#[derive(Debug, Clone, PartialOrd, Serialize, Deserialize)]
pub enum PathMember {
String { val: String, span: Span },
Int { val: usize, span: Span },
String {
val: String,
span: Span,
optional: bool,
},
Int {
val: usize,
span: Span,
optional: bool,
},
}
impl PartialEq for PathMember {
fn eq(&self, other: &Self) -> bool {
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,
}
}
@ -42,6 +72,19 @@ impl CellPath {
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)]

View File

@ -322,7 +322,6 @@ impl PipelineData {
cell_path: &[PathMember],
head: Span,
insensitive: bool,
ignore_errors: bool,
) -> Result<Value, ShellError> {
match self {
// FIXME: there are probably better ways of doing this
@ -330,8 +329,8 @@ impl PipelineData {
vals: stream.collect(),
span: head,
}
.follow_cell_path(cell_path, insensitive, ignore_errors),
PipelineData::Value(v, ..) => v.follow_cell_path(cell_path, insensitive, ignore_errors),
.follow_cell_path(cell_path, insensitive),
PipelineData::Value(v, ..) => v.follow_cell_path(cell_path, insensitive),
_ => Err(ShellError::IOError("can't follow stream paths".into())),
}
}

View File

@ -302,6 +302,7 @@ impl FromValue for CellPath {
members: vec![PathMember::String {
val: val.clone(),
span,
optional: false,
}],
}),
Value::Int { val, span } => {
@ -312,6 +313,7 @@ impl FromValue for CellPath {
members: vec![PathMember::Int {
val: *val as usize,
span: *span,
optional: false,
}],
})
}

View File

@ -681,9 +681,8 @@ impl Value {
self,
cell_path: &[PathMember],
insensitive: bool,
nullify_errors: bool,
) -> 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(
@ -691,24 +690,20 @@ impl Value {
cell_path: &[PathMember],
insensitive: bool,
) -> 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(
self,
cell_path: &[PathMember],
insensitive: bool,
nullify_errors: bool, // Turn all errors into Value::Nothing
from_user_input: bool,
) -> Result<Value, ShellError> {
let mut current = self;
// TODO remove this
macro_rules! err_or_null {
($e:expr, $span:expr) => {
return if nullify_errors {
Ok(Value::nothing($span))
} else {
Err($e)
}
return Err($e)
};
}
for member in cell_path {
@ -718,12 +713,15 @@ impl Value {
PathMember::Int {
val: count,
span: origin_span,
optional,
} => {
// Treat a numeric path member as `select <val>`
match &mut current {
Value::List { vals: val, .. } => {
if let Some(item) = val.get(*count) {
current = item.clone();
} else if *optional {
current = Value::nothing(*origin_span);
} else if val.is_empty() {
err_or_null!(
ShellError::AccessEmptyContent { span: *origin_span },
@ -742,6 +740,8 @@ impl Value {
Value::Binary { val, .. } => {
if let Some(item) = val.get(*count) {
current = Value::int(*item as i64, *origin_span);
} else if *optional {
current = Value::nothing(*origin_span);
} else if val.is_empty() {
err_or_null!(
ShellError::AccessEmptyContent { span: *origin_span },
@ -760,6 +760,8 @@ impl Value {
Value::Range { val, .. } => {
if let Some(item) = val.clone().into_range_iter(None)?.nth(*count) {
current = item.clone();
} else if *optional {
current = Value::nothing(*origin_span);
} else {
err_or_null!(
ShellError::AccessBeyondEndOfStream { span: *origin_span },
@ -768,7 +770,19 @@ impl Value {
}
}
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,
// so only use this message for them.
@ -792,6 +806,7 @@ impl Value {
PathMember::String {
val: column_name,
span: origin_span,
optional,
} => match &mut current {
Value::Record { cols, vals, span } => {
let cols = cols.clone();
@ -806,6 +821,8 @@ impl Value {
}
}) {
current = found.1.clone();
} else if *optional {
current = Value::nothing(*origin_span);
} else {
if from_user_input {
if let Some(suggestion) = did_you_mean(&cols, column_name) {
@ -830,6 +847,8 @@ impl Value {
if columns.contains(&column_name.as_str()) {
current = val.get_column_value(column_name)?;
} else if *optional {
current = Value::nothing(*origin_span);
} else {
if from_user_input {
if let Some(suggestion) = did_you_mean(&columns, column_name) {
@ -852,10 +871,9 @@ impl Value {
// String access of Lists always means Table access.
// Create a List which contains each matching value for contained
// records in the source list.
// If nullify_errors is true, table holes are converted to null.
Value::List { vals, span } => {
// TODO: this should stream instead of collecting
let mut output = vec![];
let mut found_at_least_1_value = false;
for val in vals {
// only look in records; this avoids unintentionally recursing into deeply nested tables
if matches!(val, Value::Record { .. }) {
@ -863,69 +881,40 @@ impl Value {
&[PathMember::String {
val: column_name.clone(),
span: *origin_span,
optional: *optional,
}],
insensitive,
nullify_errors,
) {
found_at_least_1_value = true;
output.push(result);
} else {
// Consider [{a:1 b:2} {a:3}]:
// [{a:1 b:2} {a:3}].b should error.
// [{a:1 b:2} {a:3}].b.1 should error.
// [{a:1 b:2} {a:3}].b.0 should NOT error because the path can find a proper value (2)
// 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),
}),
}
return Err(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 {
// See comment above.
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),
}),
}
return Err(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 {
vals: output,
span: *span,
};
} else {
err_or_null!(
ShellError::CantFindColumn {
col_name: column_name.to_string(),
span: *origin_span,
src_span: *span
},
*origin_span
);
}
current = Value::List {
vals: output,
span: *span,
};
}
Value::CustomValue { val, .. } => {
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),
x => {
err_or_null!(
@ -956,7 +945,7 @@ impl Value {
) -> Result<(), ShellError> {
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 {
Value::Error { error } => Err(*error),
@ -974,6 +963,7 @@ impl Value {
PathMember::String {
val: col_name,
span,
..
} => match self {
Value::List { vals, .. } => {
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, .. } => {
if let Some(v) = vals.get_mut(*row_num) {
v.upsert_data_at_cell_path(&cell_path[1..], new_val)?
@ -1094,7 +1086,7 @@ impl Value {
) -> Result<(), ShellError> {
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 {
Value::Error { error } => Err(*error),
@ -1113,6 +1105,7 @@ impl Value {
PathMember::String {
val: col_name,
span,
..
} => match self {
Value::List { vals, .. } => {
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, .. } => {
if let Some(v) = vals.get_mut(*row_num) {
v.update_data_at_cell_path(&cell_path[1..], new_val)?
@ -1221,6 +1216,7 @@ impl Value {
PathMember::String {
val: col_name,
span,
..
} => match self {
Value::List { vals, .. } => {
for val in vals.iter_mut() {
@ -1289,7 +1285,9 @@ impl Value {
src_span: v.span()?,
}),
},
PathMember::Int { val: row_num, span } => match self {
PathMember::Int {
val: row_num, span, ..
} => match self {
Value::List { vals, .. } => {
if vals.get_mut(*row_num).is_some() {
vals.remove(*row_num);
@ -1316,6 +1314,7 @@ impl Value {
PathMember::String {
val: col_name,
span,
..
} => match self {
Value::List { vals, .. } => {
for val in vals.iter_mut() {
@ -1380,7 +1379,9 @@ impl Value {
src_span: v.span()?,
}),
},
PathMember::Int { val: row_num, span } => match self {
PathMember::Int {
val: row_num, span, ..
} => match self {
Value::List { vals, .. } => {
if let Some(v) = vals.get_mut(*row_num) {
v.remove_data_at_cell_path(&cell_path[1..])
@ -1414,6 +1415,7 @@ impl Value {
PathMember::String {
val: col_name,
span,
..
} => match self {
Value::List { vals, .. } => {
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, .. } => {
if let Some(v) = vals.get_mut(*row_num) {
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> {
if let Some(cell_path) = &self.cell_path {
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)?;

View File

@ -17,6 +17,21 @@ fn record_single_field_success() -> TestResult {
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]
fn record_single_field_failure() -> TestResult {
fail_test("{foo: 'bar'}.foobar", "")
@ -27,6 +42,21 @@ fn record_int_failure() -> TestResult {
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]
fn nested_record_field_success() -> TestResult {
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", "")
}
#[test]
fn nested_record_field_optional() -> TestResult {
run_test("{foo: {bar: 'baz'} }.foo.asdf? | to nuon", "null")
}
#[test]
fn record_with_nested_list_success() -> TestResult {
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")
}
#[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]
fn list_row_access_failure() -> TestResult {
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
#[test]
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]
fn string_cell_path() -> TestResult {
run_test(
@ -257,9 +284,9 @@ fn length_defaulted_columns() -> TestResult {
#[test]
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(
"([{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",
)
}
@ -267,7 +294,7 @@ fn nullify_errors() -> TestResult {
#[test]
fn nullify_holes() -> TestResult {
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",
)
}