forked from extern/nushell
# Description This closes #7498, as well as fixes an issue reported in https://github.com/nushell/nushell/pull/7002#issuecomment-1368340773 BEFORE: ``` 〉[{foo: 'bar'} {}] | get foo Error: nu:🐚:column_not_found (link) × Cannot find column ╭─[entry #5:1:1] 1 │ [{foo: 'bar'} {}] | get foo · ────────┬──────── ─┬─ · │ ╰── value originates here · ╰── cannot find column 'Empty cell' ╰──── 〉[{foo: 'bar'} {}].foo ╭───┬─────╮ │ 0 │ bar │ │ 1 │ │ ╰───┴─────╯ ``` AFTER: ``` 〉[{foo: 'bar'} {}] | get foo Error: nu:🐚:column_not_found (link) × Cannot find column ╭─[entry #1:1:1] 1 │ [{foo: 'bar'} {}] | get foo · ─┬ ─┬─ · │ ╰── cannot find column 'foo' · ╰── value originates here ╰──── 〉[{foo: 'bar'} {}].foo Error: nu:🐚:column_not_found (link) × Cannot find column ╭─[entry #3:1:1] 1 │ [{foo: 'bar'} {}].foo · ─┬ ─┬─ · │ ╰── cannot find column 'foo' · ╰── value originates here ╰──── ``` EDIT: This also changes the semantics of `get`/`select` `-i` somewhat. I've decided to leave it like this because it works more intuitively with `default` and `compact`. BEFORE: ``` 〉[{a:1} {b:2} {a:3}] | select -i foo | to nuon null ``` AFTER: ``` 〉[{a:1} {b:2} {a:3}] | select -i foo | to nuon [[foo]; [null], [null], [null]] ``` # User-Facing Changes See above. EDIT: the issue with holes in cases like ` [{foo: 'bar'} {}].foo.0` versus ` [{foo: 'bar'} {}].0.foo` has been resolved. # Tests + Formatting Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass # After Submitting If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date.
This commit is contained in:
@ -468,6 +468,18 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Value into String, but propagate errors.
|
||||
pub fn nonerror_into_string(
|
||||
&self,
|
||||
separator: &str,
|
||||
config: &Config,
|
||||
) -> Result<String, ShellError> {
|
||||
match self {
|
||||
Value::Error { error } => Err(error.to_owned()),
|
||||
_ => Ok(self.into_string(separator, config)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert Value into string. Note that Streams will be consumed.
|
||||
pub fn into_string(&self, separator: &str, config: &Config) -> String {
|
||||
match self {
|
||||
@ -627,8 +639,9 @@ impl Value {
|
||||
self,
|
||||
cell_path: &[PathMember],
|
||||
insensitive: bool,
|
||||
nullify_errors: bool,
|
||||
) -> Result<Value, ShellError> {
|
||||
self.follow_cell_path_helper(cell_path, insensitive, true)
|
||||
self.follow_cell_path_helper(cell_path, insensitive, nullify_errors, true)
|
||||
}
|
||||
|
||||
pub fn follow_cell_path_not_from_user_input(
|
||||
@ -636,16 +649,26 @@ impl Value {
|
||||
cell_path: &[PathMember],
|
||||
insensitive: bool,
|
||||
) -> Result<Value, ShellError> {
|
||||
self.follow_cell_path_helper(cell_path, insensitive, false)
|
||||
self.follow_cell_path_helper(cell_path, insensitive, false, 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;
|
||||
macro_rules! err_or_null {
|
||||
($e:expr, $span:expr) => {
|
||||
return if nullify_errors {
|
||||
Ok(Value::nothing($span))
|
||||
} else {
|
||||
Err($e)
|
||||
}
|
||||
};
|
||||
}
|
||||
for member in cell_path {
|
||||
// FIXME: this uses a few extra clones for simplicity, but there may be a way
|
||||
// to traverse the path without them
|
||||
@ -660,34 +683,61 @@ impl Value {
|
||||
if let Some(item) = val.get(*count) {
|
||||
current = item.clone();
|
||||
} else if val.is_empty() {
|
||||
return Err(ShellError::AccessEmptyContent(*origin_span))
|
||||
err_or_null!(
|
||||
ShellError::AccessEmptyContent(*origin_span),
|
||||
*origin_span
|
||||
)
|
||||
} else {
|
||||
return Err(ShellError::AccessBeyondEnd(val.len() - 1, *origin_span));
|
||||
err_or_null!(
|
||||
ShellError::AccessBeyondEnd(val.len() - 1, *origin_span),
|
||||
*origin_span
|
||||
);
|
||||
}
|
||||
}
|
||||
Value::Binary { val, .. } => {
|
||||
if let Some(item) = val.get(*count) {
|
||||
current = Value::int(*item as i64, *origin_span);
|
||||
} else if val.is_empty() {
|
||||
return Err(ShellError::AccessEmptyContent(*origin_span))
|
||||
err_or_null!(
|
||||
ShellError::AccessEmptyContent(*origin_span),
|
||||
*origin_span
|
||||
)
|
||||
} else {
|
||||
return Err(ShellError::AccessBeyondEnd(val.len() - 1, *origin_span));
|
||||
err_or_null!(
|
||||
ShellError::AccessBeyondEnd(val.len() - 1, *origin_span),
|
||||
*origin_span
|
||||
);
|
||||
}
|
||||
}
|
||||
Value::Range { val, .. } => {
|
||||
if let Some(item) = val.clone().into_range_iter(None)?.nth(*count) {
|
||||
current = item.clone();
|
||||
} else {
|
||||
return Err(ShellError::AccessBeyondEndOfStream(*origin_span));
|
||||
err_or_null!(
|
||||
ShellError::AccessBeyondEndOfStream(*origin_span),
|
||||
*origin_span
|
||||
);
|
||||
}
|
||||
}
|
||||
Value::CustomValue { val, .. } => {
|
||||
current = val.follow_path_int(*count, *origin_span)?;
|
||||
}
|
||||
// Records (and tables) are the only built-in which support column names,
|
||||
// so only use this message for them.
|
||||
Value::Record { .. } => {
|
||||
err_or_null!(ShellError::TypeMismatchGenericMessage {
|
||||
err_message: "Can't access record values with a row index. Try specifying a column name instead".into(),
|
||||
span: *origin_span, }, *origin_span)
|
||||
}
|
||||
Value::Error { error } => return Err(error.to_owned()),
|
||||
x => {
|
||||
return Err(ShellError::TypeMismatchGenericMessage {
|
||||
err_message: format!("Can't access {} values with a row index. Try specifying a column name instead", x.get_type().to_shape()),
|
||||
span: *origin_span, })
|
||||
err_or_null!(
|
||||
ShellError::IncompatiblePathAccess(
|
||||
format!("{}", x.get_type()),
|
||||
*origin_span,
|
||||
),
|
||||
*origin_span
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -711,16 +761,26 @@ impl Value {
|
||||
} else {
|
||||
if from_user_input {
|
||||
if let Some(suggestion) = did_you_mean(&cols, column_name) {
|
||||
return Err(ShellError::DidYouMean(suggestion, *origin_span));
|
||||
err_or_null!(
|
||||
ShellError::DidYouMean(suggestion, *origin_span),
|
||||
*origin_span
|
||||
);
|
||||
}
|
||||
}
|
||||
return Err(ShellError::CantFindColumn(
|
||||
column_name.to_string(),
|
||||
*origin_span,
|
||||
span,
|
||||
));
|
||||
err_or_null!(
|
||||
ShellError::CantFindColumn(
|
||||
column_name.to_string(),
|
||||
*origin_span,
|
||||
span,
|
||||
),
|
||||
*origin_span
|
||||
);
|
||||
}
|
||||
}
|
||||
// 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 } => {
|
||||
let mut output = vec![];
|
||||
let mut found_at_least_1_value = false;
|
||||
@ -733,14 +793,50 @@ impl Value {
|
||||
span: *origin_span,
|
||||
}],
|
||||
insensitive,
|
||||
nullify_errors,
|
||||
) {
|
||||
found_at_least_1_value = true;
|
||||
output.push(result);
|
||||
} else {
|
||||
output.push(Value::Nothing { span: *span });
|
||||
// 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: ShellError::CantFindColumn(
|
||||
column_name.to_string(),
|
||||
*origin_span,
|
||||
// Get the exact span of the value, falling back to
|
||||
// the list's span if it's a Value::Empty
|
||||
val.span().unwrap_or(*span),
|
||||
),
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
output.push(Value::Nothing { span: *span });
|
||||
// See comment above.
|
||||
output.push(if nullify_errors {
|
||||
Value::nothing(*origin_span)
|
||||
} else {
|
||||
Value::Error {
|
||||
error: ShellError::CantFindColumn(
|
||||
column_name.to_string(),
|
||||
*origin_span,
|
||||
// Get the exact span of the value, falling back to
|
||||
// the list's span if it's a Value::Empty
|
||||
val.span().unwrap_or(*span),
|
||||
),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if found_at_least_1_value {
|
||||
@ -749,23 +845,39 @@ impl Value {
|
||||
span: *span,
|
||||
};
|
||||
} else {
|
||||
return Err(ShellError::NotFound(*span));
|
||||
err_or_null!(
|
||||
ShellError::CantFindColumn(
|
||||
column_name.to_string(),
|
||||
*origin_span,
|
||||
*span,
|
||||
),
|
||||
*origin_span
|
||||
);
|
||||
}
|
||||
}
|
||||
Value::CustomValue { val, .. } => {
|
||||
current = val.follow_path_string(column_name.clone(), *origin_span)?;
|
||||
}
|
||||
Value::Error { error } => err_or_null!(error.to_owned(), *origin_span),
|
||||
x => {
|
||||
return Err(ShellError::IncompatiblePathAccess(
|
||||
format!("{}", x.get_type()),
|
||||
*origin_span,
|
||||
))
|
||||
err_or_null!(
|
||||
ShellError::IncompatiblePathAccess(
|
||||
format!("{}", x.get_type()),
|
||||
*origin_span,
|
||||
),
|
||||
*origin_span
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(current)
|
||||
// If a single Value::Error was produced by the above (which won't happen if nullify_errors is true), unwrap it now.
|
||||
// Note that Value::Errors inside Lists remain as they are, so that the rest of the list can still potentially be used.
|
||||
if let Value::Error { error } = current {
|
||||
Err(error)
|
||||
} else {
|
||||
Ok(current)
|
||||
}
|
||||
}
|
||||
|
||||
/// Follow a given cell path into the value: for example accessing select elements in a stream or list
|
||||
@ -776,7 +888,7 @@ impl Value {
|
||||
) -> Result<(), ShellError> {
|
||||
let orig = self.clone();
|
||||
|
||||
let new_val = callback(&orig.follow_cell_path(cell_path, false)?);
|
||||
let new_val = callback(&orig.follow_cell_path(cell_path, false, false)?);
|
||||
|
||||
match new_val {
|
||||
Value::Error { error } => Err(error),
|
||||
@ -829,6 +941,7 @@ impl Value {
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Error { error } => return Err(error.to_owned()),
|
||||
v => {
|
||||
return Err(ShellError::CantFindColumn(
|
||||
col_name.to_string(),
|
||||
@ -865,6 +978,7 @@ impl Value {
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Error { error } => return Err(error.to_owned()),
|
||||
v => {
|
||||
return Err(ShellError::CantFindColumn(
|
||||
col_name.to_string(),
|
||||
@ -885,6 +999,7 @@ impl Value {
|
||||
return Err(ShellError::InsertAfterNextFreeIndex(vals.len(), *span));
|
||||
}
|
||||
}
|
||||
Value::Error { error } => return Err(error.to_owned()),
|
||||
v => return Err(ShellError::NotAList(*span, v.span()?)),
|
||||
},
|
||||
},
|
||||
@ -903,7 +1018,7 @@ impl Value {
|
||||
) -> Result<(), ShellError> {
|
||||
let orig = self.clone();
|
||||
|
||||
let new_val = callback(&orig.follow_cell_path(cell_path, false)?);
|
||||
let new_val = callback(&orig.follow_cell_path(cell_path, false, false)?);
|
||||
|
||||
match new_val {
|
||||
Value::Error { error } => Err(error),
|
||||
@ -948,6 +1063,7 @@ impl Value {
|
||||
));
|
||||
}
|
||||
}
|
||||
Value::Error { error } => return Err(error.to_owned()),
|
||||
v => {
|
||||
return Err(ShellError::CantFindColumn(
|
||||
col_name.to_string(),
|
||||
@ -981,6 +1097,7 @@ impl Value {
|
||||
));
|
||||
}
|
||||
}
|
||||
Value::Error { error } => return Err(error.to_owned()),
|
||||
v => {
|
||||
return Err(ShellError::CantFindColumn(
|
||||
col_name.to_string(),
|
||||
@ -999,6 +1116,7 @@ impl Value {
|
||||
return Err(ShellError::AccessBeyondEnd(vals.len() - 1, *span));
|
||||
}
|
||||
}
|
||||
Value::Error { error } => return Err(error.to_owned()),
|
||||
v => return Err(ShellError::NotAList(*span, v.span()?)),
|
||||
},
|
||||
},
|
||||
|
Reference in New Issue
Block a user