diff --git a/crates/nu-source/src/meta.rs b/crates/nu-source/src/meta.rs index bca7135da9..ef65ed7af8 100644 --- a/crates/nu-source/src/meta.rs +++ b/crates/nu-source/src/meta.rs @@ -393,7 +393,6 @@ impl Tag { } } -#[allow(unused)] pub fn tag_for_tagged_list(mut iter: impl Iterator) -> Tag { let first = iter.next(); @@ -410,7 +409,6 @@ pub fn tag_for_tagged_list(mut iter: impl Iterator) -> Tag { } } -#[allow(unused)] pub fn span_for_spanned_list(mut iter: impl Iterator) -> Span { let first = iter.next(); @@ -471,6 +469,12 @@ impl Span { } } + pub fn since(&self, other: impl Into) -> Span { + let other = other.into(); + + Span::new(other.start, self.end) + } + pub fn until(&self, other: impl Into) -> Span { let other = other.into(); diff --git a/src/commands/get.rs b/src/commands/get.rs index ddc1ec2382..032e4a1114 100644 --- a/src/commands/get.rs +++ b/src/commands/get.rs @@ -2,13 +2,14 @@ use crate::commands::WholeStreamCommand; use crate::data::base::shape::Shapes; use crate::prelude::*; use futures_util::pin_mut; +use indexmap::set::IndexSet; use log::trace; use nu_errors::ShellError; use nu_protocol::{ - did_you_mean, ColumnPath, ReturnSuccess, ReturnValue, Signature, SyntaxShape, UntaggedValue, - Value, + did_you_mean, ColumnPath, PathMember, ReturnSuccess, ReturnValue, Signature, SyntaxShape, + UnspannedPathMember, UntaggedValue, Value, }; -use nu_source::{span_for_spanned_list, PrettyDebug}; +use nu_source::span_for_spanned_list; use nu_value_ext::get_data_by_column_path; pub struct Get; @@ -50,42 +51,125 @@ pub fn get_column_path(path: &ColumnPath, obj: &Value) -> Result 2 { 1 } else { 0 }) - { - Some(last_field) => last_field.span, - None => column_path_tried.span, - }; + let path_members_span = span_for_spanned_list(fields.members().iter().map(|p| p.span)); - let primary_label = format!( - "There isn't a row indexed at {}", - column_path_tried.display() - ); + match &obj_source.value { + UntaggedValue::Table(rows) => match column_path_tried { + PathMember { + unspanned: UnspannedPathMember::String(column), + .. + } => { + let primary_label = format!("There isn't a column named '{}'", &column); - let secondary_label = if total == 1 { - "The table only has 1 row".to_owned() - } else { - format!("The table only has {} rows (0 to {})", total, total - 1) - }; + let suggestions: IndexSet<_> = rows + .iter() + .filter_map(|r| did_you_mean(&r, &column_path_tried)) + .map(|s| s[0].1.to_owned()) + .collect(); + let mut existing_columns: IndexSet<_> = IndexSet::default(); + let mut names: Vec = vec![]; - return ShellError::labeled_error_with_secondary( - "Row not found", - primary_label, - column_path_tried.span, - secondary_label, - end_tag, - ); + for row in rows { + for field in row.data_descriptors() { + if !existing_columns.contains(&field[..]) { + existing_columns.insert(field.clone()); + names.push(field); + } + } + } + + if names.is_empty() { + return ShellError::labeled_error_with_secondary( + "Unknown column", + primary_label, + column_path_tried.span, + "Appears to contain rows. Try indexing instead.", + column_path_tried.span.since(path_members_span), + ); + } else { + return ShellError::labeled_error_with_secondary( + "Unknown column", + primary_label, + column_path_tried.span, + format!( + "Perhaps you meant '{}'? Columns available: {}", + suggestions + .iter() + .map(|x| x.to_owned()) + .collect::>() + .join(","), + names.join(",") + ), + column_path_tried.span.since(path_members_span), + ); + }; + } + PathMember { + unspanned: UnspannedPathMember::Int(idx), + .. + } => { + let total = rows.len(); + + let secondary_label = if total == 1 { + "The table only has 1 row".to_owned() + } else { + format!("The table only has {} rows (0 to {})", total, total - 1) + }; + + return ShellError::labeled_error_with_secondary( + "Row not found", + format!("There isn't a row indexed at {}", idx), + column_path_tried.span, + secondary_label, + column_path_tried.span.since(path_members_span), + ); + } + }, + UntaggedValue::Row(columns) => match column_path_tried { + PathMember { + unspanned: UnspannedPathMember::String(column), + .. + } => { + let primary_label = format!("There isn't a column named '{}'", &column); + + if let Some(suggestions) = did_you_mean(&obj_source, column_path_tried) { + return ShellError::labeled_error_with_secondary( + "Unknown column", + primary_label, + column_path_tried.span, + format!( + "Perhaps you meant '{}'? Columns available: {}", + suggestions[0].1, + &obj_source.data_descriptors().join(",") + ), + column_path_tried.span.since(path_members_span), + ); + } + } + PathMember { + unspanned: UnspannedPathMember::Int(idx), + .. + } => { + return ShellError::labeled_error_with_secondary( + "No rows available", + format!("A row at '{}' can't be indexed.", &idx), + column_path_tried.span, + format!( + "Appears to contain columns. Columns available: {}", + columns.keys().join(",") + ), + column_path_tried.span.since(path_members_span), + ) + } + }, + _ => {} } if let Some(suggestions) = did_you_mean(&obj_source, column_path_tried) { return ShellError::labeled_error( "Unknown column", format!("did you mean '{}'?", suggestions[0].1), - span_for_spanned_list(fields.members().iter().map(|p| p.span)), + column_path_tried.span.since(path_members_span), ); } diff --git a/src/utils.rs b/src/utils.rs index 1d45307636..22a743a0be 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -316,18 +316,10 @@ mod tests { loc: fixtures().join("cargo_sample.toml"), at: 0 }, - Res { - loc: fixtures().join("fileA.txt"), - at: 0 - }, Res { loc: fixtures().join("jonathan.xml"), at: 0 }, - Res { - loc: fixtures().join("nested_uniq.json"), - at: 0 - }, Res { loc: fixtures().join("sample.bson"), at: 0 diff --git a/tests/commands/append.rs b/tests/commands/append.rs index 8d5fc36b3c..cbb0994f39 100644 --- a/tests/commands/append.rs +++ b/tests/commands/append.rs @@ -1,17 +1,30 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; use nu_test_support::{nu, pipeline}; #[test] fn adds_a_row_to_the_end() { - let actual = nu!( - cwd: "tests/fixtures/formats", pipeline( - r#" - open fileA.txt - | lines - | append "testme" - | nth 3 - | echo $it - "# - )); + Playground::setup("append_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.txt", + r#" + Andrés N. Robalino + Jonathan Turner + Yehuda Katz + "#, + )]); - assert_eq!(actual, "testme"); + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.txt + | lines + | append "pollo loco" + | nth 3 + | echo $it + "# + )); + + assert_eq!(actual, "pollo loco"); + }) } diff --git a/tests/commands/get.rs b/tests/commands/get.rs index f50a1e4be1..95e3d3dc54 100644 --- a/tests/commands/get.rs +++ b/tests/commands/get.rs @@ -154,13 +154,22 @@ fn errors_fetching_by_column_not_present() { "# )); - assert!(actual.contains("Unknown column")); - assert!(actual.contains("did you mean 'taconushell'?")); + assert!( + actual.contains("Unknown column"), + format!("actual: {:?}", actual) + ); + assert!( + actual.contains("There isn't a column named 'taco'"), + format!("actual: {:?}", actual) + ); + assert!( + actual.contains("Perhaps you meant 'taconushell'?"), + format!("actual: {:?}", actual) + ) }) } #[test] -#[should_panic] fn errors_fetching_by_column_using_a_number() { Playground::setup("get_test_7", |dirs, sandbox| { sandbox.with_files(vec![FileWithContent( @@ -175,12 +184,22 @@ fn errors_fetching_by_column_using_a_number() { cwd: dirs.test(), pipeline( r#" open sample.toml - | get spanish_lesson.9 + | get spanish_lesson.0 "# )); - assert!(actual.contains("No rows available")); - assert!(actual.contains(r#"Not a table. Perhaps you meant to get the column "0" instead?"#)) + assert!( + actual.contains("No rows available"), + format!("actual: {:?}", actual) + ); + assert!( + actual.contains("A row at '0' can't be indexed."), + format!("actual: {:?}", actual) + ); + assert!( + actual.contains("Appears to contain columns. Columns available: 0"), + format!("actual: {:?}", actual) + ) }) } #[test] diff --git a/tests/commands/mod.rs b/tests/commands/mod.rs index 80c2d48ca8..5ee8355917 100644 --- a/tests/commands/mod.rs +++ b/tests/commands/mod.rs @@ -18,6 +18,7 @@ mod mkdir; mod mv; mod open; mod parse; +mod pick; mod prepend; mod range; mod reverse; diff --git a/tests/commands/parse.rs b/tests/commands/parse.rs index 805e50fb18..d78abc1da5 100644 --- a/tests/commands/parse.rs +++ b/tests/commands/parse.rs @@ -1,17 +1,30 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; use nu_test_support::{nu, pipeline}; #[test] fn extracts_fields_from_the_given_the_pattern() { - let actual = nu!( - cwd: "tests/fixtures/formats", pipeline( - r#" - open fileA.txt - | parse "{Name}={Value}" - | nth 1 - | get Value - | echo $it - "# - )); + Playground::setup("parse_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "key_value_separated_arepa_ingredients.txt", + r#" + VAR1=Cheese + VAR2=JonathanParsed + VAR3=NushellSecretIngredient + "#, + )]); - assert_eq!(actual, "StupidLongName"); + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open key_value_separated_arepa_ingredients.txt + | parse "{Name}={Value}" + | nth 1 + | get Value + | echo $it + "# + )); + + assert_eq!(actual, "JonathanParsed"); + }) } diff --git a/tests/commands/pick.rs b/tests/commands/pick.rs new file mode 100644 index 0000000000..3d91cc7e0a --- /dev/null +++ b/tests/commands/pick.rs @@ -0,0 +1,57 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; +use nu_test_support::{nu, nu_error, pipeline}; + +#[test] +fn columns() { + Playground::setup("pick_by_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.csv", + r#" + first_name,last_name,rusty_at,type + Andrés,Robalino,10/11/2013,A + Jonathan,Turner,10/12/2013,B + Yehuda,Katz,10/11/2013,A + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.csv + | pick rusty_at last_name + | nth 0 + | get last_name + | echo $it + "# + )); + + assert_eq!(actual, "Robalino"); + }) +} + +#[should_panic] +#[test] +fn errors_if_given_unknown_column_name_is_missing() { + Playground::setup("pick_test_2", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.csv", + r#" + first_name,last_name,rusty_at,type + Andrés,Robalino,10/11/2013,A + Jonathan,Turner,10/12/2013,B + Yehuda,Katz,10/11/2013,A + "#, + )]); + + let actual = nu_error!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.csv + | pick rrusty_at + "# + )); + + assert!(actual.contains("Unknown column")); + }) +} diff --git a/tests/commands/prepend.rs b/tests/commands/prepend.rs index f728dadfab..288fad5528 100644 --- a/tests/commands/prepend.rs +++ b/tests/commands/prepend.rs @@ -1,17 +1,30 @@ +use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; +use nu_test_support::playground::Playground; use nu_test_support::{nu, pipeline}; #[test] fn adds_a_row_to_the_beginning() { - let actual = nu!( - cwd: "tests/fixtures/formats", pipeline( - r#" - open fileA.txt - | lines - | prepend "testme" - | nth 0 - | echo $it - "# - )); + Playground::setup("prepend_test_1", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "los_tres_caballeros.txt", + r#" + Andrés N. Robalino + Jonathan Turner + Yehuda Katz + "#, + )]); - assert_eq!(actual, "testme"); + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open los_tres_caballeros.txt + | lines + | prepend "pollo loco" + | nth 0 + | echo $it + "# + )); + + assert_eq!(actual, "pollo loco"); + }) } diff --git a/tests/commands/uniq.rs b/tests/commands/uniq.rs index f908224004..23c9d667ac 100644 --- a/tests/commands/uniq.rs +++ b/tests/commands/uniq.rs @@ -3,7 +3,7 @@ use nu_test_support::playground::Playground; use nu_test_support::{nu, pipeline}; #[test] -fn uniq_rows() { +fn removes_duplicate_rows() { Playground::setup("uniq_test_1", |dirs, sandbox| { sandbox.with_files(vec![FileWithContentToBeTrimmed( "los_tres_caballeros.csv", @@ -31,39 +31,9 @@ fn uniq_rows() { }) } -#[test] -fn uniq_columns() { - Playground::setup("uniq_test_2", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContentToBeTrimmed( - "los_tres_caballeros.csv", - r#" - first_name,last_name,rusty_at,type - Andrés,Robalino,10/11/2013,A - Jonathan,Turner,10/12/2013,B - Yehuda,Katz,10/11/2013,A - Jonathan,Turner,10/12/2013,B - Yehuda,Katz,10/11/2013,A - "#, - )]); - - let actual = nu!( - cwd: dirs.test(), pipeline( - r#" - open los_tres_caballeros.csv - | pick rusty_at type - | uniq - | count - | echo $it - "# - )); - - assert_eq!(actual, "2"); - }) -} - #[test] fn uniq_values() { - Playground::setup("uniq_test_3", |dirs, sandbox| { + Playground::setup("uniq_test_2", |dirs, sandbox| { sandbox.with_files(vec![FileWithContentToBeTrimmed( "los_tres_caballeros.csv", r#" @@ -91,6 +61,70 @@ fn uniq_values() { }) } +#[test] +fn nested_json_structures() { + Playground::setup("uniq_test_3", |dirs, sandbox| { + sandbox.with_files(vec![FileWithContentToBeTrimmed( + "nested_json_structures.json", + r#" + [ + { + "name": "this is duplicated", + "nesting": [ { "a": "a", "b": "b" }, + { "c": "c", "d": "d" } + ], + "can_be_ordered_differently": { + "array": [1, 2, 3, 4, 5], + "something": { "else": "works" } + } + }, + { + "can_be_ordered_differently": { + "something": { "else": "works" }, + "array": [1, 2, 3, 4, 5] + }, + "nesting": [ { "b": "b", "a": "a" }, + { "d": "d", "c": "c" } + ], + "name": "this is duplicated" + }, + { + "name": "this is unique", + "nesting": [ { "a": "b", "b": "a" }, + { "c": "d", "d": "c" } + ], + "can_be_ordered_differently": { + "array": [], + "something": { "else": "does not work" } + } + }, + { + "name": "this is unique", + "nesting": [ { "a": "a", "b": "b", "c": "c" }, + { "d": "d", "e": "e", "f": "f" } + ], + "can_be_ordered_differently": { + "array": [], + "something": { "else": "works" } + } + } + ] + "#, + )]); + + let actual = nu!( + cwd: dirs.test(), pipeline( + r#" + open nested_json_structures.json + | uniq + | count + | echo $it + "# + )); + assert_eq!(actual, "3"); + }) +} + #[test] fn uniq_when_keys_out_of_order() { let actual = nu!( @@ -106,13 +140,3 @@ fn uniq_when_keys_out_of_order() { assert_eq!(actual, "1"); } - -#[test] -fn uniq_nested_json_structures() { - let actual = nu!( - cwd: "tests/fixtures/formats", - "open nested_uniq.json | uniq | count | echo $it" - ); - - assert_eq!(actual, "3"); -} diff --git a/tests/commands/where_.rs b/tests/commands/where_.rs index 4b8f4af979..ca84bd7f99 100644 --- a/tests/commands/where_.rs +++ b/tests/commands/where_.rs @@ -7,7 +7,7 @@ fn filters_by_unit_size_comparison() { "ls | where size > 1kb | sort-by size | get name | first 1 | trim | echo $it" ); - assert_eq!(actual, "nested_uniq.json"); + assert_eq!(actual, "cargo_sample.toml"); } #[test] diff --git a/tests/filters.rs b/tests/filters.rs deleted file mode 100644 index ab29827279..0000000000 --- a/tests/filters.rs +++ /dev/null @@ -1,43 +0,0 @@ -// use nu_test_support::{nu, pipeline}; -// use nu_test_support::playground::Playground; -// use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; - -// #[test] -// fn can_sum() { -// let actual = nu!( -// cwd: "tests/fixtures/formats", h::pipeline( -// r#" -// open sgml_description.json -// | get glossary.GlossDiv.GlossList.GlossEntry.Sections -// | sum -// | echo $it -// "# -// )); - -// assert_eq!(actual, "203") -// } - -// #[test] -// fn can_average_numbers() { -// let actual = nu!( -// cwd: "tests/fixtures/formats", h::pipeline( -// r#" -// open sgml_description.json -// | get glossary.GlossDiv.GlossList.GlossEntry.Sections -// | average -// | echo $it -// "# -// )); - -// assert_eq!(actual, "101.5000000000000") -// } - -// #[test] -// fn can_average_bytes() { -// let actual = nu!( -// cwd: "tests/fixtures/formats", -// "ls | sort-by name | skip 1 | first 2 | get size | average | echo $it" -// ); - -// assert_eq!(actual, "1600.000000000000"); -// } diff --git a/tests/fixtures/formats/fileA.txt b/tests/fixtures/formats/fileA.txt deleted file mode 100644 index 0ce9fb3fa2..0000000000 --- a/tests/fixtures/formats/fileA.txt +++ /dev/null @@ -1,3 +0,0 @@ -VAR1=Chill -VAR2=StupidLongName -VAR3=AlsoChill diff --git a/tests/fixtures/formats/nested_uniq.json b/tests/fixtures/formats/nested_uniq.json deleted file mode 100644 index ac3f132d18..0000000000 --- a/tests/fixtures/formats/nested_uniq.json +++ /dev/null @@ -1,72 +0,0 @@ -[ - { - "name": "this is duplicated", - "nesting": [ - { - "a": "a", - "b": "b" - }, - { - "c": "c", - "d": "d" - } - ], - "can_be_ordered_differently": { - "array": [1, 2, 3, 4, 5], - "something": { "else": "works" } - } - }, - { - "can_be_ordered_differently": { - "something": { "else": "works" }, - "array": [1, 2, 3, 4, 5] - }, - "nesting": [ - { - "b": "b", - "a": "a" - }, - { - "d": "d", - "c": "c" - } - ], - "name": "this is duplicated" - }, - { - "name": "this is unique", - "nesting": [ - { - "a": "b", - "b": "a" - }, - { - "c": "d", - "d": "c" - } - ], - "can_be_ordered_differently": { - "array": [], - "something": { "else": "does not work" } - } - }, - { - "name": "this is unique", - "nesting": [ - { - "a": "a", - "b": "b", - "c": "c" - }, - { - "d": "d", - "e": "e", - "f": "f" - } - ], - "can_be_ordered_differently": { - "array": [], - "something": { "else": "works" } - } - } -]