mirror of
https://github.com/nushell/nushell.git
synced 2024-11-25 09:53:43 +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 {
|
||||
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,
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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(),
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
});
|
||||
|
||||
|
@ -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?);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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![];
|
||||
|
||||
|
@ -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![];
|
||||
|
||||
|
@ -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())
|
||||
}
|
||||
|
@ -277,9 +277,9 @@ fn convert_to_list(
|
||||
&[PathMember::String {
|
||||
val: header.into(),
|
||||
span: head,
|
||||
optional: false,
|
||||
}],
|
||||
false,
|
||||
false,
|
||||
),
|
||||
_ => Ok(item.clone()),
|
||||
};
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
"#
|
||||
));
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -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, .. } => {
|
||||
|
@ -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(
|
||||
|
@ -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(),
|
||||
|
@ -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(
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
|
||||
(
|
||||
|
@ -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();
|
||||
|
@ -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)]
|
||||
|
@ -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())),
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
@ -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)?
|
||||
|
@ -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)?;
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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",
|
||||
)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user