mirror of
https://github.com/nushell/nushell.git
synced 2025-06-30 22:50:14 +02: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:
@ -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(
|
||||
|
Reference in New Issue
Block a user